zoukankan      html  css  js  c++  java
  • Spring Security -- 添加图形验证码(转载)

    添加验证码大致可以分为三个步骤:

    • 根据随机数生成验证码图片;
    • 将验证码图片显示到登录页面;
    • 认证流程中加入验证码校验。

    Spring Security的认证校验是由UsernamePasswordAuthenticationFilter过滤器完成的,所以我们的验证码校验逻辑应该在这个过滤器之前。下面一起学习下如何在上一节Spring Security自定义用户认证的基础上加入验证码校验功能。

    一、生成校验码

    1、导入依赖

    验证码功能需要用到spring-social-config依赖:

    <dependency>
        <groupId>org.springframework.social</groupId>
        <artifactId>spring-social-config</artifactId>
        <version>1.1.6.RELEASE</version>
    </dependency>

    2、ImageCode实体类

    在com.goldwind.entity包下创建类ImageCode:

    package com.goldwind.entity;
    
    import lombok.Data;
    
    import java.awt.image.BufferedImage;
    import java.time.LocalDateTime;
    
    /**
     * @Author: zy
     * @Description: 验证码
     * @Date: 2020-2-9
     */
    @Data
    public class ImageCode {
    
        /**
         * 验证码图片
         */
        private BufferedImage image;
    
        /**
         * code验证码
         */
        private String code;
    
        /**
         * 过期时间 单位秒
         */
        private LocalDateTime expireTime;
    
        /**
         * 判断验证码是否过期
         * @return
         */
        public boolean isExpire() {
            return LocalDateTime.now().isAfter(expireTime);
        }
    
        /**
         * 构造函数
         * @param image
         * @param code
         * @param expireIn
         */
        public ImageCode(BufferedImage image, String code, int expireIn) {
            this.image = image;
            this.code = code;
            this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
        }
    
    }

    ImageCode对象包含了三个属性:image图片,code验证码和expireTime过期时间。isExpire方法用于判断验证码是否已过期。

    3、ValidateCodeController

    接着在包com.goldwind.controller下定义一个ValidateCodeController,用于处理生成验证码请求:

    package com.goldwind.controller;
    
    import com.goldwind.entity.ImageCode;
    import org.springframework.social.connect.web.HttpSessionSessionStrategy;
    import org.springframework.social.connect.web.SessionStrategy;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.context.request.ServletWebRequest;
    
    import javax.imageio.ImageIO;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.awt.*;
    import java.awt.image.BufferedImage;
    import java.io.IOException;
    import java.util.Random;
    
    /**
     * @Author: zy
     * @Description: 处理生成验证码的请求
     * @Date: 2020-2-9
     */
    @RestController
    @RequestMapping("/code")
    public class ValidateCodeController {
    
        public final static String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE";
    
        //使用sessionStrategy将生成的验证码对象存储到Session中,并通过IO流将生成的图片输出到登录页面上。
        private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    
        @RequestMapping("/image")
        public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
            //生成验证码对象
            ImageCode imageCode = createImageCode();
            //生成的验证码对象存储到Session中
            sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY_IMAGE_CODE,imageCode);
            //通过IO流将生成的图片输出到登录页面上
            ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());
        }
    
        /**
         * 用于生成验证码对象
         * @return
         */
        private ImageCode createImageCode() {
    
            int width = 100;    // 验证码图片宽度
            int height = 36;    // 验证码图片长度
            int length = 4;     // 验证码位数
            int expireIn = 60;  // 验证码有效时间 60s
    
            //创建一个带缓冲区图像对象
            BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            //获得在图像上绘图的Graphics对象
            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);
            }
    
            //生成随机数 并绘制
            StringBuilder sRand = new StringBuilder();
            for (int i = 0; i < length; i++) {
                String rand = String.valueOf(random.nextInt(10));
                sRand.append(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.toString(), expireIn);
        }
    
        /**
         * 获取随机演示
         * @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);
        }
    
    }

    createImageCode方法用于生成验证码对象,org.springframework.social.connect.web.HttpSessionSessionStrategy对象封装了一些处理Session的方法,包含了setAttribute、getAttribute和removeAttribute方法,具体可以查看该类的源码。使用sessionStrategy将生成的验证码对象存储到Session中,并通过IO流将生成的图片输出到登录页面上。

    生成验证码的方法写好后,接下来开始改造登录页面。

    二、登录页

    在登陆页面login.ftl追加如下代码:

    <input type="text" name="imageCode" placeholder="验证码" style=" 50%;"/>
    <img src="/code/image"/>
     <br>

    <img>标签的src属性对应ValidateController的createImageCode方法:

    要使生成验证码的请求不被拦截,需要在BrowserSecurityConfig的configure方法中配置免拦截:

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()    // 授权配置
                    .antMatchers("/code/image")
                    .permitAll()       // 无需认证的请求路径
                    .anyRequest()       // 任何请求
                    .authenticated()    //都需要身份认证
                    .and()
                    .formLogin()         // 或者httpBasic()
                    .loginPage("/login")  // 指定登录页的路径
                    .loginProcessingUrl("/login")  // 指定自定义form表单请求的路径
                    .successHandler(authenticationSucessHandler)    // 处理登录成功
                    .failureHandler(authenticationFailureHandler) // 处理登录失败
                    // 必须允许所有用户访问我们的登录页(例如未验证的用户,否则验证流程就会进入死循环)
                    // 这个formLogin().permitAll()方法允许所有用户基于表单登录访问/login这个page。
                    .permitAll()
                    .and()
                    .logout()
                    .permitAll()
                    .and()
                    //默认都会产生一个hiden标签 里面有安全相关的验证 防止请求伪造 这边我们暂时不需要 可禁用掉
                    .csrf().disable();
        }

    重启项目,访问http://localhost:8080/login,效果如下:

    三、认证流程添加验证码校验

    在校验验证码的过程中,可能会抛出各种验证码类型的异常,比如“验证码错误”、“验证码已过期”等,所以我们在com.goldwind.exception包下定义一个验证码类型的异常类:

    package com.goldwind.exception;
    
    import javax.naming.AuthenticationException;
    
    /**
     * @Author: zy
     * @Description: 验证码校验异常
     * @Date: 2020-2-9
     */
    public class ValidateCodeException extends AuthenticationException {
        private static final long serialVersionUID = 5022575393500654458L;
    
        /**
         * 构造函数
         * @param message
         */
        public ValidateCodeException(String message) {
            super(message);
        }
    }

    注意,这里继承的是AuthenticationException而不是Exception。

    我们都知道,Spring Security实际上是由许多过滤器组成的过滤器链,处理用户登录逻辑的过滤器为UsernamePasswordAuthenticationFilter,而验证码校验过程应该是在这个过滤器之前的,即只有验证码校验通过后才去校验用户名和密码。由于Spring Security并没有直接提供验证码校验相关的过滤器接口,所以我们需要在com.goldwind.filter包下自己定义一个验证码校验的过滤器ValidateCodeFilter:

    package com.goldwind.filter;
    
    
    import com.goldwind.controller.ValidateCodeController;
    import com.goldwind.entity.ImageCode;
    import com.goldwind.exception.ValidateCodeException;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.social.connect.web.HttpSessionSessionStrategy;
    import org.springframework.social.connect.web.SessionStrategy;
    import org.springframework.stereotype.Service;
    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: zy
     * @Description: 图片验证码过滤器,用于对图片验证码进行校验
     * @Date: 2020-2-9
     */
    @Service
    public class ValidateCodeFilter extends OncePerRequestFilter {
    
        @Autowired
        private AuthenticationFailureHandler authenticationFailureHandler;
    
        //使用sessionStrategy将生成的验证码对象存储到Session中
        private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    
        /**
         * 如果请求是/login、对图片验证码进行校验
         * @param httpServletRequest
         * @param httpServletResponse
         * @param filterChain
         * @throws ServletException
         * @throws IOException
         */
        @Override
        protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
            //判断请求页面是否为/login、该路径对应登录form表单的action路径,请求的方法是否为POST,是的话进行验证码校验逻辑,否则直接执行filterChain.doFilter让代码往下走
            if (StringUtils.equalsIgnoreCase("/login", httpServletRequest.getRequestURI())
                    && StringUtils.equalsIgnoreCase(httpServletRequest.getMethod(), "post")) {
                try {
                    //校验验证码 校验通过、继续向下执行   验证失败、抛出异常
                    validateCode(new ServletWebRequest(httpServletRequest));
                } catch (ValidateCodeException e) {
                    //校验失败 返回错误状态码及信息
                    authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
                    return;
                }
            }
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        }
    
        /**
         * 对图片验证码进行校验
         * @param servletWebRequest:请求参数 包含表单提交的图片验证码信息
         * @throws ServletRequestBindingException
         * @throws ValidateCodeException: 验证码校验失败 抛出异常
         */
        private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException, ValidateCodeException {
            //从Session获取保存在服务器端的验证码
            ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(servletWebRequest, ValidateCodeController.SESSION_KEY_IMAGE_CODE);
    
            //获取表单提交的图片验证码
            String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode");
    
            //验证码空校验
            if (StringUtils.isBlank(codeInRequest)) {
                throw new ValidateCodeException("验证码不能为空!");
            }
    
            //验证码校验
            if (codeInSession == null) {
                throw new ValidateCodeException("验证码不存在,请重新发送!");
            }
    
            //验证码过期校验
            if (codeInSession.isExpire()) {
                sessionStrategy.removeAttribute(servletWebRequest, ValidateCodeController.SESSION_KEY_IMAGE_CODE);
                throw new ValidateCodeException("验证码已过期!");
            }
    
            //判断是否相等
            if (!StringUtils.equalsIgnoreCase(codeInSession.getCode(), codeInRequest)) {
                throw new ValidateCodeException("验证码不正确!");
            }
    
            //从Session移除该字段信息
            sessionStrategy.removeAttribute(servletWebRequest, ValidateCodeController.SESSION_KEY_IMAGE_CODE);
        }
    }

    ValidateCodeFilter继承了org.springframework.web.filter.OncePerRequestFilter,该过滤器只会执行一次。

    在doFilterInternal方法中我们判断了请求URL是否为/login,该路径对应登录form表单的action路径,请求的方法是否为post,是的话进行验证码校验逻辑,否则直接执行filterChain.doFilter让代码往下走。当在验证码校验的过程中捕获到异常时,调用Spring Security的校验失败处理器CustomAuthenticationFailureHandler进行处理。

    在validateCode方法中我们分别从Session中获取了ImageCode对象和请求参数imageCode(对应登录页面的验证码<input>框name属性),然后进行了各种判断并抛出相应的异常。当验证码过期或者验证码校验通过时,我们便可以删除Session中的ImageCode。

    验证码校验过滤器定义好了,怎么才能将其添加到UsernamePasswordAuthenticationFilter前面呢?很简单,只需要在BrowserSecurityConfig的configure方法中添加些许配置即可:

      @Autowired
        private ValidateCodeFilter validateCodeFilter;
    
        /**
         * 配置拦截请求资源
         * @param http:HTTP请求安全处理
         * @throws Exception
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // 添加验证码校验过滤器
                    .authorizeRequests()    // 授权配置
                    .antMatchers("/code/image")
                    .permitAll()       // 无需认证的请求路径
                    .anyRequest()       // 任何请求
                    .authenticated()    //都需要身份认证
                    .and()
                    .formLogin()         // 或者httpBasic()
                    .loginPage("/login")  // 指定登录页的路径
                    .loginProcessingUrl("/login")  // 指定自定义form表单请求的路径
                    .successHandler(authenticationSucessHandler)    // 处理登录成功
                    .failureHandler(authenticationFailureHandler) // 处理登录失败
                    // 必须允许所有用户访问我们的登录页(例如未验证的用户,否则验证流程就会进入死循环)
                    // 这个formLogin().permitAll()方法允许所有用户基于表单登录访问/login这个page。
                    .permitAll()
                    .and()
                    .logout()
                    .permitAll()
                    .and()
                    //默认都会产生一个hiden标签 里面有安全相关的验证 防止请求伪造 这边我们暂时不需要 可禁用掉
                    .csrf().disable();
        }

    上面代码中,我们注入了ValidateCodeFilter,然后通过addFilterBefore方法将ValidateCodeFilter验证码校验过滤器添加到了UsernamePasswordAuthenticationFilter前面。

    大功告成,重启项目,访问http://localhost:8080/login,当不输入验证码时点击登录,页面显示如下:

    当输入错误的验证码时点击登录,页面显示如下:

    当页面加载60秒后再输入验证码点击登录,页面显示如下:

    当验证码通过,并且用户名密码正确时,页面显示如下:

    参考博客:

    [1] Spring Security添加图形验证码(转载)

  • 相关阅读:
    leetcode教程系列——Binary Tree
    《Ranked List Loss for Deep Metric Learning》CVPR 2019
    《Domain Agnostic Learning with Disentangled Representations》ICML 2019
    Pytorch从0开始实现YOLO V3指南 part5——设计输入和输出的流程
    Pytorch从0开始实现YOLO V3指南 part4——置信度阈值和非极大值抑制
    Pytorch从0开始实现YOLO V3指南 part3——实现网络前向传播
    Pytorch从0开始实现YOLO V3指南 part2——搭建网络结构层
    Pytorch从0开始实现YOLO V3指南 part1——理解YOLO的工作
    让我佩服的人生 文章
    win8.1配置cordova+ionic等一系列东西
  • 原文地址:https://www.cnblogs.com/zyly/p/12287310.html
Copyright © 2011-2022 走看看