SpringSecurity jwt认证

1、认证流程

image-20231101181239583

2、登录

2.1、自定义登录配置器

2.2、过滤器

通过自定义过滤器可以实现多条件校验,例如校验验证码,默认的登录过滤器UsernamePasswordAuthenticationFilter只能校验用户名和密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* <p>
* 自定义用户认证filter
* </p>
*/

public class UserAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

private static final AntPathRequestMatcher ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/user/login", "POST");

protected UserAuthenticationFilter() {
super(ANT_PATH_REQUEST_MATCHER);
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
String username = request.getParameter("username");
String password = request.getParameter("password");
String checkCode = request.getParameter("checkCode");
//校验验证码是否正确
if(!StringUtils.hasText(checkCode)){
throw new RuntimeException("验证码不能为空");
}
if(!"11111".equals(checkCode)){
throw new RuntimeException("验证码错误");
}
//TODO 其他校验逻辑

UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
return this.getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
}
}

2.2、登录配置器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 自定义登录配置器
* @param <H>
*/
@Component
public final class UserLoginConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<UserLoginConfigurer<H>, H> {


@Autowired
private UserLoginSuccessHandler userLoginSuccessHandler;

@Override
public void configure(H http) {
UserAuthenticationFilter authFilter = new UserAuthenticationFilter();
authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// authFilter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());

authFilter.setAuthenticationSuccessHandler(userLoginSuccessHandler);
// // 登录失败处理器
// authFilter.setAuthenticationFailureHandler(new HttpStatusLoginFailureHandler());

// 拦截器位置
// UserAuthenticationFilter filter = postProcess(authFilter);
http.addFilterAfter(authFilter, LogoutFilter.class);
}
}

2.3、登录成功处理器

登录成功后给前端响应一个jwt token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* <p>
* 登录成功处理器
* </p>
*
* @author YangAns
* @since 2023/11/1 - 13:53
*/
@Component
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler {

@Autowired
private RedisTemplate<String, String> redisTemplate;


@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
LoginUser principal = (LoginUser) authentication.getPrincipal();
Long id = principal.getUser().getId();
//生成jwt令牌,返回给前端
String jwt = JwtUtil.createJWT(String.valueOf(id));
System.out.println(principal);
ResponseResult<String> result = new ResponseResult<>(200, "登录成功", jwt);
redisTemplate.opsForValue().set("login:" + id, JSON.toJSONString(principal));
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
}
}

2.4、WebSecurityConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Configuration
@EnableWebSecurity
//开启权限校验注解
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

private final UserLoginConfigurer<HttpSecurity> userLoginConfigurer;


private final JwtTokenFilter jwtTokenFilter;

//自定义密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}


//过滤器链配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
//自定义登录接口,anonymous()表示只有在登录认证的时候才能访问
.antMatchers("/user/login").anonymous()
.anyRequest().authenticated()
.and()
// 禁用跨站点伪造请求
.csrf().disable()
//启用跨域资源
.cors()
.and()
//禁用Session会话机制,jwt方式不用开启
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//将登录配置器放入HttpSecurity中
.apply(userLoginConfigurer).and()
.addFilterBefore(jwtTokenFilter, RequestCacheAwareFilter.class);
}
}

2.5、自定义UserDetailServiceImpl

通过实现UserDetailsService的方式来处理自已登录检验逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* <p>
* 自定义UserDetailServiceImpl
* </p>
*/

@Component
@RequiredArgsConstructor
public class UserDetailServiceImpl implements UserDetailsService {

private final UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUserName, username));
if (user == null) {
throw new RuntimeException("用户不存在");
}
//权限 从数据库中获取
List<String> list = Arrays.asList("test");

LoginUser loginUser = new LoginUser(user, list);
return loginUser;
}
}

2.6、自定义LoginUser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* <p>
* LoginUser
* </p>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class LoginUser implements UserDetails, Serializable {

private User user;


private List<String> permissions;

private Set<GrantedAuthority> authorities;

public LoginUser(User user,List<String> permissions){
this.permissions = permissions;
this.user = user;
// this.authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}

@Override
public String getPassword() {
return user.getPassword();
}

@Override
public String getUsername() {
return user.getUserName();
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}

2.7、流程分析

image-20231101183512312

自定义的登录过滤器UserAuthenticationFilter成功添加到过滤器链中

进入到UserDetailServiceImpl中

image-20231101183837192

从数据库中查询用户信息,将用户信息封装成一个UserDetails对象返回,如果认证成功则进去登录成功处理器,进一步处理

image-20231101184110712

最后将获取到的用户信息,根据用户id生成一个jwt令牌返回给前端,最后将用户信息存入redis中

image-20231101184346998

image-20231101184513100

3、授权

3.1、自定义Jwt认证过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* <p>
* 校验jwt
* </p>
*/
@Component
@RequiredArgsConstructor
public class JwtTokenFilter extends OncePerRequestFilter {
private final RedisTemplate<String, String> redisTemplate;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//没有携带令牌则放行交给下一个处理器
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
filterChain.doFilter(request, response);
return;
}
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
if (claims == null) {
throw new RuntimeException("token非法");
}
userId = claims.getSubject();
} catch (Exception e) {
throw new RuntimeException(e);
}

//从redis中获取用户信息
String redisKey = "login:" + userId;
String jsonString = redisTemplate.opsForValue().get(redisKey);
LoginUser loginUser = JSON.parseObject(jsonString, LoginUser.class);
assert loginUser != null;



List<String> permissions = loginUser.getPermissions();
Set<SimpleGrantedAuthority> authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
//将redis中的用户信息封装成一个UsernamePasswordAuthenticationToken对象存入SecurityContextHolder中
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, authorities);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(request, response);
}
}

在授权过程中会根据存入的SecurityContextHolder中的认证信息进行权限校验,校验成功则可以访问相应资源,校验失败则拒绝访问

配置过滤器,将过滤器添加到过滤器执行链中

1
2
 //添加到RequestCacheAwareFilter之后
.addFilterBefore(jwtTokenFilter, RequestCacheAwareFilter.class);

3.2、流程分析

可以看到过滤器执行链中多了一条过滤器

image-20231101190136571

流程:

  1. 除了登录请求,其他所有的请求都会经过这个过滤器,判断有没有携带token
  2. 如果没有token令牌直接放行交给下个处理器直到执行到最后一个进行授权判断,由于没有token令牌则授权SecurityContextHolder就没有对应的认证信息授权失败
  3. 如果有存在token令牌,且token令牌有效则从获取用户id,根据用户id从redis中取出用户信息(包括权限信息)存入SecurityContextHolder中,放行直到执行到最后一个过滤器,根据SecurityContextHolder中的用户信息授予对应的访问权限

4、异常处理

在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可

4.1、处理登录认证过程中异常

认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。

自定义实现类

1
2
3
4
5
6
7
8
9
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
}
}

SpringSecurityConfig

1
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);

测试

输入一个错误的用户名和密码

image-20231101232259868

4.1、处理授权过程过程中异常

授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理

1
2
3
4
5
6
7
8
9
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "权限不足");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
}
}

SpringSecurityConfig

1
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);

测试

访问权限与数据库中的权限不一致

image-20231101232755230

补充

其验证逻辑也可以通过这种方式处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class CheckCodeFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String defaultFilterProcessUrl = "/login";
if ("POST".equalsIgnoreCase(request.getMethod()) && defaultFilterProcessUrl.equals(request.getServletPath())) {
// 验证码验证
String requestCaptcha = request.getParameter("code");
String genCaptcha = (String) request.getSession().getAttribute("index_code");
if (StringUtils.isEmpty(requestCaptcha))
throw new AuthenticationServiceException("验证码不能为空!");
if (!genCaptcha.toLowerCase().equals(requestCaptcha.toLowerCase())) {
throw new AuthenticationServiceException("验证码错误!");
}
}
filterChain.doFilter(request, response);
}

}

http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);