SpringSecurity + JWT 实现登录认证

514 阅读4分钟

一、SpringSecurity表单登录认证原理

image.png

  1. 客户端发起一个请求,进入 Security 过滤器链。
  2. 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理。如果不是登出路径则直接进入下一个过滤器。
  3. 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler ,登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
  4. 进入认证BasicAuthenticationFilter进行用户认证,成功的话会把认证了的结果写入到SecurityContextHolder中SecurityContext的属性authentication上面。如果认证失败就会交给AuthenticationEntryPoint认证失败处理类,或者抛出异常被后续ExceptionTranslationFilter过滤器处理异常,如果是AuthenticationException就交给AuthenticationEntryPoint处理,如果是AccessDeniedException异常则交给AccessDeniedHandler处理。
  5. 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层,否则到 AccessDeniedHandler 鉴权失败处理器处理。

二、实现验证码 + JWT表单认证

image.png

在Spring Security表单登录认证基础上添加验证码过滤器JWT自定义AuthenticationEntryPoint

三、实现

思路

  1. 生成验证码code时,需要生成一个key与验证码绑定,并将这一对值保存到内存中;
  2. 用户在执行登录认证时,无论登录是否成功,都需要删除内存中的验证码和key;
  3. 创建验证码过滤器,并将该过滤器放在 UsernamePasswordAuthenticationFilter 前;
  4. 创建JWT验证过滤器,用来验证在非登录请求时用户是否合法。
1、用户执行登录认证操作

1、验证码获取

<dependency>
    <groupId>com.github.axet</groupId>
    <artifactId>kaptcha</artifactId>
    <version>0.0.9</version>
</dependency>
@Bean
public DefaultKaptcha producer() {
    Properties properties = new Properties();
    properties.put("kaptcha.border", "no");
    properties.put("kaptcha.textproducer.font.color", "black");
    properties.put("kaptcha.textproducer.char.space", "4");
    properties.put("kaptcha.image.height", "40");
    properties.put("kaptcha.image.width", "120");
    properties.put("kaptcha.textproducer.font.size", "30");
    Config config = new Config(properties);
    DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
    defaultKaptcha.setConfig(config);
    return defaultKaptcha;
}
// 验证码生成器
@Autowired
private Producer producer;
@Autowired
private RedisTemplate<String, Object> redisTemplate;

@GetMapping("/captcha")
public CommonResult captcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
    // 获取验证码
    String code = producer.createText();
    String key = UUID.randomUUID().toString();
    BufferedImage image = producer.createImage(code);
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    ImageIO.write(image, "jpg", outputStream);
    BASE64Encoder encoder = new BASE64Encoder();
    String str = "data:image/jpeg;base64,";
    String base64Img = str + encoder.encode(outputStream.toByteArray());

    redisTemplate.opsForValue().set(RedisKey.KAPTCHA_KEY.getName() + key, code, 30, TimeUnit.MINUTES);
    log.info("验证码 -- {} - {}", key, code);
    Map<String, String> resultData = new HashMap<>();
    resultData.put("token", key);
    resultData.put("base64Img", base64Img);
    return CommonResult.success(resultData);
}

2、验证码过滤器

@Component
public class CaptchaFilter extends OncePerRequestFilter {
    @Value("${login.url}")
    private String LOGIN_URL;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private LoginFailureHandler loginFailureHandler;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.debug("CaptchaFilter begin ------------");
        String url = request.getRequestURI();
        if (LOGIN_URL.equals(url) && request.getMethod().equals("POST")) {
            log.info("获取到login链接,正在校验验证码 -- {}", url);
            try {
                validate(request);
            } catch (CaptchaException e) {
                log.info(e.getMessage());
                loginFailureHandler.onAuthenticationFailure(request, response, e);
            }
        }
        log.debug("CaptchaFilter end ------------");
        filterChain.doFilter(request, response);
    }

    private void validate(HttpServletRequest request) throws CaptchaException {
        String code = request.getParameter("code");
        String token = request.getParameter("token");
        if (StringUtils.isEmpty(code) || StringUtils.isEmpty(token)) {
            throw new CaptchaException("验证码不可为空");
        }
        Object realKey = redisTemplate.opsForValue().get(RedisKey.KAPTCHA_KEY.getName() + token);
        if (!code.equals(realKey)) {
            throw new CaptchaException("验证码不正确");
        }
        redisTemplate.delete(RedisKey.KAPTCHA_KEY.getName() + token);
    }
}

3、根据需要配置认证成功失败处理器

@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private JwtUtils jwtUtils;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        ServletOutputStream outputStream = null;
        try {
            response.setContentType("application/json;charset=utf-8");
            outputStream = response.getOutputStream();
            String jwt = jwtUtils.generateToken(authentication.getName());
            response.setHeader(jwtUtils.getHeader(), jwt);
            CommonResult result = CommonResult.success();
            ObjectMapper objectMapper = new ObjectMapper();
            String s = objectMapper.writeValueAsString(result);
            outputStream.write(s.getBytes(StandardCharsets.UTF_8));
            outputStream.flush();
        } finally {
            if (outputStream != null) {
                outputStream.close();
            }
        }

    }
}

2、用户执行不是登录的请求

1、配置JWT验证过滤器

@Slf4j
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {

    @Autowired
    private JwtUtils jwtUtils;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {
        super(authenticationManager, authenticationEntryPoint);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("jwt 校验filter");
        String jwt = request.getHeader(jwtUtils.getHeader());
        if (StringUtils.isEmpty(jwt)) {
            chain.doFilter(request, response);
            return;
        }
        Claims claimByToken = jwtUtils.getClaimByToken(jwt);
        if (claimByToken == null) {
            throw new JwtException("token异常");
        }
        if (jwtUtils.isTokenExpired(claimByToken)) {
            throw new JwtException("token过期");
        }
        String username = claimByToken.getSubject();
        log.info("用户 {} 正在登录", username);
        UsernamePasswordAuthenticationToken token =
                new UsernamePasswordAuthenticationToken(username, null, new TreeSet<>());
        SecurityContextHolder.getContext().setAuthentication(token);
        chain.doFilter(request, response);
    }
}

2、JWT认证失败处理节点、权限不足处理器

@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        log.info("登录认证失败");
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            CommonResult result = CommonResult.fail(authException.getMessage());
            ObjectMapper objectMapper = new ObjectMapper();
            String s = objectMapper.writeValueAsString(result);
            outputStream.write(s.getBytes(StandardCharsets.UTF_8));
            outputStream.flush();
        } finally {
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            ObjectMapper objectMapper = new ObjectMapper();
            String result = objectMapper.writeValueAsString(CommonResult.fail(accessDeniedException.getMessage()));
            outputStream.write(result.getBytes(StandardCharsets.UTF_8));
            outputStream.flush();
        } finally {
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

3、将验证码验证过滤器、JWT验证过滤器、认证失败成功处理器等添加到Security配置中

@Configuration
@EnableWebSecurity(debug = false)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

...

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.cors().and().csrf().disable()
            .formLogin()
            .failureHandler(loginFailureHandler)
            .successHandler(loginSuccessHandler)
            .and()
            .logout()
            .logoutSuccessHandler(logoutSuccessHandler) // 登录成功处理器
            .and()
            .authorizeRequests()
            .antMatchers(URL_WHITE_LIST)
            .permitAll()
            .anyRequest().authenticated()
            .and()
            .exceptionHandling()
            .authenticationEntryPoint(jwtAuthenticationEntryPoint) // jwt认证失败处理节点
            .accessDeniedHandler(jwtAccessDeniedHandler) //jwt认证时用户权限不足处理器
            .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and().addFilter(jwtAuthenticationFilter()) // jwt认证过滤器
            .addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);
}

/**
 * 配置认证时用户数据获取方式,userDetailsService自行配置
 * @param auth
 * @throws Exception
 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService);
}

}