重构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.验证码的生成逻辑可配置

原文地址:http://blog.51cto.com/mazongfei/2340369

时间: 2024-08-29 17:54:41

重构Spring Security实现图形验证码的功能的相关文章

Spring Security实现图形验证码的功能

一.生成图片验证码的步骤1.根据随机数生成数字2.将随机数存到Session中3.将生成的图片写到接口的响应中 public class ImageCode { private BufferedImage image;//展示的图片 private String code;//生成的随机数,Session private LocalDateTime expireTime;//过期时间 public BufferedImage getImage() { return image; } public

Spring Security 实现手机验证码登录

思路:参考用户名密码登录过滤器链,重写认证和授权 示例如下(该篇示例以精简为主,演示主要实现功能,全面完整版会在以后的博文中发出): 由于涉及内容较多,建议先复制到本地工程中,然后在细细研究. 1.   新建Maven项目  sms-code-validate 2.   pom.xml <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchem

Spring Security(十):3. What’s New in Spring Security 4.2 (新功能)

Among other things, Spring Security 4.2 brings early support for Spring Framework 5. You can find the change logs for 4.2.0.M1, 4.2.0.RC1, 4.2.0.RELEASE which closes over 80 issues. The overwhelming majority of these features were contributed by the

spring security实现限制登录次数功能

本节是在基于注解方式进行的,后面的例子都会基于注解形式,不再实现XML配置形式,毕竟注解才是趋势嘛! 关键在于实现自定义的UserDetailsService和AuthenticationProvider 项目结构如下: 查看spring security的源代码可以发现默认security已经定义的user中的一些变量,鉴于此创建users表如下: CREATE TABLE users ( username VARCHAR(45) NOT NULL, password VARCHAR(45)

Spring Security简单实现自定义退出功能

1.前端页面写法 <a href="javascript:;" onclick="logoutBackground()">退出</a> 2.js /** * 退出后台 */ function logoutBackground() { $.get("/admin/logout", function (msg) { if (msg === "true") { window.location.href = &

Spring Security技术栈开发企业级认证与授权

Spring Security技术栈开发企业级认证与授权网盘地址:https://pan.baidu.com/s/1mj8u6JQ 密码: 92rp备用地址(腾讯微云):https://share.weiyun.com/8b2ffc1839069b4399950333860754a4 密码:a539tn 第1章 课程导学介绍课程内容.课程特点,使用的主要技术栈,以及学习课程所需的前置知识 第2章 开始开发安装开发工具,介绍项目代码结构并搭建,基本的依赖和参数设置,开发hello world 第3

Spring Security(十七):5.8 Method Security

From version 2.0 onwards Spring Security has improved support substantially for adding security to your service layer methods. It provides support for JSR-250 annotation security as well as the framework's original @Secured annotation. From 3.0 you c

Spring Security(二十二):6.4 Method Security

From version 2.0 onwards Spring Security has improved support substantially for adding security to your service layer methods. It provides support for JSR-250 annotation security as well as the framework's original @Secured annotation. From 3.0 you c

安全框架Shiro和Spring Security比较

Shiro 首先Shiro较之 Spring Security,Shiro在保持强大功能的同时,还在简单性和灵活性方面拥有巨大优势. Shiro是一个强大而灵活的开源安全框架,能够非常清晰的处理认证.授权.管理会话以及密码加密.如下是它所具有的特点: 易于理解的 Java Security API: 简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory 等): 对角色的简单的签权(访问控制),支持细粒度的签权: 支持一级缓存,以提升应用程序