重构Spring Security实现图形验证码的功能
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重构Spring Security实现图形验证码的功能相关的知识,希望对你有一定的参考价值。
不单要写完功能,而是要把它变的可以配置,供其他的应用可以使用
优化要点
- 验证码的基本参数可配置(宽/高/验证码数字的长度/验证码的有效时间等)
- 验证码的拦截接口可配置(url地址)
- 验证码的生成逻辑可配置(更复杂的验证码生成逻辑)
1.验证码的基本参数可配置
在调用方 调用验证码的时候,没有做任何配置,则使用默认的验证码生成规则,如果有则覆盖掉默认配置。
默认配置
//生成二维码默认配置
public class ImageCodeProperties {
private int width = 67; //图片长度
private int height = 23; //图片高度
private int length = 4; //验证码长度
private int expireIn = 60; //失效时间
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public int getExpireIn() {
return expireIn;
}
public void setExpireIn(int expireIn) {
this.expireIn = expireIn;
}
}
//再此基础上,再封装一层。
public class ValidateCodeProperties {
private ImageCodeProperties image = new ImageCodeProperties();
public ImageCodeProperties getImage() {
return image;
}
public void setImage(ImageCodeProperties image) {
this.image = image;
}
}
//之后,再把ValidateCodeProperties放置在SecurityProperties中
再调用方则需要在配置文件中配置即可。
#code length
core.security.code.image.length = 6
core.security.code.image.width = 100
完成代码如下:
@RestController
public class ValidateCodeController {
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
//操作Session的类
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private SecurityProperties securityProperties;
@GetMapping("/code/image")
public void createCode(HttpServletRequest request,HttpServletResponse response) throws IOException {
//1.根据随机数生成数字
ImageCode imageCode = createImageCode(new ServletWebRequest(request));
//2.将随机数存到Session中
//把请求传递进ServletWebRequest,
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
//3.将生成的图片写到接口的响应中
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
//生成图片
private ImageCode createImageCode(ServletWebRequest request) {
//宽和高需要从request来取,如果没有传递,再从配置的值来取
//验证码宽和高
int width = ServletRequestUtils.getIntParameter(request.getRequest(), "width", securityProperties.getCode().getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(request.getRequest(), "height", securityProperties.getCode().getImage().getHeight());
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics graphics = image.getGraphics();
Random random = new Random();
graphics.setColor(getRandColor(200,250));
graphics.fillRect(0, 0, width, height);
graphics.setFont(new Font("Times New Roman", Font.ITALIC, 20));
graphics.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);
graphics.drawLine(x, y, x+xl, y+yl);
}
String sRand = "";
//验证码长度
for (int i = 0; i < securityProperties.getCode().getImage().getLength(); i++) {
String rand = String.valueOf(random.nextInt(10));
sRand +=rand;
graphics.setColor(new Color(20, random.nextInt(110), 20+random.nextInt(110),20+random.nextInt(110)));
graphics.drawString(rand, 13*i+6, 16);
}
graphics.dispose();
//过期时间
return new ImageCode(image, sRand, securityProperties.getCode().getImage().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);
}
}
<tr>
<td>图形验证码:</td>
<td>
<input type="text" name="imageCode">
<img src="/code/image?width=200">
</td>
</tr>
在配置文件里配置了验证码的长度和宽度,也在验证码的请求里增加了width参数,这个时候请求我们的页面;width=200会覆盖掉core.security.code.image.width = 100这个属性,
而core.security.code.image.length = 6会覆盖掉我们默认的4位长度验证码属性。
2.验证码的拦截接口可配置
在ImageCodeProperties增加url参数,用来配置哪些url请求需要验证码。
//生成二维码默认配置
public class ImageCodeProperties {
private int width = 67; //图片长度
private int height = 23; //图片高度
private int length = 4; //验证码长度
private int expireIn = 60; //失效时间
private String url; //多个请求需要验证;逗号隔开
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public int getExpireIn() {
return expireIn;
}
public void setExpireIn(int expireIn) {
this.expireIn = expireIn;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
//application.properties中配置需要拦截的url
core.security.code.image.url = /user,/user/*
//更改ValidateCodeFilter过滤中的doFilterInternal方法
//OncePerRequestFilter保证每次只被调用一次
//实现InitializingBean接口的目的是:其他的参数都组装完毕之后,初始化urls的值
@Component
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean{
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
//存储需要拦截的url
private Set<String> urls = new HashSet<>();
@Autowired
private SecurityProperties securityProperties;
private AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
//做urls处理
String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getImage().getUrl(),",");
for (String configUrl : configUrls) {
urls.add(configUrl);
}
//登录的请求一定要做验证码校验的
urls.add("/authentication/form");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//1.判断表单提交的请求(是否为登录请求)
//因为请求中有/user,/user/*这种方式的请求,就不能使用equals这种方式来判断,需要用到spring的工具类AntPathMatcher
boolean action = false;
for (String url : urls) {
if (pathMatcher.match(url, request.getRequestURI())) {
action = true;
}
}
if (action) {
try {
validate(new ServletWebRequest(request));
//为什么要用自定义异常,因为这是还是属于认证的过滤链中
} catch (ValidateCodeException e) {
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
filterChain.doFilter(request, response);
}
//校验验证码
private void validate(ServletWebRequest request) throws ServletRequestBindingException{
ImageCode codeInSession = (ImageCode)sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
//从请求里,拿到imageCode[来源于表单]
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("验证码不能为空");
}
if (codeInSession == null) {
throw new ValidateCodeException("验证码不存在");
}
if (codeInSession.isExpried()) {
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("验证码已过期");
}
if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
}
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
public Set<String> getUrls() {
return urls;
}
public void setUrls(Set<String> urls) {
this.urls = urls;
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
public SessionStrategy getSessionStrategy() {
return sessionStrategy;
}
public void setSessionStrategy(SessionStrategy sessionStrategy) {
this.sessionStrategy = sessionStrategy;
}
}
//最后需要配置BrowserSecurityConfig使其生效
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
private final static String loginPage = "/authentication/require";
@Autowired
private SecurityProperties securityProperties;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailHandler myAuthenticationFailHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(myAuthenticationFailHandler);
//传递参数
validateCodeFilter.setSecurityProperties(securityProperties);
validateCodeFilter.afterPropertiesSet();
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage(loginPage)
.loginProcessingUrl("/authentication/form")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailHandler)
.and()
.authorizeRequests()
.antMatchers(loginPage).permitAll()
.antMatchers(securityProperties.getBrowser().getLoginPage(),
"/code/image").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
}
从我们的配置上来说,目前有三个请求需要验证码
分别是:登录的,/user以及/user/*的
验证成功就是这些请求的时候,都会做验证码的非空/正确校验。
3.验证码的生成逻辑可配置
以上是关于重构Spring Security实现图形验证码的功能的主要内容,如果未能解决你的问题,请参考以下文章
Spring Security教程(13)---- 验证码功能的实现
Spring-Security 自定义Filter完成验证码校验