美文网首页SpringSecurity
七、手机短信验证码登陆

七、手机短信验证码登陆

作者: 紫荆秋雪_文 | 来源:发表于2020-05-05 15:16 被阅读0次

    源码下载

    摘要

    前面做的用户名和密码登陆是SpringSecurity框架内部集成好的功能,但是手机短信验证码登陆SpringSecurity框架并没有内部实现这个功能,所以需要使用者自己去实现手机短信验证码的功能,最后在把自己的实现嫁接到SpringSecurity框架上。

    一、手机短信验证码实现原理

    通过前面对SpringSecurity的用户名密码登陆源码的分析,主要流程就是:

    • 1、通过过滤器(UsernamePasswordAuthenticationFilter)来拦截指定的url接口
    • 2、通过用户名和密码来生成一个用于认证的Token(UsernamePasswordAuthenticationToken)
    • 3、然后通过AuthenticationManager管理器(ProviderManager)来找到一个合适的处理器(DaoAuthenticationProvider)处理UsernamePasswordAuthenticationToken
    • 4、通过UserDetailsService接口中loadUserByUsername方法把用户信息传给SpringSecurity框架,然后与UsernamePasswordAuthenticationToken中的用户名和密码进行比对校验,如果校验成功,那就认证通过
    • 5、认证通过后把带有权限的UserDetails存储到session
    • 6、加入自定义图形验证码时,需要在UsernamePasswordAuthenticationFilter过滤器之前再加上一个验证码过滤器,图片验证码验证通过后才可以继续执行后面的步骤 用户名密码登陆流程.png

      通过上面对用户名密码登陆的分析,可以推出手机验证码登陆流程如下

    • 1、自定义手机短信验证过滤器(RavenMobileCodeValidateFilter)
    • 2、拦截指定url的过滤器(RavenMobileCodeAuthenticationFilter)
    • 3、封装手机号为Token(RavenMobileCodeAuthenticationToken)
    • 4、用户处理Token的Provider(RavenMobileCodeAuthenticationProvider)
    • 5、通过UserDetailsService加载用户信息
    • 6、认证通过后把带有权限的UserDetails存储到session


      手机短信验证登陆.png

    二、编写代码

    • RavenMobileCodeAuthenticationFilter 过滤器
    /**
     * 手机短信验证码过滤器
     */
    public class RavenMobileCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    
        public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
    
        private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
        private boolean postOnly = true;
    
    
        /**
         * 指定拦截的请求url 和 请求方法
         */
        public RavenMobileCodeAuthenticationFilter() {
            super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
        }
    
    
        /**
         * 认证逻辑
         */
        public Authentication attemptAuthentication(HttpServletRequest request,
                                                    HttpServletResponse response) throws AuthenticationException {
            if (postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException(
                        "Authentication method not supported: " + request.getMethod());
            }
    
            String mobile = obtainMobile(request);
    
            if (mobile == null) {
                mobile = "";
            }
    
            mobile = mobile.trim();
    
            RavenMobileCodeAuthenticationToken authRequest = new RavenMobileCodeAuthenticationToken(mobile);
    
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
    
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    
    
    //    protected String obtainPassword(HttpServletRequest request) {
    //        return request.getParameter(passwordParameter);
    //    }
    
    
        protected String obtainMobile(HttpServletRequest request) {
            return request.getParameter(mobileParameter);
        }
    
    
        protected void setDetails(HttpServletRequest request,
                                  RavenMobileCodeAuthenticationToken authRequest) {
            authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
        }
    
        public void setMobileParameter(String mobileParameter) {
            Assert.hasText(mobileParameter, "Username parameter must not be empty or null");
            this.mobileParameter = mobileParameter;
        }
    
        public void setPasswordParameter(String passwordParameter) {
            Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
        }
    
        public void setPostOnly(boolean postOnly) {
            this.postOnly = postOnly;
        }
    
        public final String getMobileParameter() {
            return mobileParameter;
        }
    
        public final String getPasswordParameter() {
            return null;
        }
    }
    
    • RavenMobileCodeAuthenticationToken
    public class RavenMobileCodeAuthenticationToken extends AbstractAuthenticationToken {
    
        private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    
        /**
         * 认证主题
         * 认证前:principal存储的是手机号
         * 认证后:principal存储的是认证信息
         */
        private final Object principal;
    
    
        /**
         * 通过手机号来构造
         */
        public RavenMobileCodeAuthenticationToken(String mobile) {
            super(null);
            this.principal = mobile;
            setAuthenticated(false);
        }
    
        /**
         * 认证成功后
         * principal:认证信息
         * 设置被已认证
         */
        public RavenMobileCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            this.principal = principal;
            super.setAuthenticated(true); // must use super, as we override
        }
    
        public Object getCredentials() {
            return null;
        }
    
        public Object getPrincipal() {
            return this.principal;
        }
    
        public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
            if (isAuthenticated) {
                throw new IllegalArgumentException(
                        "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
            }
    
            super.setAuthenticated(false);
        }
    
        @Override
        public void eraseCredentials() {
            super.eraseCredentials();
        }
    }
    
    • RavenMobileCodeAuthenticationProvider
    @Setter
    @Getter
    public class RavenMobileCodeAuthenticationProvider implements AuthenticationProvider {
    
        private UserDetailsService userDetailsService;
    
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            RavenMobileCodeAuthenticationToken mobileToken = (RavenMobileCodeAuthenticationToken) authentication;
            // 通过手机号来验证
            UserDetails userDetails = this.userDetailsService.loadUserByUsername((String) mobileToken.getPrincipal());
            if (userDetails == null) {
                throw new InternalAuthenticationServiceException("无法获取用户信息");
            }
            /**
             * 到这里认证通过
             * 通过重新创建RavenMobileCodeAuthenticationToken,使用2个参数的构造器
             * 这样认证标识会被设置为true
             */
            RavenMobileCodeAuthenticationToken mobileAuthenticationToken = new RavenMobileCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
            // 重新赋值
            mobileAuthenticationToken.setDetails(mobileToken.getDetails());
            return mobileAuthenticationToken;
        }
    
        /**
         * AuthenticationManager 选择要执行的Provider
         * 这里就决定了不同的 Provider 执行不同的 Token
         * RavenMobileCodeAuthenticationToken
         */
        @Override
        public boolean supports(Class<?> authentication) {
            return (RavenMobileCodeAuthenticationToken.class
                    .isAssignableFrom(authentication));
        }
    }
    
    • 添加配置,把自定义的Provider和Filter添加到SpringSecurity的过滤器链上
    @Component
    public class RavenMobileCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    
        @Autowired
        private AuthenticationSuccessHandler successHandler;
        @Autowired
        private AuthenticationFailureHandler failureHandler;
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
    
            // 手机短信登录过滤器
            RavenMobileCodeAuthenticationFilter mobileFilter = new RavenMobileCodeAuthenticationFilter();
            mobileFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
            mobileFilter.setAuthenticationSuccessHandler(this.successHandler);
            mobileFilter.setAuthenticationFailureHandler(this.failureHandler);
    
            //Provider
            RavenMobileCodeAuthenticationProvider mobileProvider = new RavenMobileCodeAuthenticationProvider();
            mobileProvider.setUserDetailsService(this.userDetailsService);
    
            // 把自定义Provider 和 Filter 加入到SpringSecurity的过滤器链上
            http.authenticationProvider(mobileProvider)
                    .addFilterAfter(mobileFilter, UsernamePasswordAuthenticationFilter.class);
        }
    }
    

    三、手机短信登录验证

    • bw-login.html
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>首页</title>
    </head>
    <body>
    
    <h3>短信登录</h3>
    <form action="/authentication/mobile" method="post">
        <table>
            <tr>
                <td>手机号:</td>
                <td><input type="text" name="mobile" value="18212345678"></td>
            </tr>
            <tr>
                <td>短信验证码:</td>
                <td>
                    <input type="text" name="smsCode">
                    <a href="/code/sms?mobile=18212345678">发送验证码</a>
                </td>
            </tr>
            <tr>
                <td colspan='2'><input name="remember-me" type="checkbox" value="true" />记住我</td>
            </tr>
            <tr>
                <td colspan="2"><button type="submit">登录</button></td>
            </tr>
        </table>
    </form>
    </body>
    </html>
    
    
    • 处理验证码请求
    @RestController
    public class RavenValidateCodeController {
    
    
        /**
         * 操作session的工具类
         */
        private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    
        @Autowired
        private IRavenValidateCodeGenerator imageValidateCodeGenerator;
        @Autowired
        private IRavenMobileValidateCodeGenerator mobileValidateCodeGenerator;
        @Autowired
        private IRavenMobileCodeSendService mobileCodeSendService;
    
    
        @GetMapping("/code/image")
        public void createCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
            // 生成图片
            RavenImageCode imageCode = this.imageValidateCodeGenerator.generator(new ServletWebRequest(request, response));
            // 保存
            sessionStrategy.setAttribute(new ServletWebRequest(request, response), RavenSecurityConstants.SESSION_KEY_PREFIX, imageCode);
            // 发送
            ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
        }
    
        @GetMapping("/code/sms")
        public void createMobileCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
            // 获取手机号
            String mobile = ServletRequestUtils.getStringParameter(request, "mobile");
            // 生成验证码
            RavenValidateCode validateCode = this.mobileValidateCodeGenerator.generator(new ServletWebRequest(request, response));
            // 保存
            sessionStrategy.setAttribute(new ServletWebRequest(request, response), RavenSecurityConstants.SESSION_KEY_PREFIX, validateCode);
            // 发送
            this.mobileCodeSendService.send(mobile, validateCode.getCode());
        }
    
    }
    
    • 验证码RavenValidateCode
    @Setter
    @Getter
    public class RavenValidateCode {
        private String code;
        private LocalDateTime expireTime;
    
        public RavenValidateCode(String code, int expireIn) {
            this.code = code;
            this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
        }
    
        public boolean isExpried() {
            return LocalDateTime.now().isAfter(this.expireTime);
        }
    }
    
    • 短信生成器IRavenMobileValidateCodeGenerator
    public interface IRavenMobileValidateCodeGenerator {
    
        /**
         * 生成验证码
         */
        RavenValidateCode generator(ServletWebRequest request);
    }
    
    • 默认手机验证码生成器
    /**
     * 默认的手机验证码生成器
     */
    @Setter
    public class DefaultRavenMobileValidateCodeGenerator implements IRavenMobileValidateCodeGenerator {
    
        private RavenSecurityProperties securityProperties;
    
    
        @Override
        public RavenValidateCode generator(ServletWebRequest request) {
            int expireIn = securityProperties.getValidate().getMobile().getExpireIn();
            int count = securityProperties.getValidate().getMobile().getCount();
            String code = RandomStringUtils.randomNumeric(count);
            return new RavenValidateCode(code, expireIn);
        }
    }
    
    • 发送验证码到手机 IRavenMobileCodeSendService
    public interface IRavenMobileCodeSendService {
        /**
         * 给手机发送短信
         * 默认实现使用服务器随机生成验证码
         */
        void send(String mobile, String code);
    }
    
    • 默认发送验证码到手机实现 DefaultRavenMobileCodeSendServiceImpl
    /**
     * 由于 IRavenMobileCodeSendService 需要外接来实现发送短信验证码到手机
     * 所以这里就不能直接入住的spring容器中,需要通过Bean的方式来配置注入
     */
    public class DefaultRavenMobileCodeSendServiceImpl implements IRavenMobileCodeSendService {
    
        private Logger logger = LoggerFactory.getLogger(getClass());
    
        @Override
        public void send(String mobile, String code) {
            logger.info("发送验证码:" + code + " 到手机:" + mobile + " 请注意查收");
        }
    }
    
    • 短信验证码生成器、发送短信验证码Bean配置。因为需要外接自己实现,所以就以配置Bean的方式来注入容器
    @Configuration
    public class RavenBeanConfig {
    
        @Autowired
        private RavenSecurityProperties securityProperties;
    
        @Bean
        PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        /**
         * 图形验证码生成器
         * 由于IRavenValidateCodeGenerator这个接口是为了让外界实现,所以不能在它默认的实现类
         * DefaultRavenValidateCodeGenerator上直接使用@Component,防止会造成2个Bean,
         * 所以需要使用@ConditionalOnMissingBean(name = "imageValidateCodeGenerator")
         * 来判断Bean imageValidateCodeGenerator是否存在,如果存在就不会执行下面的Bean
         */
        @Bean
        @ConditionalOnMissingBean(name = "imageValidateCodeGenerator")
        IRavenValidateCodeGenerator imageValidateCodeGenerator() {
            DefaultRavenValidateCodeGenerator generator = new DefaultRavenValidateCodeGenerator();
            generator.setSecurityProperties(this.securityProperties);
            return generator;
        }
    
        /**
         * 手机短信验证码生成器
         */
        @Bean
        @ConditionalOnMissingBean(name = "mobileValidateCodeGenerator")
        IRavenMobileValidateCodeGenerator mobileValidateCodeGenerator() {
            DefaultRavenMobileValidateCodeGenerator generator = new DefaultRavenMobileValidateCodeGenerator();
            generator.setSecurityProperties(this.securityProperties);
            return generator;
        }
    
        /**
         * 发送手机短信
         */
        @Bean
        @ConditionalOnMissingBean(name = "mobileCodeSendService")
        IRavenMobileCodeSendService mobileCodeSendService() {
            DefaultRavenMobileCodeSendServiceImpl mobileCodeSendImpl = new DefaultRavenMobileCodeSendServiceImpl();
            return mobileCodeSendImpl;
        }
    }
    
    • 自己实现手机短信的生成
    @Component("mobileValidateCodeGenerator")
    public class DemoMobileCodeGenerator implements IRavenMobileValidateCodeGenerator {
        @Override
        public RavenValidateCode generator(ServletWebRequest request) {
            return new RavenValidateCode("111", 100);
        }
    }
    
    • 自己实现手机短信发送
    @Component("mobileCodeSend")
    public class DemoMobileCodeSend implements IRavenMobileCodeSendService {
        private Logger logger = LoggerFactory.getLogger(getClass());
        @Override
        public void send(String mobile, String code) {
            logger.info(mobile);
            logger.info(code);
        }
    }
    
    • 配置
    /**
     * Web端security配置
     */
    @Configuration
    public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private RavenSecurityProperties securityProperties;
        @Autowired
        private BrowserAuthenticationSuccessHandler browserAuthenticationSuccessHandler;
        @Autowired
        private BrowserAuthenticationFailureHandler browserAuthenticationFailureHandler;
        @Autowired
        private RavenValidateCodeFilter validateCodeFilter;
        @Autowired
        private RavenMobileCodeAuthenticationSecurityConfig mobileCodeConfig;
        @Autowired
        private HikariDataSource hikariDataSource;
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Bean
        public PersistentTokenRepository persistentTokenRepository() {
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            tokenRepository.setDataSource(this.hikariDataSource);
    //        tokenRepository.setCreateTableOnStartup(true);
            return tokenRepository;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 配置登录界面
            String loginPage = this.securityProperties.getBrowser().getLoginPage();
            int tokenTime = this.securityProperties.getBrowser().getTokenTime();
    
            RavenMobileValidateCodeFilter mobileFilter = new RavenMobileValidateCodeFilter();
            mobileFilter.setFailureHandler(this.browserAuthenticationFailureHandler);
            mobileFilter.setSecurityProperties(this.securityProperties);
            mobileFilter.afterPropertiesSet();
    
    
            http.csrf().disable()
                    .apply(this.mobileCodeConfig)
                    .and()
                    .addFilterBefore(this.validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                    .addFilterBefore(mobileFilter, UsernamePasswordAuthenticationFilter.class)
                    .formLogin()
                    .loginPage("/authentication/require")    // 当需要身份认证时,跳转到这里
                    .loginProcessingUrl("/authentication/form") // 默认处理的/login,自定义登录界面需要指定请求路径
                    .successHandler(this.browserAuthenticationSuccessHandler)
                    .failureHandler(this.browserAuthenticationFailureHandler)
                    .and()
                    .rememberMe()
                    .tokenRepository(persistentTokenRepository())
                    .tokenValiditySeconds(tokenTime)
                    .userDetailsService(this.userDetailsService)
                    .and()
                    .authorizeRequests()
                    .antMatchers("/authentication/require",
                            loginPage,
                            "/code/*"
                    ).permitAll()
                    .anyRequest()
                    .authenticated();
        }
    }
    

    相关文章

      网友评论

        本文标题:七、手机短信验证码登陆

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