05. Spring Security 图形验证码
参考:
https://blog.csdn.net/yuanlaijike/article/details/80253922
https://mrbird.cc/Spring-Security-ValidateCode.html
#依赖
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-config</artifactId>
<version>1.1.6.RELEASE</version>
</dependency>
#验证码对象Image
/**
* 验证码
*/
@Data
public class ImageCode {
//允许出现的序列值
private char[] codeSequence = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
//有效时间
private LocalDateTime ttl;
//验证码内容
private String code;
//验证码长度
private Integer codeCount = 4;
private BufferedImage bufferedImage;
public ImageCode(Integer ttl, BufferedImage bufferedImage) {
this.ttl = LocalDateTime.now().plusSeconds(ttl);
this.bufferedImage = bufferedImage;
}
public ImageCode(Integer ttl, String code, BufferedImage bufferedImage) {
this.ttl = LocalDateTime.now().plusSeconds(ttl);
this.code = code;
this.bufferedImage = bufferedImage;
}
/**
* 判断验证码是否过期
*/
public boolean isExpire() {
return LocalDateTime.now().isAfter(ttl);
}
}
#生成验证码的controller
@Slf4j
@RestController
public class ValidateController {
public final static String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE";
//无法显示导包
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
/**
* 生成验证码请求
*/
@GetMapping("/code/image")
public void createImageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = createImageCode(100, 36, 60);
//存储一个session key 为SESSION_KEY_IMAGE_CODE, 值为imageCode的到session域中
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode);
log.warn("验证码"+imageCode.getCode());
//将图片写入到前端
ImageIO.write(imageCode.getBufferedImage(), "png", response.getOutputStream());
}
/**
* @param width 验证码图片宽度
* @param height 验证码图片长度
* @param ttl 验证码有效时间 60s
* @return
*/
private ImageCode createImageCode(int width, int height, int ttl) {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
//设置颜色
g.setColor(getRandomColor(200, 259));
//设置形状
g.fillRect(0, 0, width, height);
//设置字体颜色
g.setColor(getRandomColor(160, 200));
//设置字体
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
//干扰线
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
StringBuilder condeStr = new StringBuilder();
ImageCode imageCode = new ImageCode(ttl, image);
char[] codeSequence = imageCode.getCodeSequence();
//验证码内容
for (int i = 0; i < imageCode.getCodeCount(); i++) {
//随机获取校验内容
String rand = String.valueOf(codeSequence[random.nextInt(codeSequence.length)]);
//为每个字符设置不同的颜色
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 17 * i + 6, 25);
condeStr.append(rand);
}
//将验证码的内容封装到对象中以便做对比
imageCode.setCode(condeStr.toString());
g.dispose();
return imageCode;
}
/**
* 获取随机颜色
*/
private Color getRandomColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
#html
<form class="login-page" th:action="@{/login}" method="post">
<div class="form">
<h3>账户登录</h3>
<input type="text" placeholder="用户名" name="username" required="required"/>
<input type="password" placeholder="密码" name="password" required="required"/>
<!-- 自动登入设置的name必须是remember-me-->
<span><input type="checkbox" name="remember-me"/>自动登录<span/>
<!-- 图形验证码-->
<span style="display: inline">
<input type="text" name="imageCode" placeholder="验证码" style=" 50%;"/>
<img src="/code/image"/>
</span>
<button type="submit">登录</button>
</div>
</form>
img 的 src属性对应@GetMapping("/code/image")
#修改配置类
/**
* 校验图形验证码
*/
@Bean
public OncePerRequestFilter validatedCodeFilter() {
return new ValidateCodeFilter();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
//放行图形验证码
.antMatchers("/login", "/code/image").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login")
.successForwardUrl("/")
.failureForwardUrl("/fail2")
.and()
.logout().permitAll()
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.and()
.rememberMe()
.tokenValiditySeconds(60)
.tokenRepository(persistentTokenRepository())
.userDetailsService(userDetailsService)
.and()
.csrf()
.disable();
}
启动后, 效果如下
#添加验证码校验
在校验验证码的过程中,可能会抛出各种验证码类型的异常,比如“验证码错误”、“验证码已过期”等,所以我们定义一个验证码类型的异常类:
/**
* 自定义验证码错误
*/
public class ValidateCodeException extends AuthenticationException {
private static final long serialVersionUID = 1L;
public ValidateCodeException(String msg) {
super(msg);
}
}
注意,这里继承的是AuthenticationException
而不是Exception
。
我们都知道,Spring Security实际上是由许多过滤器组成的过滤器链,处理用户登录逻辑的过滤器为UsernamePasswordAuthenticationFilter
,而验证码校验过程应该是在这个过滤器之前的,即只有验证码校验通过后才去校验用户名和密码。由于Spring Security并没有直接提供验证码校验相关的过滤器接口,所以我们需要自己定义一个验证码校验的过滤器ValidateCodeFilter
:
- 方法一: 重定向
/**
* 自定义过滤器,如果没有权限验证,每发送一次请求调用一次
*/
public class ValidateCodeFilter extends OncePerRequestFilter {
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//只对登入请求校验
if (isProtectedUrl(request)) {
//将httpServlet包装为ServletWebRequest
try {
validateCode(new ServletWebRequest(request));
} catch (ValidateCodeException e) {
//如果校验失败,请求转发
request.getRequestDispatcher("/fail").forward(request,response);
}
}
//放行请求
filterChain.doFilter(request, response);
}
/**
* 校验验证码
* @param servletWebRequest 通过ServletWebRequest可以拿到webRequest(所有的web request请求)是一个适配器
* @throws ServletRequestBindingException
*/
private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {
//通过session key获取到对应的imageCode
ImageCode codeInSession = (ImageCode) sessionStrategy
.getAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE);
//从request获取name为imageCode的值
String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode");
if (StringUtils.isBlank(codeInRequest)){
throw new ValidateCodeException("验证码不能为空");
}else if (ObjectUtils.isEmpty(codeInSession)){
throw new ValidateCodeException("验证码不存在");
}else if (codeInSession.isExpire()){
throw new ValidateCodeException("验证码过期");
}else if (!StringUtils.equalsIgnoreCase(codeInSession.getCode(),codeInRequest)){
throw new ValidateCodeException("验证码不正确");
}
//如果校验通过, 删除session中的imageCode
sessionStrategy.removeAttribute(servletWebRequest,ValidateController.SESSION_KEY_IMAGE_CODE);
}
/**
* 过滤请求
*/
private boolean isProtectedUrl(HttpServletRequest request) {
return StringUtils.equalsIgnoreCase("/login", request.getRequestURL())
&& StringUtils.equalsIgnoreCase("post", request.getMethod());
}
}
- 方法二: failureHandler
Handler
public class FailureHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper mapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(mapper.writeValueAsString(exception.getMessage()));
}
}
修改ValidateCodeFilter
@Autowired
AuthenticationFailureHandler failureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//只对登入请求校验
if (isProtectedUrl(request)) {
//将httpServlet包装为ServletWebRequest
try {
validateCode(new ServletWebRequest(request));
} catch (ValidateCodeException e) {
failureHandler.onAuthenticationFailure(request,response,e);
//这里必须要return,否则还是会执行doFilter,验证码不正确也会校验成功
return;
}
}
//放行请求
filterChain.doFilter(request, response);
}
配置类
@Bean
public AuthenticationFailureHandler failureHandler(){
return new FailureHandler();
}
/**
* 校验图形验证码
*/
@Bean
public OncePerRequestFilter validatedCodeFilter() {
return new ValidateCodeFilter();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//添加过滤器
http.addFilterBefore(validatedCodeFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
//放行图形验证码
.antMatchers("/login", "/code/image").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login")
.successForwardUrl("/")
.failureHandler(failureHandler())
.and()
.logout().permitAll()
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.and()
.rememberMe()
.tokenValiditySeconds(60)
.tokenRepository(persistentTokenRepository())
.userDetailsService(userDetailsService)
.and()
.csrf()
.disable();
}