实现图形验证码功能
- 开发生产图形验证码接口
- 在认证流程中加入图形验证码校验
- 重构代码
开发生产图形验证码接口
- 根据随机数生成图片
- 将随机数存到Session中
- 将生成的图片写到响应的接口中
首先我们定义一个类专门用于表示图形验证码,代码如下:
package com.yun.security.core.validate.code;
import java.awt.image.BufferedImage;
import java.time.LocalDateTime;
public class ImageCode {
public BufferedImage getImage() {
return image;
}
public void setImage(BufferedImage image) {
this.image = image;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public LocalDateTime getExpireTime() {
return expireTime;
}
public void setExpireTime(LocalDateTime expireTime) {
this.expireTime = expireTime;
}
private BufferedImage image;
private String code;
private LocalDateTime expireTime;
public ImageCode(BufferedImage image,String code,LocalDateTime expireTime){
this.image = image;
this.code = code;
this.expireTime =expireTime;
}
public ImageCode(BufferedImage image,String code,int expireIn){
this.image = image;
this.code = code;
this.expireTime =LocalDateTime.now().plusSeconds(expireIn);
}
public Boolean isExpired(){
return LocalDateTime.now().isAfter(expireTime);
}
}
上述这个ImageCode类中,属性image保存了图片,code保存验证码的值,expireTime保存了过期时间。
接下来我们对登录页面进行修改,添加图形验证码:
登陆页面
标准登陆页面
表单登录
从上述html代码中可以看出,验证码图片的来源是一个请求的url段,即在渲染页面的时候首先访问/code/image地址来获取图片。这个请求对应有一个控制器,其代码如下:
@RestController
public class ValidatorController {
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@GetMapping("/code/image")
public void createCode(HttpServletRequest request,HttpServletResponse response) throws Exception{
ImageCode imageCode = createImageCode(request);
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
private ImageCode createImageCode(HttpServletRequest request){
int width = 67;
int height = 23;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
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);
}
String sRand = "";
for (int i = 0; i < 4; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
return new ImageCode(image, sRand, 60);
}
private Color getRandColor(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);
}
}
**粗体文本**
在上述代码中createCode方法处理/code/image请求,并通过createImageCode方法生成图片对象,生成的原理是生成随机数并结合java的awt绘图技术生成图片对象。生成图片验证码后,sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
这句代码表示将这个图片验证码对象放到session中,这样点击登录后,就可以拿这个保存到session中的验证码和实际我们在表单中填写的验证码进行比对判断验证码是否正确。
最后一步就是在填写完表单信息点击登录后首先判断验证码这一步操作,为此我们将这一步写在一个自定义的过滤器中,这个过滤器在UsernamePasswordAuthenticationFilter这个用户名密码验证过滤器之前执行。为此,我们要修改一个配置类:
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
SecurityProperties securityProperties;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailHandler myAuthenticationFailHandler;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception{
ValidateCodeFilter validateCodeFilter =new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(myAuthenticationFailHandler);
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/login.html") //跳转的登录页/authentication/require
.loginProcessingUrl("/authentication/form") //登录时的请求
.successHandler(myAuthenticationSuccessHandler) //表单登录成功时使用我们自己写的处理类
.failureHandler(myAuthenticationFailHandler) //表单登录失败时使用我们自己写的处理类
.and()
.authorizeRequests()
.antMatchers(
securityProperties.getBrowser().getLoginPage(),
"/code/image").permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable();
}
}
上述代码中http.addFilterBefore
这个方法就是在UsernamePasswordAuthenticationFilter过滤器之前加入我们的图片验证码过滤器。
接着看一下这个过滤器的具体实现:
public class ValidateCodeFilter extends OncePerRequestFilter {
private AuthenticationFailureHandler authenticationFailureHandler;
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if(StringUtils.equals("/authentication/form", request.getRequestURI())
&& StringUtils.equals(request.getMethod(), "POST")){
try{
validate(new ServletWebRequest(request));
}
catch(ValidateCodeException e){
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;//当失败时则不执行后面的过滤器
}
}
else{
filterChain.doFilter(request, response);
}
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException{
ImageCode codeInSession = (ImageCode)sessionStrategy.getAttribute(request, ValidatorController.SESSION_KEY);
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
if("".equals(codeInRequest)){
throw new ValidateCodeException("验证码不能为空");
}
if(codeInSession == null){
throw new ValidateCodeException("验证码不存在");
}
if(codeInSession.isExpired()){
sessionStrategy.removeAttribute(request, ValidatorController.SESSION_KEY);
throw new ValidateCodeException("验证码已过期");
}
if(!StringUtils.equals(codeInSession.getCode(), codeInRequest)){
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(request, ValidatorController.SESSION_KEY);
}
}
上述代码中doFilterInternal方法就是过滤器的处理方法,当是表单登录并且是POST请求时调用validate方法进行校验。校验方法主要就是比对session中的验证码和当前请求中的验证码是否相同。
这样我们登录图形验证码功能就初步实现了,登陆页面显示如下: