zoukankan      html  css  js  c++  java
  • spring security 实现登录验证码及记住我

    spring security 验证码登录:

      在现在主流的网站登录页上,我们经常可以看到登陆的时候是通过账号密码登录,那么时常会看到需要我们输入一个图片验证码里面的值。或者通过手机验证码进行短信登陆,进行获取验证码进行登录。而这两种登陆方式都用到了验证码,前者是图片验证码,后者是短信验证码。在spring security 中使用验证码来验证登录,其核心还是拦截器链,当请求获取验证码的时候,将该验证码放入request中,当用户收到后进行输入。然后取出request里面的值进行比对。或者将这个东西存在别的比较安全的地方都是可以的。然后添加自己的校验拦截器。进行响应的逻辑处理,即可达到验证码登录的效果。

      为了让读者能更好的理解这一套处理逻辑,下图展示了这个功能的类图:

      在这里我们需要导入以下依赖,commons没用到的可以删掉:

    <dependency>
                <groupId>org.springframework.social</groupId>
                <artifactId>spring-social-config</artifactId>
                <version>1.1.4.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.social</groupId>
                <artifactId>spring-social-core</artifactId>
                <version>1.1.4.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.social</groupId>
                <artifactId>spring-social-security</artifactId>
                <version>1.1.4.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.social</groupId>
                <artifactId>spring-social-web</artifactId>
                <version>1.1.4.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>commons-lang</groupId>
                <artifactId>commons-lang</artifactId>
                <version>2.6</version>
            </dependency>
            <dependency>
                <groupId>commons-beanutils</groupId>
                <artifactId>commons-beanutils</artifactId>
                <version>1.8.3</version>
            </dependency>
            <dependency>
                <groupId>commons-collections</groupId>
                <artifactId>commons-collections</artifactId>
                <version>3.2.1</version>
            </dependency>

    1.验证码生成器 ValidateCodeGenerator,主要逻辑是用于生成验证码:

    public interface ValidateCodeGenerator {
        /**
         * 生成验证码
         */
        ValidateCode generate(ServletWebRequest request);
    }

      这里需要定义我们验证码的值及过期时间:

    public class ValidateCode {
    
        private String code;
    
        private LocalDateTime expireTime;
    
        public ValidateCode(String code, int expireIn){
            this.code = code;
            this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
        }
    
        public ValidateCode(String code, LocalDateTime expireTime){
            this.code = code;
            this.expireTime = expireTime;
        }
       // 判断验证码是否超时
        public boolean isExpried() {
            return LocalDateTime.now().isAfter(expireTime);
        }
       // 省略get/set
    }

      上述两种类型的验证码中,图片验证码需要向浏览器发送图片流,所以这里还需要定义一个图片验证码类:

    public class ImageCode extends ValidateCode {
    
        private BufferedImage image;
    
        public ImageCode(BufferedImage image, String code, int expireIn) {
            super(code, expireIn);
            this.image = image;
        }
    
        public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
            super(code, expireTime);
            this.image = image;
        }
        // 省略get/set
    }

      图片验证码生成,样式可以自己调整,不行的话就百度:

    public class ImageCodeGenerator implements ValidateCodeGenerator {
    
        @Override
        public ImageCode generate(ServletWebRequest request) {
            int width = ServletRequestUtils.getIntParameter(request.getRequest(), "width",60);
            int height = ServletRequestUtils.getIntParameter(request.getRequest(), "height",20);
            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 < 4; 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, 60);
        }
    
        /**
         * 生成随机背景条纹
         */
        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);
        }
    }

      短信验证码,但是测试环境我们就随机生成一个六位数的密码:

    @Component("smsValidateCodeGenerator")
    public class SmsCodeGenerator implements ValidateCodeGenerator {
    
    
        @Override
        public ValidateCode generate(ServletWebRequest request) {
            String code = RandomStringUtils.randomNumeric(6);
            return new ValidateCode(code, 60);
        }
    }

    2.验证码处理器 ValidateCodeProcessor ,这里主要是创建验证码及校验验证码功能,还有一个是需要往 request 里面存放我们的验证码,所以这里定义一个前缀。

    public interface ValidateCodeProcessor {
        /**
         * 验证码放入session时的前缀
         */
        String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_";
    
        /**
         * 创建校验码*/
        void create(ServletWebRequest request) throws Exception;
    
        /**
         * 校验验证码*/
        void validate(ServletWebRequest servletWebRequest);
    }

      抽象的验证码处理器:

    public abstract class AbstractValidateCodeProcessor<C extends ValidateCode> implements ValidateCodeProcessor {
    
        /**
         * 操作session的工具类
         */
        private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
        /**
         * 收集系统中所有的 {@link ValidateCodeGenerator} 接口的实现。
         */
        @Autowired
        private Map<String, ValidateCodeGenerator> validateCodeGenerators;
    
        /*
         * (non-Javadoc)
         */
        @Override
        public void create(ServletWebRequest request) throws Exception {
            C validateCode = generate(request);
            save(request, validateCode);
            send(request, validateCode);
        }
    
        /**
         * 生成校验码
         *
         */
        @SuppressWarnings("unchecked")
        private C generate(ServletWebRequest request) {
            String type = getValidateCodeType(request).toString().toLowerCase();
            String generatorName = type + ValidateCodeGenerator.class.getSimpleName();
            ValidateCodeGenerator validateCodeGenerator = validateCodeGenerators.get(generatorName);
            if (validateCodeGenerator == null) {
                throw new ValidateCodeException("验证码生成器" + generatorName + "不存在");
            }
            return (C) validateCodeGenerator.generate(request);
        }
    
        /**
         * 保存校验码
         *
         * @param request
         * @param validateCode
         */
        private void save(ServletWebRequest request, C validateCode) {
            sessionStrategy.setAttribute(request, getSessionKey(request), validateCode);
        }
    
        /**
         * 构建验证码放入session时的key
         *
         * @param request
         * @return
         */
        private String getSessionKey(ServletWebRequest request) {
            return SESSION_KEY_PREFIX + getValidateCodeType(request).toString().toUpperCase();
        }
    
        /**
         * 发送校验码,由子类实现
         *
         * @param request
         * @param validateCode
         * @throws Exception
         */
        protected abstract void send(ServletWebRequest request, C validateCode) throws Exception;
    
        /**
         * 根据请求的url获取校验码的类型
         *
         * @param request
         * @return
         */
        private ValidateCodeType getValidateCodeType(ServletWebRequest request) {
            String type = StringUtils.substringBefore(getClass().getSimpleName(), "CodeProcessor");
            return ValidateCodeType.valueOf(type.toUpperCase());
        }
    
        @SuppressWarnings("unchecked")
        @Override
        public void validate(ServletWebRequest request) {
    
            ValidateCodeType processorType = getValidateCodeType(request);
            String sessionKey = getSessionKey(request);
    
            C codeInSession = (C) sessionStrategy.getAttribute(request, sessionKey);
    
            String codeInRequest;
            try {
                codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(),
                        processorType.getParamNameOnValidate());
            } catch (ServletRequestBindingException e) {
                throw new ValidateCodeException("获取验证码的值失败");
            }
    
            if (StringUtils.isBlank(codeInRequest)) {
                throw new ValidateCodeException(processorType + "验证码的值不能为空");
            }
    
            if (codeInSession == null) {
                throw new ValidateCodeException(processorType + "验证码不存在");
            }
    
            if (codeInSession.isExpried()) {
                sessionStrategy.removeAttribute(request, sessionKey);
                throw new ValidateCodeException(processorType + "验证码已过期");
            }
    
            if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
                throw new ValidateCodeException(processorType + "验证码不匹配");
            }
    
            sessionStrategy.removeAttribute(request, sessionKey);
        }
    }

      子类实现,图片验证码处理器:

    @Component("imageValidateCodeProcessor")
    public class ImageCodeProcessor extends AbstractValidateCodeProcessor<ImageCode> {
    
        /**
         * 发送图形验证码,将其写到响应中
         */
        @Override
        protected void send(ServletWebRequest request, ImageCode imageCode) throws Exception {
            ImageIO.write(imageCode.getImage(), "JPEG", request.getResponse().getOutputStream());
        }
    
    }

      短信验证码处理器,这里主要需要调用发送短信的api进行短信发送,这里就直接通过打印一行日志来表示:

    @Component("smsValidateCodeProcessor")
    public class SmsCodeProcessor extends AbstractValidateCodeProcessor<ValidateCode> {
        /**
         * 短信验证码发送器
         */
        @Autowired
        private SmsCodeSender smsCodeSender;
    
        @Override
        protected void send(ServletWebRequest request, ValidateCode validateCode) throws Exception {
            String paramName = "mobile";
            String mobile = ServletRequestUtils.getRequiredStringParameter(request.getRequest(), paramName);
            smsCodeSender.send(mobile, validateCode.getCode());
        }
    }
    
    public interface SmsCodeSender {
       void send(String mobile, String code);
    }
    public class DefaultSmsCodeSender implements SmsCodeSender {
    
       @Override
       public void send(String mobile, String code) {
          System.out.println("向手机"+mobile+"发送短信验证码"+code);
       }
    }

      那么到现在为止,验证码的生成及处理已经完成。

    3.向spring容器注入相关类,当然也可以直接标在类上

    @Configuration
    public class ValidateCodeBeanConfig {
    
        @Bean
        @ConditionalOnMissingBean(name = "imageValidateCodeGenerator")
        public ValidateCodeGenerator imageValidateCodeGenerator() {
            ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
            return codeGenerator;
        }
    
        @Bean
        @ConditionalOnMissingBean(SmsCodeSender.class)
        public SmsCodeSender smsCodeSender() {
            return new DefaultSmsCodeSender();
        }
    }

    4.验证码处理器控制类 ValidateCodeProcessorHolder

    @Component
    public class ValidateCodeProcessorHolder {
    
        @Autowired
        private Map<String, ValidateCodeProcessor> validateCodeProcessors;
    
        public ValidateCodeProcessor findValidateCodeProcessor(ValidateCodeType type) {
            return findValidateCodeProcessor(type.toString().toLowerCase());
        }
    
        public ValidateCodeProcessor findValidateCodeProcessor(String type) {
            String name = type.toLowerCase() + ValidateCodeProcessor.class.getSimpleName();
            ValidateCodeProcessor processor = validateCodeProcessors.get(name);
            if (processor == null) {
                throw new ValidateCodeException("验证码处理器" + name + "不存在");
            }
            return processor;
        }
    }

    5.接下去就是需要我们的拦截器登场了

    @Component("validateCodeFilter")
    public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
    
        /**
         * 验证码校验失败处理器
         */
        @Autowired
        private AuthenticationFailureHandler authenticationFailureHandler;
        /**
         * 系统中的校验码处理器
         */
        @Autowired
        private ValidateCodeProcessorHolder validateCodeProcessorHolder;
        /**
         * 存放所有需要校验验证码的url
         */
        private Map<String, ValidateCodeType> urlMap = new HashMap<>();
        /**
         * 验证请求url与配置的url是否匹配的工具类
         */
        private AntPathMatcher pathMatcher = new AntPathMatcher();
    
        /**
         * 初始化要拦截的url配置信息
         */
        @Override
        public void afterPropertiesSet() throws ServletException {
            super.afterPropertiesSet();
    
            urlMap.put("/authentication/form", ValidateCodeType.IMAGE);
            addUrlToMap("/user/*", ValidateCodeType.IMAGE);
    
            urlMap.put("/authentication/mobile", ValidateCodeType.SMS);
            addUrlToMap("/user/*", ValidateCodeType.SMS);
        }
    
        /**
         * 系统中配置的需要校验验证码的URL根据校验的类型放入map*/
        protected void addUrlToMap(String urlString, ValidateCodeType type) {
            if (StringUtils.isNotBlank(urlString)) {
                String[] urls = StringUtils.splitByWholeSeparatorPreserveAllTokens(urlString, ",");
                for (String url : urls) {
                    urlMap.put(url, type);
                }
            }
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
                throws ServletException, IOException {
    
            ValidateCodeType type = getValidateCodeType(request);
            if (type != null) {
                logger.info("校验请求(" + request.getRequestURI() + ")中的验证码,验证码类型" + type);
                try {
                    validateCodeProcessorHolder.findValidateCodeProcessor(type)
                            .validate(new ServletWebRequest(request, response));
                    logger.info("验证码校验通过");
                } catch (ValidateCodeException exception) {
                    authenticationFailureHandler.onAuthenticationFailure(request, response, exception);
                    return;
                }
            }
    
            chain.doFilter(request, response);
    
        }
    
        /**
         * 获取校验码的类型,如果当前请求不需要校验,则返回null
         */
        private ValidateCodeType getValidateCodeType(HttpServletRequest request) {
            ValidateCodeType result = null;
            if (!StringUtils.equalsIgnoreCase(request.getMethod(), "get")) {
                Set<String> urls = urlMap.keySet();
                for (String url : urls) {
                    if (pathMatcher.match(url, request.getRequestURI())) {
                        result = urlMap.get(url);
                    }
                }
            }
            return result;
        }
    }

      这里我们通过枚举来定义处理器类型,同时需要实现我们的异常类:

    public enum ValidateCodeType {
    
        /**
         * 短信验证码
         */
        SMS {
            @Override
            public String getParamNameOnValidate() {
                return "smsCode";
            }
        },
        /**
         * 图片验证码
         */
        IMAGE {
            @Override
            public String getParamNameOnValidate() {
                return "imageCode";
            }
        };
    
        /**
         * 校验时从请求中获取的参数的名字
         * @return
         */
        public abstract String getParamNameOnValidate();
    
    }
    public class ValidateCodeException extends AuthenticationException {
    
        private static final long serialVersionUID = -7285211528095468156L;
    
        public ValidateCodeException(String msg) {
            super(msg);
        }
    }

    6.接下去就是需要把这个拦截器加入到 spring security的拦截器链中,就是在配置类中将我们的拦截器注入,然后通过调用http.addFilterBefore添加到某个拦截器前面,就可以了:

    http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)

    7.提供一个接口进行验证码获取:

    @RestController
    public class ValidateCodeController {
    
        @Autowired
        private ValidateCodeProcessorHolder validateCodeProcessorHolder;
    
        /**
         * 创建验证码,根据验证码类型不同,调用不同的*/
        @GetMapping("/code/{type}")
        public void createCode(HttpServletRequest request, HttpServletResponse response, @PathVariable String type)
                throws Exception {
            validateCodeProcessorHolder.findValidateCodeProcessor(type).create(new ServletWebRequest(request, response));
        }
    }

      经过以上的步骤我们就能获取到验证码了,可以看到接口上有个 type属性。这个属性用于区分时图片验证码还是短信验证码,是因为这两个处理器在注入容器的时候命名来决定的,所以type可以为 image 、sms 类型。小伙伴本也可以进行代码重构,将其中的一些配置通过配置类的方式注入。

    spring security 记住我:

      在Security 中要实现记住我功能很简单,先来看代码:

    @Configuration
    @EnableWebSecurity// 开启Security
    @EnableGlobalMethodSecurity(prePostEnabled = true)//开启Spring方法级安全
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        // Secutiry 处理链
    //    SecurityContextPersistenceFilter
    //    --> UsernamePasswordAuthenticationFilter
    //    --> BasicAuthenticationFilter
    //    --> ExceptionTranslationFilter
    //    --> FilterSecurityInterceptor
        @Autowired
        private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    
        @Autowired
        private MyAuthenticationProvider myAuthenticationProvider;
    
        @Autowired
        private MyAuthenctiationFailureHandler myAuthenctiationFailureHandler;
    
        @Autowired
        private MyUserDetailService myUserDetailService;
    
        @Autowired
        private ValidateCodeFilter validateCodeFilter;
    
        @Autowired
        private DataSource dataSource;
    
        //密码加密
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        // 自定义认证配置
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.authenticationProvider(myAuthenticationProvider);
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //关闭Security功能
    //        http.csrf().disable()
    //                .authorizeRequests()
    //                .anyRequest().permitAll()
    //                .and().logout().permitAll();
    
            http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                        .authorizeRequests()
                        .antMatchers("/wuzz/test4","/code/*").permitAll() //不需要保护的资源,可以多个
                        .antMatchers("/wuzz/**").authenticated()// 需要认证得资源,可以多个
                        .and()
                    .formLogin().loginPage("http://localhost:8080/#/login")//自定义登陆地址
                        .loginProcessingUrl("/authentication/form") //登录处理地址
                        .successHandler(myAuthenticationSuccessHandler) // 登陆成功处理器
                        .failureHandler(myAuthenctiationFailureHandler) // 登陆失败处理器
                        .permitAll()
                        .and()
                        .userDetailsService(myUserDetailService)//设置userDetailsService,处理用户信息
                    .rememberMe()//实现记住我功能 RememberMeAuthenticationFilter
                    .tokenRepository(persistentTokenRepository())
                    .tokenValiditySeconds(3600)
    
            ;
            http.headers().cacheControl(); //禁用缓存
            http.csrf().disable(); //禁用csrf校验
        }
        //忽略的uri
    //    @Override
    //    public void configure(WebSecurity web) throws Exception {
    //        web.ignoring()
    //                .antMatchers( "/api/**", "/resources/**", "/static/**", "/public/**", "/webui/**", "/h2-console/**"
    //                        , "/configuration/**",  "/swagger-resources/**", "/api-docs", "/api-docs/**", "/v2/api-docs/**"
    //                        ,  "/**/*.css", "/**/*.js","/**/*.ftl", "/**/*.png ", "/**/*.jpg", "/**/*.gif ", "/**/*.svg", "/**/*.ico", "/**/*.ttf", "/**/*.woff");
    //    }
    
        @Bean
        public PersistentTokenRepository persistentTokenRepository() {
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            tokenRepository.setDataSource(dataSource);
            //启动的时候是否创建该表,这个表格是保存用户登录信息的
    //        tokenRepository.setCreateTableOnStartup(true);
            return tokenRepository;
        }
    }

      上述代码中新增注入两个类DataSource、PersistentTokenRepository(通过jdbcTemplate实现),因为记住我功能是将token信息存入到数据库,实现类由两种,一种基于内存,另一种基于数据库,后者比较有保障。所以这里使用JDBC,当然需要在application.properties 加入以下配置:

    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://192.168.1.101:3306/study?useUnicode=true&characterEncoding=utf-8
    spring.datasource.username=root
    spring.datasource.password=123456

      然后配置启用记住我功能:

    .rememberMe()//实现记住我功能 RememberMeAuthenticationFilter
    .tokenRepository(persistentTokenRepository())
     .tokenValiditySeconds(3600) // 过期时间

      就这样就配置好了记住我功能,然后需要我们在前端请求登录的时候加上参数 remember-me,这个参数是固定的。在 AbstractRememberMeServices 类中声明:

    public static final String SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY = "remember-me";

      当我们登陆的时候这个值传的是 true,那么就会进行进驻我操作。

       记住我操作的源码可以阅读 RememberMeAuthenticationFilter,流程很简单。多过几遍就清晰了。

  • 相关阅读:
    配置Express中间件
    C#字符串中特殊字符的转义
    JSON.NET 简单的使用
    ASP.NET 解决URL中文乱码的解决
    ASP.NET MVC 笔记
    VS中一些不常用的快捷键
    Visual Studio 中突出显示的引用
    Silverlight从客户端上传文件到服务器
    silverlight打开和保存文件
    sliverlight资源文件的URI调用
  • 原文地址:https://www.cnblogs.com/wuzhenzhao/p/13169023.html
Copyright © 2011-2022 走看看