Spring Security源碼分析五:Spring Security實現(xiàn)短信登錄

目前常見的社交軟件惕稻、購物軟件竖共、支付軟件、理財軟件等俺祠,均需要用戶進行登錄才可享受軟件提供的服務(wù)肘迎。目前主流的登錄方式主要有 3 種:賬號密碼登錄甥温、短信驗證碼登錄和第三方授權(quán)登錄。我們已經(jīng)實現(xiàn)了賬號密碼和第三方授權(quán)登錄妓布。本章我們將使用Spring Security實現(xiàn)短信驗證碼登錄姻蚓。

概述

Spring Security源碼分析一:Spring Security認證過程Spring Security源碼分析二:Spring Security授權(quán)過程兩章中。我們已經(jīng)詳細解讀過Spring Security如何處理用戶名和密碼登錄匣沼。(其實就是過濾器鏈)本章我們將仿照用戶名密碼來顯示短信登錄狰挡。

目錄結(jié)構(gòu)

SmsCodeAuthenticationFilter

SmsCodeAuthenticationFilter對應(yīng)用戶名密碼登錄的UsernamePasswordAuthenticationFilter同樣繼承AbstractAuthenticationProcessingFilter

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    /**
     * request中必須含有mobile參數(shù)
     */
    private String mobileParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_MOBILE;
    /**
     * post請求
     */
    private boolean postOnly = true;

    protected SmsCodeAuthenticationFilter() {
        /**
         * 處理的手機驗證碼登錄請求處理url
         */
        super(new AntPathRequestMatcher(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_MOBILE, "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        //判斷是是不是post請求
        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();
        //創(chuàng)建SmsCodeAuthenticationToken(未認證)
        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

        //設(shè)置用戶信息
        setDetails(request, authRequest);
        //返回Authentication實例
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    /**
     * 獲取手機號
     */
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }

    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    public void setMobileParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.mobileParameter = usernameParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getMobileParameter() {
        return mobileParameter;
    }
}
  1. 認證請求的方法必須為POST
  2. 從request中獲取手機號
  3. 封裝成自己的Authenticaiton的實現(xiàn)類SmsCodeAuthenticationToken(未認證)
  4. 調(diào)用 AuthenticationManagerauthenticate 方法進行驗證(即SmsCodeAuthenticationProvider

SmsCodeAuthenticationToken

SmsCodeAuthenticationToken對應(yīng)用戶名密碼登錄的UsernamePasswordAuthenticationToken

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 2383092775910246006L;

    /**
     * 手機號
     */
    private final Object principal;

    /**
     * SmsCodeAuthenticationFilter中構(gòu)建的未認證的Authentication
     * @param mobile
     */
    public SmsCodeAuthenticationToken(String mobile) {
        super(null);
        this.principal = mobile;
        setAuthenticated(false);
    }

    /**
     * SmsCodeAuthenticationProvider中構(gòu)建已認證的Authentication
     * @param principal
     * @param authorities
     */
    public SmsCodeAuthenticationToken(Object principal,
                                      Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true); // must use super, as we override
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    /**
     * @param isAuthenticated
     * @throws IllegalArgumentException
     */
    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();
    }
}

SmsCodeAuthenticationProvider

SmsCodeAuthenticationProvider對應(yīng)用戶名密碼登錄的DaoAuthenticationProvider

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
        //調(diào)用自定義的userDetailsService認證
        UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());

        if (user == null) {
            throw new InternalAuthenticationServiceException("無法獲取用戶信息");
        }
        //如果user不為空重新構(gòu)建SmsCodeAuthenticationToken(已認證)
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());

        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }
    
    /**
     * 只有Authentication為SmsCodeAuthenticationToken使用此Provider認證
     * @param authentication
     * @return
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}

SmsCodeAuthenticationSecurityConfig短信登錄配置

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private AuthenticationFailureHandler merryyouAuthenticationFailureHandler;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        //自定義SmsCodeAuthenticationFilter過濾器
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(merryyouAuthenticationFailureHandler);

        //設(shè)置自定義SmsCodeAuthenticationProvider的認證器userDetailsService
        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
        //在UsernamePasswordAuthenticationFilter過濾前執(zhí)行
        http.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

MerryyouSecurityConfig 主配置文件

 @Override
    protected void configure(HttpSecurity http) throws Exception {
//        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
        http
                .formLogin()//使用表單登錄,不再使用默認httpBasic方式
                .loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)//如果請求的URL需要認證則跳轉(zhuǎn)的URL
                .loginProcessingUrl(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM)//處理表單中自定義的登錄URL
                .and()
                .apply(validateCodeSecurityConfig)//驗證碼攔截
                .and()
                .apply(smsCodeAuthenticationSecurityConfig)
                .and()
                .apply(merryyouSpringSocialConfigurer)//社交登錄
                .and()
                .rememberMe()
......

調(diào)試過程

短信登錄攔截請求/authentication/mobile

自定義SmsCodeAuthenticationProvider

效果如下:

代碼下載

從我的 github 中下載释涛,https://github.com/longfeizheng/logback

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末加叁,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子唇撬,更是在濱河造成了極大的恐慌它匕,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窖认,死亡現(xiàn)場離奇詭異豫柬,居然都是意外死亡,警方通過查閱死者的電腦和手機扑浸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門烧给,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人喝噪,你說我怎么就攤上這事础嫡。” “怎么了酝惧?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵榴鼎,是天一觀的道長。 經(jīng)常有香客問我晚唇,道長巫财,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任缺亮,我火速辦了婚禮翁涤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘萌踱。我一直安慰自己葵礼,他們只是感情好,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布并鸵。 她就那樣靜靜地躺著鸳粉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪园担。 梳的紋絲不亂的頭發(fā)上届谈,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天枯夜,我揣著相機與錄音,去河邊找鬼艰山。 笑死湖雹,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的曙搬。 我是一名探鬼主播摔吏,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼纵装!你這毒婦竟也來了征讲?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤橡娄,失蹤者是張志新(化名)和其女友劉穎诗箍,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挽唉,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡滤祖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了橱夭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片氨距。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡桑逝,死狀恐怖棘劣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情楞遏,我是刑警寧澤茬暇,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站寡喝,受9級特大地震影響糙俗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜预鬓,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一巧骚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧格二,春花似錦劈彪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至长窄,卻和暖如春滔吠,著一層夾襖步出監(jiān)牢的瞬間纲菌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工疮绷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留翰舌,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓冬骚,卻偏偏與公主長得像灶芝,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子唉韭,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容