美文网首页Spring Security待整理
SpringSecurity图片验证码

SpringSecurity图片验证码

作者: 青衣敖王侯 | 来源:发表于2019-06-08 13:35 被阅读9次

       上一篇文章中我们对SpringSecurity认证流程源码进行了讲解。本章我们讲解一下SpringSecurity的图片验证码。
       实现图形验证码功能要有两个步骤:
       1.开发生成图形验证码接口
       2.在认证流程中加入图形验证码校验

    1.开发生成图形验证码接口

    1.1验证码信息封装类

    验证码要包含图片,code,还有超时时间3个要素,考虑到我们的browser模块和APP模块都会用到验证码信息,所以我们把这块代码放入到core模块中

    public class ImageCode {
    
        private BufferedImage image;
    
        private String code;
    
        private LocalDateTime 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;
        }
    
        public boolean isExpired() {
            return LocalDateTime.now().isAfter(expireTime);
        }
    
        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;
        }
    
    }
    
    

    1.2验证码生成器接口

    public interface ValidateCodeGenerator {
        ImageCode generate(ServletWebRequest request);
    }
    

    就一个验证码生成方法,这里为什么要定义接口呢,因为本章节中我们只讲解图形验证码,后面我们可能还有短信验证码,所以这里必须要以接口的形式提供。

    1.3验证码生成器实现类

    public class ImageCodeGenerator implements ValidateCodeGenerator {
        @Autowired
        private SecurityProperties securityProperties;
    
        @Override
        public ImageCode generate(ServletWebRequest request) {
    
            int width = ServletRequestUtils.getIntParameter(request.getRequest(), "width",
                    securityProperties.getCode().getImage().getLength());
            int height = ServletRequestUtils.getIntParameter(request.getRequest(), "height",
                    securityProperties.getCode().getImage().getHeight());
            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.getCode().getImage().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.getCode().getImage().getExpireIn());
    
        }
    
        /**
         * 生成随机背景条纹
         * 
         * @param fc
         * @param bc
         * @return
         */
        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);
        }
    
        public SecurityProperties getSecurityProperties() {
            return securityProperties;
        }
    
        public void setSecurityProperties(SecurityProperties securityProperties) {
            this.securityProperties = securityProperties;
        }
    
    }
    

    这里面包含了验证码的生成逻辑,另外我们看到还引入了SecurityProperties这个类,这个类主要是包含了对图形验证码的一些配置。我们可以在1.4中看看SecurityProperties的一些信息

    1.4SecurityProperties

    @ConfigurationProperties(prefix = "imooc.security")
    public class SecurityProperties {
        private BrowserProperties browser = new BrowserProperties();
    
        private ValidateCodeProperties code = new ValidateCodeProperties();
    
        public BrowserProperties getBrowser() {
            return browser;
        }
    
        public void setBrowser(BrowserProperties browser) {
            this.browser = browser;
        }
    
        public ValidateCodeProperties getCode() {
            return code;
        }
    
        public void setCode(ValidateCodeProperties code) {
            this.code = code;
        }
    
    }
    

    BrowserProperties是上一章节中讲解的配置了,这里我们还引入了ValidateCodeProperties是验证码的配置信息,但是刚才我们也提到了验证码有短信验证码,图形验证码之分,所以ValidateCodeProperties里面还会封装一层信息,见1.5

    1.5ValidateCodeProperties

    public class ValidateCodeProperties {
        
        /**
         * 图片验证码配置
         */
        private ImageCodeProperties image = new ImageCodeProperties();
        
    
        public ImageCodeProperties getImage() {
            return image; 
        }
    
        public void setImage(ImageCodeProperties image) {
            this.image = image;
        }
    }
    

    1.6ImageCodeProperties

    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;
        }
    
    }
    

      首先这个配置项包含了验证码图片的宽度、高度,验证码位数,验证码的超时时间,为什么有这么一个配置项,主要是为了让demo用户可以对这些选项做到灵活配置。那么它和1.1中的ImageCode又有什么区别呢?区别在于ImageCodeGenerator会根据ImageCodeProperties配置来生成ImageCode信息!!

    1.7ValidateBeanConfig

    @Configuration
    public class ValidateBeanConfig {
        @Autowired
        private SecurityProperties securityProperties;
    
        @Bean
        @ConditionalOnMissingBean(name = "imageCodeGenertor")
        public ValidateCodeGenerator imageCodeGenerator() {
            ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
            codeGenerator.setSecurityProperties(securityProperties);
            return codeGenerator;
        }
    }
    

      这个类主要是验证码生成的配置类,为什么要做这么一想配置,是因为有时候我们希望让用户在demo里面建立自己的验证码生成器。只要它把生成器的名字命名为imageCodeGenertor。这么做的好处是什么呢?当我们想拥有新的验证码生成器的时候可以不用改旧的代码,而是让用户自己重写即可,可以做到代码的无污染和改动,一个好的架构就是这样形成滴~~

    1.8ValidateCodeController

    我们的html页面在初始化的时候必须访问Controller来生成图形验证码,此时我们需要提供一个Controller来完成这段逻辑,Controller会用到ValidateCodeGenerator来生成验证码,代码如下:

    @RestController
    public class ValidateCodeController {
    
        public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
        private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
        @Autowired
        private ValidateCodeGenerator imageCodeGenerator;
    
        /**
         * 创建验证码,根据验证码类型不同,调用不同的 {@link ValidateCodeProcessor}接口实现
         * 
         * @param request
         * @param response
         * @param type
         * @throws Exception
         */
        @GetMapping("/code/image")
        public void createCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
            ImageCode imageCode = imageCodeGenerator.generate(new ServletWebRequest(request));
            sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
            ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
        }
    
    }
    

    为什么我们的验证码的输入参数是一个request请求呢?这是因为我们希望用户除了能够在他们的应用中配置验证码的参数外,我们还希望在请求的时候也能修改这些参数,我们的思路如下图所示:


    image.png

    好了验证码的生成接口我们就介绍完成了,那么我们如何用到它呢?

    2.在认证流程中加入图形验证码校验

    2.1使用过滤器来完成对图形验证码的校验--ValidateCodeFilter

    public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
        private AuthenticationFailureHandler authenticationFailureHandler;
        private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
        private Set<String> urls = new HashSet<>();
        private SecurityProperties securityProperties;
        private AntPathMatcher pathMatcher = new AntPathMatcher();
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
            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);
    
        }
    
        @Override
        public void afterPropertiesSet() throws ServletException {
            super.afterPropertiesSet();
            String[] configUrls = StringUtils
                    .splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getImage().getUrl(), ",");
            for (String configUrl : configUrls) {
                urls.add(configUrl);
            }
            urls.add("/authentication/form");
        }
    
        private void validate(ServletWebRequest request) throws ServletRequestBindingException {
    
            ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
    
            String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
    
            if (StringUtils.isBlank(codeInRequest)) {
                throw new ValidateCodeException("验证码的值不能为空");
            }
    
            if (codeInSession == null) {
                throw new ValidateCodeException("验证码不存在");
            }
    
            if (codeInSession.isExpired()) {
                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 SessionStrategy getSessionStrategy() {
            return sessionStrategy;
        }
    
        public void setSessionStrategy(SessionStrategy sessionStrategy) {
            this.sessionStrategy = sessionStrategy;
        }
    
        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;
        }
    
    }
    

    这个过滤器继承了OncePerRequestFilter,这个Filter里面有这些成员变量:

    • authenticationFailureHandler
      主要用于图形验证码校验失败后的处理逻辑
    • sessionStrategy
      验证码验证完毕后,我们必须将它从session中remove掉
    • Set<String> urls
      定义哪些访问的url必须走验证码逻辑。我们这里设置了对/user和/authentication/form的请求必须带验证码。其余的请求不需要提供验证码。
      Filter的验证逻辑有判断前台传递的验证码的值是否为空、Session中是否有验证码、验证码是否过期、验证码是否匹配。如果发生异常则会抛出ValidateCodeException

    2.2ValidateCodeException

    public class ValidateCodeException extends AuthenticationException {
    
        /**
         * 
         */
        private static final long serialVersionUID = -7285211528095468156L;
    
        public ValidateCodeException(String msg) {
            super(msg);
        }
    
    }
    

    这个异常继承了AuthenticationException,这样的话可以被失败处理器处理

    2.3BrowserSecurityConfig


    这个配置里面将验证码的Filter设置在了用户名密码FIlter的前面,另外也排除了/code/image路径的拦截

    2.4signIn.html

    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>登录</title>
    </head>
    <body>
        <h2>标准登录页面</h2>
        <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?width=200"></td>
                </tr>
                <tr>
                    <td colspan="2"><button type="submit">登录</button></td>
                </tr>
            </table>
        </form>
    </body>
    </html>
    

    2.5验证访问


    image.png

    输入完验证码后,成功实现了跳转

    相关文章

      网友评论

        本文标题:SpringSecurity图片验证码

        本文链接:https://www.haomeiwen.com/subject/uxzmxctx.html