一、SpringSecurity表单登录认证原理
- 客户端发起一个请求,进入 Security 过滤器链。
- 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理。如果不是登出路径则直接进入下一个过滤器。
- 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler ,登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
- 进入认证BasicAuthenticationFilter进行用户认证,成功的话会把认证了的结果写入到SecurityContextHolder中SecurityContext的属性authentication上面。如果认证失败就会交给AuthenticationEntryPoint认证失败处理类,或者抛出异常被后续ExceptionTranslationFilter过滤器处理异常,如果是AuthenticationException就交给AuthenticationEntryPoint处理,如果是AccessDeniedException异常则交给AccessDeniedHandler处理。
- 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层,否则到 AccessDeniedHandler 鉴权失败处理器处理。
二、实现验证码 + JWT表单认证
在Spring Security表单登录认证基础上添加验证码过滤器 和 JWT自定义AuthenticationEntryPoint
三、实现
思路:
- 生成验证码code时,需要生成一个key与验证码绑定,并将这一对值保存到内存中;
- 用户在执行登录认证时,无论登录是否成功,都需要删除内存中的验证码和key;
- 创建验证码过滤器,并将该过滤器放在 UsernamePasswordAuthenticationFilter 前;
- 创建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);
}
}