图片验证码是当今使用最广泛的用户验证方式之一,在之前实现了简单的表单登录的基础上,今天用spring security来实现一下图片验证码的实现过程。
1.demo结构
2.实现过程
首先创建ImageCode实体类,定义图片验证码各参数。
ImageCode.java
package com.security.validate.code;
import lombok.Data;
import java.awt.image.BufferedImage;
import java.time.LocalDateTime;
/**
* @author ShotMoon
*/
@Data
public class ImageCode {
//验证码图片
private BufferedImage image;
//验证码随机数字
private String code;
//验证码失效时间
private LocalDateTime expireTime;
//验证码过期与否
public boolean isExpired(){
return LocalDateTime.now().isAfter(expireTime);
}
public ImageCode(BufferedImage image, String code, int expireIn) {
this.image = image;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
this.image = image;
this.code = code;
this.expireTime = expireTime;
}
}
登录页面添加图片验证码显示
default-loginPage.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>security login</title>
</head>
<body>
<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="/code/image">
</td>
</tr>
<tr>
<td colspan="2"><button type="submit">登录</button></td>
</tr>
</table>
</form>
</body>
</html>
登录时页面会向"/code/image"发送一个GET请求来获取图片验证码,我们来写一下图片验证码的生成逻辑。
ValidateController.java:
package com.security.validate.code;
import com.security.properties.SecurityProperties;
import org.apache.catalina.servlet4preview.http.HttpServletRequest;
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.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
/**
* @author ShotMoon
*/
@RestController
public class ValidateController {
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";//图片验证码存入session时,作为Key
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private SecurityProperties securityProperties;
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = createImageCode(request);
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);//将验证码存入session,留验证之用
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());//将验证码图片以jpeg格式写入输出流,以在页面显示
}
private ImageCode createImageCode(HttpServletRequest request) {
int width = 65; //验证码图片长度
int height = 25;//宽
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 < securityProperties.getImageCodeProperties().getLength(); 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, securityProperties.getImageCodeProperties().getExpireIn());
}
/**
* 生成随机背景条纹
*
*/
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);
}
}
经上述步骤,我们的验证码已经可以在页面显示了,效果如下图,
接下来是校验的逻辑,还记得我们之前说过,spring security的本质是一系列的过滤器组成的过滤器链吗,我们只需要自定义一个过滤器并将其插入到spring security的过滤器链上,就可以执行我们自定义的认证逻辑了。
ValidateCodeFilter.java:
package com.security.validate.code;
import lombok.Data;
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;
/**
* @author ShotMoon
*/
@Data
public class ValidateCodeFilter extends OncePerRequestFilter { //保证过滤器每次请求只会被调用一次
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//当请求路径为/authentication/form,且请求为POST请求时,才执行验证。(对应登录页面发送的请求)
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);
}
}
filterChain.doFilter(request, response);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException, ValidateCodeException {
//从请求中取出之前存入session的验证码
ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request,
ValidateController.SESSION_KEY);
//获取form表单中用户输入的验证码
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode"); //对应form表单中图片name
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("验证码不能为空");
}
if (codeInSession == null) {
throw new ValidateCodeException("验证码不存在");
}
if (codeInSession.isExpired()) {
sessionStrategy.removeAttribute(request, ValidateController.SESSION_KEY);
throw new ValidateCodeException("验证码已过期");
}
if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(request, ValidateController.SESSION_KEY);
}
}
ValidateCodeException.java:
package com.security.validate.code;
import org.springframework.security.core.AuthenticationException;
/**
* @author ShotMoon
*/
public class ValidateCodeException extends AuthenticationException {
public ValidateCodeException(String msg) {
super(msg);
}
}
然后,我们将定义好的过滤器插入到UsernamePasswordAuthenticationFilter前面,来验证图片验证码
SecurityConfig.java:
package com.security.config;
import com.security.authentication.MyAuthenticationFailureHandler;
import com.security.authentication.MyAuthenticationSuccessHandler;
import com.security.validate.code.ValidateCodeFilter;
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;
/**
* @author ShotMoon
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
/**
* @description :
* @param : [http]
* @return : void
* @date : 2018/5/13 13:41
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) //插入目标过滤器之前
.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler)
.and()
.authorizeRequests()
.antMatchers( //不需验证的请求路径,防止出现redirect too many times死循环
"/default-loginPage.html",
"/customized-loginPage.html",
"/authentication/require",
"/code/image"
).permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable();
}
}
为了增强程序的可定义性,我们同样增加了配置验证码相关选项的功能:
ImageCodeProperties.java:
package com.security.properties;
import lombok.Data;
/**
* @author ShotMoon
*/
@Data
public class ImageCodeProperties {
//若application.properties配置了相关参数,则默认数值被覆盖不生效
private int width = 65;
private int height = 25;
private int length = 4;
private int expireIn = 60;
}
application.properties:
spring.datasource.driver-class-name = com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/security?useUnicode=yes&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=qwe123
security.basic.enabled=false
spring.session.store-type=none
#shotmoon.security.LoginProperties.loginPage = /customized-loginPage.html
//配置验证码过期时间为120秒
shotmoon.security.ImageCodeProperties.expireIn = 120
好了,大功告成,经过以上代码,我们已经可以生成校验图片验证码了!(其他代码在上次的博文中,不重复列举了)
测试下
验证码正确:
验证码错误:(输出exception)