zoukankan      html  css  js  c++  java
  • 05. Spring Security 图形验证码

    05. Spring Security 图形验证码

    参考:

    https://blog.csdn.net/yuanlaijike/article/details/80253922

    https://mrbird.cc/Spring-Security-ValidateCode.html

    #依赖

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

    #验证码对象Image

    /**
     * 验证码
     */
    @Data
    public class ImageCode {
    
        //允许出现的序列值
        private char[] codeSequence = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
                'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
                'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
        //有效时间
        private LocalDateTime ttl;
        //验证码内容
        private String code;
        //验证码长度
        private Integer codeCount = 4;
        private BufferedImage bufferedImage;
    
        public ImageCode(Integer ttl, BufferedImage bufferedImage) {
            this.ttl = LocalDateTime.now().plusSeconds(ttl);
            this.bufferedImage = bufferedImage;
        }
    
        public ImageCode(Integer ttl, String code, BufferedImage bufferedImage) {
            this.ttl = LocalDateTime.now().plusSeconds(ttl);
            this.code = code;
            this.bufferedImage = bufferedImage;
        }
    
        /**
         * 判断验证码是否过期
         */
        public boolean isExpire() {
            return LocalDateTime.now().isAfter(ttl);
        }
    }
    

    #生成验证码的controller

    @Slf4j
    @RestController
    public class ValidateController {
        public final static String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE";
        //无法显示导包
        private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    
        /**
         * 生成验证码请求
         */
        @GetMapping("/code/image")
        public void createImageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
            ImageCode imageCode = createImageCode(100, 36, 60);
            //存储一个session key 为SESSION_KEY_IMAGE_CODE, 值为imageCode的到session域中
            sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode);
            log.warn("验证码"+imageCode.getCode());
            //将图片写入到前端
            ImageIO.write(imageCode.getBufferedImage(), "png", response.getOutputStream());
        }
    
        /**
         * @param width  验证码图片宽度
         * @param height 验证码图片长度
         * @param ttl    验证码有效时间 60s
         * @return
         */
        private ImageCode createImageCode(int width, int height, int ttl) {
            BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            Graphics g = image.getGraphics();
            Random random = new Random();
            //设置颜色
            g.setColor(getRandomColor(200, 259));
            //设置形状
            g.fillRect(0, 0, width, height);
            //设置字体颜色
            g.setColor(getRandomColor(160, 200));
            //设置字体
            g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
            //干扰线
            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 condeStr = new StringBuilder();
            ImageCode imageCode = new ImageCode(ttl, image);
            char[] codeSequence = imageCode.getCodeSequence();
            //验证码内容
            for (int i = 0; i < imageCode.getCodeCount(); i++) {
                //随机获取校验内容
                String rand = String.valueOf(codeSequence[random.nextInt(codeSequence.length)]);
                //为每个字符设置不同的颜色
                g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
                g.drawString(rand, 17 * i + 6, 25);
                condeStr.append(rand);
            }
            //将验证码的内容封装到对象中以便做对比
            imageCode.setCode(condeStr.toString());
            g.dispose();
            return imageCode;
        }
    
        /**
         * 获取随机颜色
         */
        private Color getRandomColor(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);
        }
    }
    

    #html

    <form class="login-page" th:action="@{/login}" method="post">
        <div class="form">
            <h3>账户登录</h3>
            <input type="text" placeholder="用户名" name="username" required="required"/>
            <input type="password" placeholder="密码" name="password" required="required"/>
            <!--        自动登入设置的name必须是remember-me-->
            <span><input type="checkbox" name="remember-me"/>自动登录<span/>
            <!--        图形验证码-->
            <span style="display: inline">
                <input type="text" name="imageCode" placeholder="验证码" style=" 50%;"/>
                <img src="/code/image"/>
            </span>
            <button type="submit">登录</button>
        </div>
    </form>
    

    img 的 src属性对应@GetMapping("/code/image")

    #修改配置类

    	/**
         * 校验图形验证码
         */
        @Bean
        public OncePerRequestFilter validatedCodeFilter() {
            return new ValidateCodeFilter();
        }
    
    	@Override
        protected void configure(HttpSecurity http) throws Exception {
           		 http
                    .authorizeRequests()
                    //放行图形验证码
                    .antMatchers("/login", "/code/image").permitAll()
                    .anyRequest()
                    .authenticated()
                    .and()
                    .formLogin()
                    .loginPage("/login")
                    .successForwardUrl("/")
                    .failureForwardUrl("/fail2")
                    .and()
                    .logout().permitAll()
                    .logoutUrl("/logout")
                    .logoutSuccessUrl("/login")
                    .clearAuthentication(true)
                    .invalidateHttpSession(true)
                    .deleteCookies("JSESSIONID")
                    .and()
                    .rememberMe()
                    .tokenValiditySeconds(60)
                    .tokenRepository(persistentTokenRepository())
                    .userDetailsService(userDetailsService)
                    .and()
                    .csrf()
                    .disable();
        }
    

    启动后, 效果如下

    #添加验证码校验

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

    /**
     * 自定义验证码错误
     */
    public class ValidateCodeException extends AuthenticationException {
        private static final long serialVersionUID = 1L;
        public ValidateCodeException(String msg) {
            super(msg);
        }
    }
    

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

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

    • 方法一: 重定向
    /**
     * 自定义过滤器,如果没有权限验证,每发送一次请求调用一次
     */
    public class ValidateCodeFilter extends OncePerRequestFilter {
        private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            //只对登入请求校验
            if (isProtectedUrl(request)) {
                //将httpServlet包装为ServletWebRequest
                try {
                    validateCode(new ServletWebRequest(request));
                } catch (ValidateCodeException e) {
                    //如果校验失败,请求转发
                    request.getRequestDispatcher("/fail").forward(request,response);
                }
            }
            //放行请求
            filterChain.doFilter(request, response);
        }
    
        /**
         *  校验验证码
         * @param servletWebRequest 通过ServletWebRequest可以拿到webRequest(所有的web request请求)是一个适配器
         * @throws ServletRequestBindingException
         */
        private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {
            //通过session key获取到对应的imageCode
            ImageCode codeInSession = (ImageCode) sessionStrategy
                    .getAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE);
            //从request获取name为imageCode的值
            String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode");
            if (StringUtils.isBlank(codeInRequest)){
                throw new ValidateCodeException("验证码不能为空");
            }else if (ObjectUtils.isEmpty(codeInSession)){
                throw new ValidateCodeException("验证码不存在");
            }else if (codeInSession.isExpire()){
                throw new ValidateCodeException("验证码过期");
            }else if (!StringUtils.equalsIgnoreCase(codeInSession.getCode(),codeInRequest)){
                throw new ValidateCodeException("验证码不正确");
            }
            //如果校验通过, 删除session中的imageCode
            sessionStrategy.removeAttribute(servletWebRequest,ValidateController.SESSION_KEY_IMAGE_CODE);
        }
    
        /**
         * 过滤请求
         */
        private boolean isProtectedUrl(HttpServletRequest request) {
            return StringUtils.equalsIgnoreCase("/login", request.getRequestURL())
                    && StringUtils.equalsIgnoreCase("post", request.getMethod());
        }
    }
    
    
    • 方法二: failureHandler

    Handler

    public class FailureHandler implements AuthenticationFailureHandler {
        @Autowired
        private ObjectMapper mapper;
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(mapper.writeValueAsString(exception.getMessage()));
        }
    }
    

    修改ValidateCodeFilter

        @Autowired
        AuthenticationFailureHandler failureHandler;
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            //只对登入请求校验
            if (isProtectedUrl(request)) {
                //将httpServlet包装为ServletWebRequest
                try {
                    validateCode(new ServletWebRequest(request));
                } catch (ValidateCodeException e) {
                    failureHandler.onAuthenticationFailure(request,response,e);
                    //这里必须要return,否则还是会执行doFilter,验证码不正确也会校验成功
                    return;
                }
            }
            //放行请求
            filterChain.doFilter(request, response);
        }
    

    配置类

        @Bean
        public AuthenticationFailureHandler failureHandler(){
            return new FailureHandler();
        }
    
    	/**
         * 校验图形验证码
         */
        @Bean
        public OncePerRequestFilter validatedCodeFilter() {
            return new ValidateCodeFilter();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //添加过滤器
            http.addFilterBefore(validatedCodeFilter(), UsernamePasswordAuthenticationFilter.class)
                    .authorizeRequests()
                    //放行图形验证码
                    .antMatchers("/login", "/code/image").permitAll()
                    .anyRequest()
                    .authenticated()
                    .and()
                    .formLogin()
                    .loginPage("/login")
                    .successForwardUrl("/")
                    .failureHandler(failureHandler())
                    .and()
                    .logout().permitAll()
                    .logoutUrl("/logout")
                    .logoutSuccessUrl("/login")
                    .clearAuthentication(true)
                    .invalidateHttpSession(true)
                    .deleteCookies("JSESSIONID")
                    .and()
                    .rememberMe()
                    .tokenValiditySeconds(60)
                    .tokenRepository(persistentTokenRepository())
                    .userDetailsService(userDetailsService)
                    .and()
                    .csrf()
                    .disable();
        }
    
  • 相关阅读:
    C#动态执行代码
    C#的动态编译执行
    Win7/Vista激活后添加grub引导Linux最简单方法!无需命令行!
    乔布斯:关于 Flash 的思考
    I want to live in a honest country
    twitter bbs
    my follow rule on twitter
    blog will be repleace by twitter?
    blog Prediction
    也说说对blog是否需要静态页面的一点看法
  • 原文地址:https://www.cnblogs.com/kikochz/p/12895842.html
Copyright © 2011-2022 走看看