自實現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也有這個問題。雖然不影響功能使用,但是有時間還是需要研究一下的。