SpringSecurity jwt认证
1、认证流程

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
|
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("验证码错误"); } 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
|
@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.setAuthenticationSuccessHandler(userLoginSuccessHandler);
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
|
@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(); 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() .antMatchers("/user/login").anonymous() .anyRequest().authenticated() .and() .csrf().disable() .cors() .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .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
|
@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
|
@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;
}
@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、流程分析

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

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

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


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
|
@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); } 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()); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, authorities); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); filterChain.doFilter(request, response); } }
|
在授权过程中会根据存入的SecurityContextHolder中的认证信息进行权限校验,校验成功则可以访问相应资源,校验失败则拒绝访问
配置过滤器,将过滤器添加到过滤器执行链中
1 2
| .addFilterBefore(jwtTokenFilter, RequestCacheAwareFilter.class);
|
3.2、流程分析
可以看到过滤器执行链中多了一条过滤器

流程:
- 除了登录请求,其他所有的请求都会经过这个过滤器,判断有没有携带token
- 如果没有token令牌直接放行交给下个处理器直到执行到最后一个进行授权判断,由于没有token令牌则授权SecurityContextHolder就没有对应的认证信息授权失败
- 如果有存在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);
|
测试
输入一个错误的用户名和密码

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);
|
测试
访问权限与数据库中的权限不一致

补充
其验证逻辑也可以通过这种方式处理
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);
|