美文网首页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