七、手机短信验证码登陆

源码下载

摘要

前面做的用户名和密码登陆是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();
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,794评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,050评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,587评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,861评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,901评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,898评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,832评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,617评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,077评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,349评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,483评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,199评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,824评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,442评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,632评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,474评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,393评论 2 352