自實現oauth2驗證與spring-security的結合

自實現oauth2驗證與spring-security的結合

前言

前面寫了一篇關於spring-security-oauth2適配的文章,但是種種原因,項目中正在使用的spring-security版本暫時不能更換,沒法直接使用spring-security-oauth2,無奈只能自己實現驗證過程。
本文主要總結一下自實現oauth2驗證遇到的一些問題及解決辦法。

一、自實現oauth2驗證v1.0

自己實現一個簡單oauth2驗證過程,很簡單,不需要像開源軟件那樣考慮各種模塊的配合和彈性,也不需要各種模式都支持。
我的第一個版本就是把所有過程放在一個函數中處理,回調函數傳回code之後,使用code去換取AT,然後用AT去獲取用戶信息,這樣就驗證過程就結束了,是不是很簡單?從功能上來看,這麼做是沒問題的,但是會有兩個問題:

1、沒有與spring-security結合起來

也就是驗證雖然結束了,但是後續的權限控制之類的跟安全相關的功能其實是用不起來的。這個最終是使用設置安全上下文的方式來解決的:

//auth就是認證成功後自己生成的UsernamePasswordAuthenticationToken
SecurityContextHolder.getContext().setAuthentication(auth);

2、session id沒有變更

對安全比較瞭解的同學都知道,驗證成功後,需要改變session id,防止之前的session id被利用。而我這個版本的驗證過程,因爲沒有與任何已有的security組件配合,session id肯定是不會變更的。這個問題最終被我用一個比較暴力的方法解決掉:

//request就是請求的HttpServletRequest
request.changeSessionId()

上面就是我之前遇到的兩個問題,雖然都解決了,但是心裏肯定比較虛,是不是還有其它問題沒有發現?畢竟spring-security做了那麼多工作,而我搞的這麼簡單,沒出問題,不代表沒有啊。。。

二、 自實現oauth2驗證v2.0

由於有了上面的顧慮,我就考慮利用spring-security自身的機制來完成這個驗證過程,最終,被我找到了一個不算優雅,但是比較取巧的方法–利用spring-security自身的用戶名密碼驗證機制來幫助實現這一過程。

1、 用戶名密碼驗證機制

用戶名密碼的配置一般在WebSecurityConfigurerAdapter中配置,類似:

//實現安全的一些配置
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
    //設置驗證接口
    http.formLogin().loginProcessingUrl("/login/auth")
                //設置用戶名、密碼的參數名稱
                .usernameParameter("account")
                .passwordParameter("password");

        ...
    }

    //將驗證過程交給自定義驗證工具
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(new MyAuthenticationProvider());
    }
}

實現自定義驗證過程:

public class MyAuthenticationProvider implements AuthenticationProvider {
    public Authentication authenticate(Authentication authentication) {
        //獲取用戶名、密碼
        String username = authentication.getName();
        String password = (String) authentication.getCredentials();
    }
}

2、利用用戶名密碼驗證機制

從上面的用戶名密碼驗證機制可以看出,只要我們能夠利用getName()來獲取code,那剩下的驗證過程通過重寫AuthenticationProvider肯定能夠實現,也就是類似:

http.formLogin().loginProcessingUrl("/login/auth")
                //設置用戶名、密碼的參數名稱
                .usernameParameter("code")

too naive,試了一下,不行。。。老辦法,debug,過程不贅述,簡單講一下涉及用戶名密碼驗證的幾個類及遇到的問題。

1) AbstractAuthenticationProcessingFilter

這個是實現安全驗證的一個核心filter,安全驗證的過濾器都是繼承這個類來完成整個驗證過程。該類的doFilter函數:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
    if (!this.requiresAuthentication(request, response)) {
        chain.doFilter(request, response);
    } else {
        Authentication authResult;
        try {
            authResult = this.attemptAuthentication(request, response);

            //成功驗證後的session處理
            this.sessionStrategy.onAuthentication(authResult, request, response);
        } catch (InternalAuthenticationServiceException var8) {
            ...
        }
        //成功驗證後處理
        this.successfulAuthentication(request, response, chain, authResult);
    }
}

其中,this.sessionStrategy.onAuthentication就是處理上文中提到的session id變更的問題,而this.successfulAuthentication就是處理上文中提到的安全上下文設置的問題。雖然從源碼中看,主要涉及的也就是這兩項,但是源碼中名顯然做了更多的工作,利用spring-security自身機制始終是更好的選擇。。。

繼續,我的請求是在requiresAuthentication這個函數中被拒掉的,原因是http method不是post。。。直接引出了第二個類。

2)FormLoginConfigurer

該類就是http.formLogin()函數返回的設置類,而設置監聽接口的函數:

protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
    return new AntPathRequestMatcher(loginProcessingUrl, "POST");
}

看到這段代碼,心裏立馬有一萬隻CNM飄過。。。爲毛POST是寫死的,爲毛訪問控制是protected。。。
沒有辦法,翻來覆去,確認了確實沒有辦法在原有代碼的基礎上設置http method。。。
咱也不是菜鳥,重寫,仿照FormLoginConfigurer重寫了configer,當然這個過程很簡單,最主要的其實只要重寫這個函數就行,其它的函數基本都可以刪除,然後,需要的是將自定義configer應用到HttpSecurity上:

MyLoginConfigure<HttpSecurity> loginConfigure = new MyLoginConfigure<>();
loginConfigure.setBuilder(http);

loginConfigure.loginProcessingUrl("/login/auth")
        .usernameParameter("code");

http.apply(loginConfigure);

跟默認設置差不多,只不過需要apply一下。

繼續,又不行,吐血,直接引出第三個類。

3)UsernamePasswordAuthenticationFilter

該類繼承AbstractAuthenticationProcessingFilter,實現attemptAuthentication函數來執行驗證工作:

//attemptAuthentication函數節選
if (this.postOnly && !request.getMethod().equals("POST")) {
    throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} 

這次是掛在this.postOnly,默認是true,簡單,這個在自定義configer構造函數中直接設置一下就ok:

getAuthenticationFilter().setPostOnly(false);

總結

最終結果,只需要重新實現一個configer就能達到目的,其他的功能都是按照spring-security自身的機制在運作。不算優雅,總之還算是個不錯的方法。
遺留一個問題,oauth2模式是支持攜帶state來防止CSRF的(個人覺得意義不大,code本身應該能解決這個問題,更多的我認爲還是防暴力攻擊),通常做法是state存在session中,然後跟回調的state相比較,如果不一致,則請求可以丟棄。 但是在調試過程中,發現存在請求的session與回調的session(全新session)不一致的情況,這尼瑪就沒法搞了啊,state肯定不一樣啊。。。不是必現,但是頻率比較高,沒定位出來,尚不清楚是三方服務器的原因還是代碼的原因,spring-security-oauth2也有這個問題。雖然不影響功能使用,但是有時間還是需要研究一下的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章