- spring security的认证流程
2. 从文档上可以看出来,UsernamePasswordAuthenticationFilter和AuthenticationManager是认证的关键步骤,/login传过来的username和password由UsernamePasswordAuthenticationFilter接收处理成UsernamePasswordAuthenticationToken,再传给AuthenticationManager,在AuthenticationManager中判断密码正确与否
3. 代码实现
(一)、登录部分:登录可以使用两种方式进行登陆
第一种:只用spring security默认提供的/login接口进行登录(这种方式需要修改spring security默认的登录设置,比较办法)
第二种:自己写登陆的controller(这种比较简单就不研究了)
第一步 WebSecurityConfig配置文件
package com.xuhao.springsecuritycom.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xuhao.springsecuritycom.filters.CustomUsernamePasswordAuthenticationFilter;
import com.xuhao.springsecuritycom.filters.TokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableWebSecurity
// 开启注解模式,否则权限注解会无效
@EnableGlobalMethodSecurity(
prePostEnabled = true, // 启用@PreAuthorize和@PostAuthorize注解
securedEnabled = true // 启用@Secured注解)
)
public class SecurityConfig {
@Autowired
private TokenFilter tokenFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests((requests) -> requests
.requestMatchers(HttpMethod.POST,"/user/create", "/login").permitAll()
.anyRequest().authenticated()
)
.logout((logout) -> logout.permitAll());
// 设置无权访问时的返回
httpSecurity.exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler());
// 设置未登录,访问受保护地址的拦截
httpSecurity.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint());
// 配置token拦截过滤器,这个过滤器很重要,用来判断用户是否登录的逻辑
httpSecurity.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
// 关闭csrf,否则ajax无法通过拦截
httpSecurity.csrf().disable();
// 不通过Session获取SecurityContext
httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
return httpSecurity.build();
}
/**
* 设置加密规则
* PasswordEncoderFactories.createDelegatingPasswordEncoder()
* 创建的DelegatingPasswordEncoder会自动根据配置的密码存储类型来选择合适的加密规则
* */
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
/**
* 这是登录过滤器的bean
*/
@Bean
public CustomUsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() {
CustomUsernamePasswordAuthenticationFilter loginFilter = new CustomUsernamePasswordAuthenticationFilter();
loginFilter.setAuthenticationManager(customAuthenticationManager());
return loginFilter;
}
// 设置判断登录成功的实现类的bean
@Bean
public CustomAuthenticationManager customAuthenticationManager() {
return new CustomAuthenticationManager();
}
}
第二步 重写登录接口/login的逻辑
package com.xuhao.springsecuritycom.filters;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 重写登录拦截过滤器
* */
public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
// 接收请求参数
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = this.obtainUsername(request);
username = username != null ? username.trim() : "";
String password = this.obtainPassword(request);
password = password != null ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
// 设置/login接口登录成功以后得接口返回
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
Map<String,Object> map = new HashMap<>();
map.put("code",200);
map.put("msg","登陆成功");
// 获取用户信息这里也可以把生成的token返回给前端,写死token懒得接jwt了,后面就判断token=1就是登录了
map.put("token","1");
map.put("authentication",authResult);
String string = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(string);
}
// 设置/login接口登录失败以后得接口返回
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
Map<String,Object> map = new HashMap<>();
map.put("code",302);
map.put("msg","登陆失败");
String string = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(string);
}
}
第三步 重写AuthenticationManager,用于判断密码是否正确
package com.xuhao.springsecuritycom.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.ArrayList;
import java.util.Collection;
public class CustomAuthenticationManager implements AuthenticationManager {
@Autowired
PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 从传入的 Authentication 对象中获取用户提供的凭证信息,比如用户名和密码
String username = authentication.getName();
String password = authentication.getCredentials().toString();
// 写死密码123456用户名xuhao,开发时应该是从数据库获取到的密码
String contrastPaw = passwordEncoder.encode("123456");
// 模拟一个权限列表
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_ADD"));
if(username.equals("xuhao") && passwordEncoder.matches(password, contrastPaw)) {
return new UsernamePasswordAuthenticationToken(username, password, authorities);
} else {
throw new BadCredentialsException("Invalid username or password");
}
}
}
第四步 重写未登录,未授权的异常返回
package com.xuhao.springsecuritycom.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 未授权访问拦截
* */
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Map<String,Object> map = new HashMap<>();
map.put("code",403);
map.put("msg","您无权访问此地址");
String string = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(string);
}
}
package com.xuhao.springsecuritycom.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 未登录返回拦截
* */
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Map<String,Object> map = new HashMap<>();
map.put("code",401);
map.put("msg","请登录后访问");
String string = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(string);
}
}
第五步 写测试接口
package com.xuhao.springsecuritycom.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/user")
public class User {
@Autowired
PasswordEncoder encoder;
@PostMapping("/create")
public ResponseEntity<?> createUser(@RequestParam String userName, @RequestParam String password) {
Map<String, String> map = new HashMap<>();
map.put("userName", userName);
map.put("password", encoder.encode(password));
System.out.println(encoder.matches("123456", encoder.encode(password)));
return ResponseEntity.ok().body(map);
}
@GetMapping("/see")
// 测试测试权限,全局写死权限为ROLE_ADD,所以访问SEE时就会被权限过滤器拦截
@Secured("ROLE_SEE")
public ResponseEntity<?> seeUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return ResponseEntity.ok().body(authentication);
}
}
附上源码地址: spring security6示例代码