zoukankan      html  css  js  c++  java
  • SpringSceurity(3)---图形验证码功能实现

    SpringSceurity(3)---图形验证码功能实现

    有关springSceurity之前有写过两篇文章:

    1、SpringSecurity(1)---认证+授权代码实现

    2、SpringSecurity(2)---记住我功能实现

    这篇我们来讲图形验证码功能实现。

    一、思路

    我整理下springSceurity整合图形验证码的大致思路:

    1、首先对于验证码本身而言,应该有三部分组成 1、存放验证码的背景图片 2、验证码 3、验证码的有效时间。
    
    2、对于springSceurity而言,验证码的执行校验顺序肯定是在UsernamePasswordAuthenticationFilter之前的,因为如果验证码都不对,那么
    根本都不需要验证账号密码。所以我们需要自定义一个验证码过滤器,并且配置在UsernamePasswordAuthenticationFilter之前执行。
    
    3、对于获取验证码的接口,肯定是不需要进行认证限制的。
    
    4、对于获取验证码的接口的时候,需要把该验证码信息+当前浏览器的SessonId绑定在一起存在Seesion中,为了后面校验的时候通过SessonId
    去取这个验证码信息。
    
    5、登陆请求接口,除了带上用户名和密码之外,还需要带上验证码信息。在进入验证码过滤器的时候,首先通过SessonId获取存在Sesson中的
    验证码信息,拿到验证码信息之后首先还要校验该验证码是否在有效期内。之后再和当前登陆接口带来的验证码进行对比,如果一致,那么当前
    验证码这一关就过了,就开始验证下一步账号和密码是否正确了。
    

    整个流程大致就是这样。下面现在是具体代码,然后进行测试。


    二、代码展示

    这里只展示一些核心代码,具体完整项目会放到github上。

    1、ImageCodeProperties

    这个是一个bean实体,是一个图形验证码的默认配置。

    @Data
    public class ImageCodeProperties {
        /**
         * 验证码宽度
         */
        private int width = 67;
        /**
         * 验证码高度
         */
        private int height = 23;
        /**
         * 验证码长度
         */
        private int length = 4;
        /**
         * 验证码过期时间
         */
        private int expireIn = 60;
        /**
         * 需要验证码的请求url字符串,用英文逗号隔开
         */
        private String url = "/login";
    
    }
    

    2、ImageCode

    这个是图片验证码的完整信息,也会将这个完整信息存放于Sesson中。

    图片验证码信息 由三部分组成 :

    1.图片信息(长、宽、背景色等等)。2.code就是真正的验证码,用来验证用。3.该验证码的有效时间。

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    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 boolean isExpired() {
            return LocalDateTime.now().isAfter(expireTime);
        }
    }
    

    3、ValidateCodeGeneratorService

    获取验证码的接口

    public interface ValidateCodeGeneratorService {
    
        /**
         * 生成图片验证码
         *
         * @param request 请求
         * @return ImageCode实例对象
         */
        ImageCode generate(ServletWebRequest request);
    }
    

    4、ImageCodeGeneratorServiceImpl

    获取图片验证码的接口的实现类

    @Data
    public class ImageCodeGeneratorServiceImpl implements ValidateCodeGeneratorService {
    
        private static final String IMAGE_WIDTH_NAME = "width";
        private static final String IMAGE_HEIGHT_NAME = "height";
        private static final Integer MAX_COLOR_VALUE = 255;
    
        private ImageCodeProperties imageCodeProperties;
    
        @Override
        public ImageCode generate(ServletWebRequest request) {
            //设置图片的宽度和高度
            int width = ServletRequestUtils.getIntParameter(request.getRequest(), IMAGE_WIDTH_NAME, imageCodeProperties.getWidth());
            int height = ServletRequestUtils.getIntParameter(request.getRequest(), IMAGE_HEIGHT_NAME, imageCodeProperties.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);
            }
    
            // 生成数字验证码
            StringBuilder sRand = new StringBuilder();
            for (int i = 0; i < imageCodeProperties.getLength(); 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(), imageCodeProperties.getExpireIn());
        }
    
        /**
         * 生成随机背景条纹
         *
         * @param fc 前景色
         * @param bc 背景色
         * @return RGB颜色
         */
        private Color getRandColor(int fc, int bc) {
            Random random = new Random();
            if (fc > MAX_COLOR_VALUE) {
                fc = MAX_COLOR_VALUE;
            }
            if (bc > MAX_COLOR_VALUE) {
                bc = MAX_COLOR_VALUE;
            }
            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);
        }
    }
    

    5、ValidateCodeController

    获取验证码的请求接口。

    @RestController
    public class ValidateCodeController {
    
        /**
         * 前缀
         */
       public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
        private static final String FORMAT_NAME = "JPEG";
       @Autowired
        private ValidateCodeGeneratorService imageCodeGenerator;
        private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    
        @GetMapping("/code/image")
        public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
            // 第一步:根据请求生成一个图形验证码对象
            ImageCode imageCode = imageCodeGenerator.generate(new ServletWebRequest(request));
            // 第二步:将图形验证码对象存到session中,第一个参数可以从传入的请求中获取session
            sessionStrategy.setAttribute(new ServletRequestAttributes(request), SESSION_KEY, imageCode);
            // 第三步:将生成的图片写到接口的响应中
            ImageIO.write(imageCode.getImage(), FORMAT_NAME, response.getOutputStream());
        }
    
    }
    

    到这里,我们可用请求获取图片验证码的信息了。接下来我们就要登陆请求部分的代码。

    6、ValidateCodeFilter

    自定义过滤器,这里面才是核心的代码,首先继承OncePerRequestFilter(直接继承Filter也是可用的),实现InitializingBean是为了初始化一些初始数据。

    这里走的逻辑就是把存在session中的图片验证码和当前请求的验证码进行比较,如果相同则放行,否则直接抛出异常

    @Data
    @Slf4j
    @Component
    public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
    
        private static final String SUBMIT_FORM_DATA_PATH = "/login";
    
        /**
         * 失败处理器
         */
        @Autowired
        private AuthenctiationFailHandler authenctiationFailHandler;
        /**
         * 验证码属性类
         */
        @Autowired
        private ImageCodeProperties imageCodeProperties;
        
        /**
         * 存放需要走验证码请求url
         */
        private Set<String> urls = new HashSet<>();
    
        /**
         * 处理session工具类
         */
        private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
        /**
         * 正则配置工具
         */
        private final AntPathMatcher antPathMatcher = new AntPathMatcher();
        /**
         * 在初始化bean的时候都会执行该方法
         */
        @Override
        public void afterPropertiesSet() throws ServletException {
            super.afterPropertiesSet();
            String[] configUrls = StringUtils.split(imageCodeProperties.getUrl(), ",");
            // 登录的链接是必须要进行验证码验证的
            urls.addAll(Arrays.asList(configUrls));
        }
    
        /**
         * 拦截请求进来的方法。
         */
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            boolean action = false;
            for (String url : urls) {
                // 如果实际访问的URL可以与用户在imageCodeProperties中url配置的相同,那么就进行验证码校验
                log.info("request.getRequestURI = {}",request.getRequestURI());
                if (antPathMatcher.match(url, request.getRequestURI())) {
                    action = true;
                }
            }
            //说明需要校验
            if (action) {
                try {
                    validate(new ServletWebRequest(request));
                } catch (ValidateCodeException e) {
                    authenctiationFailHandler.onAuthenticationFailure(request, response, e);
                    return;
                }
            }
    
            //进入下一个过滤器
            filterChain.doFilter(request, response);
        }
    
        /**
         * 验证码校验逻辑
         *
         */
        private void validate(ServletWebRequest request) throws ServletRequestBindingException {
            // 从session中获取图片验证码
            ImageCode imageCodeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
    
            // 从请求中获取用户填写的验证码
            String imageCodeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
            if (StringUtils.isBlank(imageCodeInRequest)) {
                throw new ValidateCodeException("验证码不能为空");
            }
            if (null == imageCodeInSession) {
                throw new ValidateCodeException("验证码不存在");
            }
            if (imageCodeInSession.isExpired()) {
                sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
                throw new ValidateCodeException("验证码已过期");
            }
    
            log.info("session中获取的验证码={},sessionId ={}",imageCodeInSession.getCode(),request.getSessionId());
            log.info("登陆操作传来的验证码={}",imageCodeInRequest);
            if (!StringUtils.equalsIgnoreCase(imageCodeInRequest, imageCodeInSession.getCode())) {
                throw new ValidateCodeException("验证码不匹配");
            }
            // 验证成功,删除session中的验证码
            sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
        }
    }
    
    

    7、WebSecurityConfig

    SpringSecurity的Java 配置类也需要做一点点改动。那就是需要设置ValidateCodeFilter要在UsernamePasswordAuthenticationFilter之前进行拦截过滤。

    到这里整个图形验证码的功能就开发完成了,具体代码放在github上下面进行测试。


    三、测试

    说明下我这里懒的写前端相关代码了,所以直接用posman用请求来获取验证码,获取验证码之后再进行登陆操作。

    1、获取验证码

    这里验证码code为:1848

    2、登陆成功

    输入的验证码也是1848,显示登陆成功。

    3、登陆失败

    因为配置的时候图片验证码有效期为60秒,所以在我们获取验证码后,过60秒再去登陆,就能发现,验证码已过期。

    整个验证码功能大致就是这样。


    Github地址 : spring-boot-security-study-03



    别人骂我胖,我会生气,因为我心里承认了我胖。别人说我矮,我就会觉得好笑,因为我心里知道我不可能矮。这就是我们为什么会对别人的攻击生气。
    攻我盾者,乃我内心之矛(19)
    
  • 相关阅读:
    基本MVVM 和 ICommand用法举例(转)
    WPF C# 命令的运行机制
    628. Maximum Product of Three Numbers
    605. Can Place Flowers
    581. Shortest Unsorted Continuous Subarray
    152. Maximum Product Subarray
    216. Combination Sum III
    448. Find All Numbers Disappeared in an Array
    268. Missing Number
    414. Third Maximum Number
  • 原文地址:https://www.cnblogs.com/qdhxhz/p/12791083.html
Copyright © 2011-2022 走看看