图片验证码
- 图片验证码生成接口
- 认证流程加入图片验证码校验
- 图片验证码重构
1.图片验证码生成接口
- 调用com.google.code.kaptcha.Producer生成图片验证码
- 将随机数存到session缓存中
- 将生成的图片写到响应流中
图片验证码封装类 ImageCaptchaVo
package com.rui.tiger.auth.core.captcha;
import lombok.Data;
import java.awt.image.BufferedImage;
import java.time.LocalDateTime;
/**
* 图片验证码信息对象
* @author CaiRui
* @Date 2018/12/9 18:03
*/
@Data
public class ImageCaptchaVo {
/**
* 图片验证码
*/
private BufferedImage image;
/**
* 验证码
*/
private String code;
/**
* 失效时间 这个通常放在缓存中或维护在数据库中
*/
private LocalDateTime expireTime;
public ImageCaptchaVo(BufferedImage image, String code, int expireAfterSeconds){
this.image = image;
this.code = code;
//多少秒后
this.expireTime = LocalDateTime.now().plusSeconds(expireAfterSeconds);
}
public ImageCaptchaVo(BufferedImage image, String code, LocalDateTime expireTime){
this.image = image;
this.code = code;
this.expireTime = expireTime;
}
/**
* 是否失效
* @return
*/
public boolean isExpried() {
return LocalDateTime.now().isAfter(expireTime);
}
}
图片验证码服务类 CaptchaController 注意这个路径/captcha/image要在BrowserSecurityConfig配置中放行
package com.rui.tiger.auth.core.captcha;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 验证码控制器
* @author CaiRui
* @date 2018-12-10 12:13
*/
@RestController
public class CaptchaController {
public static final String IMAGE_CAPTCHA_SESSION_KEY="image_captcha_session_key";
private static final String FORMAT_NAME="JPEG";
@Autowired
private CaptchaGenerate captchaGenerate;
//spring session 工具类
private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
/**
* 获取图片验证码
* @param request
* @param response
* @throws IOException
*/
@GetMapping("/captcha/image")
public void createKaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
//1.接口生成验证码
ImageCaptchaVo imageCaptcha= (ImageCaptchaVo) captchaGenerate.generate();
//2.保存到session中
sessionStrategy.setAttribute(new ServletWebRequest(request), IMAGE_CAPTCHA_SESSION_KEY, imageCaptcha);
//3.写到响应流中
response.setHeader("Cache-Control", "no-store, no-cache");// 没有缓存
response.setContentType("image/jpeg");
ImageIO.write(imageCaptcha.getImage(),FORMAT_NAME,response.getOutputStream());
}
}
CaptchaGenerate 接口及其实现类
package com.rui.tiger.auth.core.captcha;
/**
* 验证码生成接口
*
* @author CaiRui
* @date 2018-12-10 12:03
*/
public interface CaptchaGenerate {
/**
* 生成图片验证码
*
* @return
*/
ImageCaptchaVo generate();
}
package com.rui.tiger.auth.core.captcha;
import com.google.code.kaptcha.Producer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.awt.image.BufferedImage;
/**
* 图片验证码生成接口
*
* @author CaiRui
* @date 2018-12-10 12:07
*/
@Service("imageCaptchaGenerate")
public class ImageCaptchaGenerate implements CaptchaGenerate {
@Autowired
private Producer producer;//config bean中配置
@Override
public ImageCaptchaVo generate() {
String code = producer.createText();
BufferedImage bufferedImage = producer.createImage(code);
return new ImageCaptchaVo(bufferedImage, code, 60 * 5);//5分钟过期
}
}
Producer配置类 KaptchaGenerateConfig
package com.rui.tiger.auth.core.config;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
/**
* 验证码生成配置类
* @author CaiRui
* @date 2018-12-10 12:09
*/
@Configuration
public class KaptchaGenerateConfig {
//TODO 配置项放在配置文件中
@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", "5");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
2.认证流程加入图片验证码校验
通过上篇的源码分析spring-security原理,其实就是过滤器链上的各个过滤器协同工作,思路如下:
- 编写我们的自定义图片验证码过滤器
- 将它放在UsernamePasswordAuthenticationFilter表单过滤器之前
验证码过滤器
package com.rui.tiger.auth.core.captcha;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang.StringUtils;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 图片验证码过滤器
* OncePerRequestFilter 过滤器只会调用一次
*
* @author CaiRui
* @date 2018-12-10 12:23
*/
public class CaptchaFilter extends OncePerRequestFilter {
//一般在配置类中进行注入
@Setter
@Getter
private AuthenticationFailureHandler failureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//表单登录的post请求
if (StringUtils.equals("/authentication/form", request.getRequestURI())
&& StringUtils.equalsIgnoreCase("post", request.getMethod())) {
try {
validate(request);
} catch (CaptchaException captchaException) {
//失败调用我们的自定义失败处理器
failureHandler.onAuthenticationFailure(request, response, captchaException);
//后续流程终止
return;
}
}
filterChain.doFilter(request, response);
}
/**
* 图片验证码校验
*
* @param request
*/
private void validate(HttpServletRequest request) throws ServletRequestBindingException {
// 拿到之前存储的imageCode信息
ServletWebRequest swr = new ServletWebRequest(request);
ImageCaptchaVo imageCodeInSession = (ImageCaptchaVo) sessionStrategy.getAttribute(swr, CaptchaController.IMAGE_CAPTCHA_SESSION_KEY);
String codeInRequest = ServletRequestUtils.getStringParameter(request, "imageCode");
if (StringUtils.isBlank(codeInRequest)) {
throw new CaptchaException("验证码的值不能为空");
}
if (imageCodeInSession == null) {
throw new CaptchaException("验证码不存在");
}
if (imageCodeInSession.isExpried()) {
sessionStrategy.removeAttribute(swr, CaptchaController.IMAGE_CAPTCHA_SESSION_KEY);
throw new CaptchaException("验证码已过期");
}
if (!StringUtils.equals(imageCodeInSession.getCode(), codeInRequest)) {
throw new CaptchaException("验证码不匹配");
}
//验证通过 移除缓存
sessionStrategy.removeAttribute(swr, CaptchaController.IMAGE_CAPTCHA_SESSION_KEY);
}
}
自定义验证码异常
package com.rui.tiger.auth.core.captcha;
import org.springframework.security.core.AuthenticationException;
/**
* 自定义验证码异常
* @author CaiRui
* @date 2018-12-10 12:43
*/
public class CaptchaException extends AuthenticationException {
public CaptchaException(String msg, Throwable t) {
super(msg, t);
}
public CaptchaException(String msg) {
super(msg);
}
}
将过滤器加入到浏览器权限配置中
package com.rui.tiger.auth.browser.config;
import com.rui.tiger.auth.core.authentication.TigerAuthenticationFailureHandler;
import com.rui.tiger.auth.core.authentication.TigerAuthenticationSuccessHandler;
import com.rui.tiger.auth.core.captcha.CaptchaFilter;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* 浏览器security配置类
*
* @author CaiRui
* @date 2018-12-4 8:41
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private TigerAuthenticationFailureHandler tigerAuthenticationFailureHandler;
@Autowired
private TigerAuthenticationSuccessHandler tigerAuthenticationSuccessHandler;
/**
* 密码加密解密
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//加入图片验证码过滤器
CaptchaFilter captchaFilter=new CaptchaFilter();
captchaFilter.setFailureHandler(tigerAuthenticationFailureHandler);
//图片验证码放在认证之前
http.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage( "/authentication/require")//自定义登录请求
.loginProcessingUrl("/authentication/form")//自定义登录表单请求
.successHandler(tigerAuthenticationSuccessHandler)
.failureHandler(tigerAuthenticationFailureHandler)
.and()
.authorizeRequests()
.antMatchers(securityProperties.getBrowser().getLoginPage(),
"/authentication/require","/captcha/image")//此路径放行 否则会陷入死循环
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable()//跨域关闭
;
}
}
前段标准登录界面加入验证码改造
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h2>标准登录页面</h2>
<h3>表单登录</h3>
<form action="/authentication/form" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td>图形验证码:</td>
<td>
<input type="text" name="imageCode">
<img src="/captcha/image">
</td>
</tr>
<tr>
<td colspan="2"><button type="submit">登录</button></td>
</tr>
</table>
</form>
</body>
</html>
ok 我们项目重新启动下 来看下自定义的验证码过滤器是否可用,直接浏览器输入我们的登录地址http://localhost:8070/tiger-login.html
可以看到图片验证码已经成功显示出来了,我们来看看验证逻辑是否可用,试下不输入验证码
这是因为我们自定义的失败处理器,打印了全部的错误堆栈信息我们来调整下,调整后如下
package com.rui.tiger.auth.core.authentication;
import com.alibaba.fastjson.JSON;
import com.rui.tiger.auth.core.model.enums.LoginTypeEnum;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import com.rui.tiger.auth.core.support.SimpleResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 认证失败处理器
* @author CaiRui
* @date 2018-12-6 12:40
*/
@Component("tigerAuthenticationFailureHandler")
@Slf4j
public class TigerAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private SecurityProperties securityProperties;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("登录失败");
if (LoginTypeEnum.JSON.equals(securityProperties.getBrowser().getLoginType())) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(new SimpleResponse(exception.getMessage())));
} else {
// 如果用户配置为跳转,则跳到Spring Boot默认的错误页面
super.onAuthenticationFailure(request, response, exception);
}
}
}
这次我们试下一个错误的验证码登录看下
ok 我们的自定义验证码生效,其它的情况可以自行调试 下面我们将对验证码进行重构
3.图片验证码重构
- 验证码生成的基本参数可以配置
- 验证码拦截的接口可以配置
- 验证码的生成逻辑可以配置