重构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---验证码详解

Spring Security教程(13)---- 验证码功能的实现

Spring-Security 自定义Filter完成验证码校验

springboot集成spring security实现restful风格的登录认证 附代码

图形验证码的实现

spring security