基於OAUTH2的統一認證的例項解析
基於OAUTH2的統一認證的例項解析
在一個單位中,可能是存在多個不同的應用,比如學校會有財務的系統會有學生工作的系統,還有圖書館的系統等等,如果每個系統都用獨立的賬號認證體系,會給使用者帶來很大困擾,也給管理帶來很大不便。所以需要設計一種統一登入的解決方案。比如我登陸了百度賬號,進貼吧時發現已經登入了,進糯米發現也自動登入了。常見的有兩種情況,一種是SSO(單點登入)效果是一次輸入密碼多個網站可以識別線上狀態;還有一種是多平臺登入,效果是可以用一個賬號(比如QQ賬號)登入多個不同的網站。
SSO與多平臺登入
SSO一般用於同一單位的多個站點的登陸狀態保持,技術上一般參考CAS協議;多平臺登入一般是Oauth體系的協議,有多種認證模式但是不具備會話管理和狀態保持。
不過從本質上講,我覺得兩者都是通過可信的第三方進行身份驗證,如果說同一單位的多個子系統共同只圍繞一個第三方賬戶(可以稱為認證中心)進行多平臺登入驗證,那麼在第三方平臺登入後再訪問其他網站,效果和統一登入是差不多的。此外,Oauth2還有個好處就是可以實現跨平臺的登入管理,因為他的認證過程不依賴於session和cookie,比如對於移動端裝置,以及在前後端分離後這種登入認證方式也可以起到很大作用。
這篇文章裡我就著結合之前專案中整合過的OAUTH2來講一講這種登入認證的過程。專案是基於Shiro+ALTU實現,參考方案
oauth2的基本概念
在Oauth中至少是有使用者,應用伺服器,認證伺服器這幾個角色在互動。OAuth的作用就是讓"客戶端"安全可控地獲取"使用者"的授權,與"應用伺服器"進行互動。
OAuth2的基本流程
使用者通過瀏覽器訪問一個應用,比如我要上慕課網學習。
- 網站要求我登入,我選擇使用QQ登入,這裡的QQ登入就是那個認證伺服器。
- 這個時候慕課提供的QQ登入連結會把我帶到QQ登入頁面
- 在QQ的登入頁面完成登入後,選擇授權,也就是允許慕課網獲取我的資料。
- 這個時候我們看到瀏覽器經過幾次跳轉後返回慕課網,這個時候我們已經完成了登入。
重點在於幾次跳轉的過程中,慕課網和QQ登入的服務之間還有過幾次互動。
- 我們選擇了授權的時候QQ登入伺服器會根據慕課跳轉到QQ時候給出的重定向連結返回給慕課網一個code,這個code代表QQ的登入伺服器認可慕課網這個應用伺服器的這個請求是合法的予以放行.
- 慕課這個時候就會用這個code再次向QQ登入服務發起請求服務令牌(token)。
- 拿到這個令牌之後,接下來慕課需要使用者的一些基本資訊時就可以通過在向QQ服務提交的請求頭裡帶上這個令牌,令牌驗證通過就可以拿到使用者資源。
這一部分的操作是應用伺服器和驗證伺服器之間的互動,這個過程對使用者是透明的。這個過程中慕課網是不需要知道使用者的賬號密碼也可以完成對使用者身份的認證,這個token就可以用來標識使用者資源。
官方的執行流程圖是這樣的:
OAuth的幾種認證模式
上述講的是OAuth2中支援的授權碼(CODE)方式的認證流程,也是其支援的四種認證方式裡最複雜的,其他的三種種包括:
- 簡化模式(implicit),(在redirect_uri 的Hash傳遞token; Auth客戶端執行在瀏覽器中,如JS,Flash)
- 密碼模式(resource owner password credentials),將使用者名稱,密碼傳過去,直接獲取token;
- 客戶端模式(client credentials),無使用者,使用者向客戶端註冊,然後客戶端以自己的名義向'服務端'獲取資源;
詳細的OAuth2資料參考理解OAuth 2.0|阮一峰的網路日誌
分別適用不同場景,複雜度也比授權碼模式要低,所以這裡就只說說授權碼模式的具體過程。
CODE方式認證例項
假設現在有一個應用伺服器跑在我本機8000埠,認證伺服器在8090埠。在需要使用者登入時候把使用者帶到以下的一個URL.
http://localhost:8090/oauth/authorize?response_type=code&scope=read write&client_id=test&redirect_uri=http://localhost:8000/login&state=09876999
我們注意到幾個重要的引數:
- response_type:表示授權型別,就是上面講的那四種類型,這裡用的是code方式。
- client_id:表示客戶端的ID,代表哪個應用請求驗證
- redirect_uri:表示重定向URI,驗證以後的回撥地址,一般用來接收返回的code,以及做下一步處理。
- scope:表示申請的許可權範圍,
- state:表示客戶端的當前狀態,可以指定任意值,認證伺服器會原封不動地返回這個值。作為安全校驗。
下面是驗證伺服器接受這個請求的控制器關鍵程式碼:
@RequestMapping("authorize")
public void authorize(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
OAuthAuthxRequest oauthRequest = new OAuthAuthxRequest(request);
if (oauthRequest.isCode()) {
CodeAuthorizeHandler codeAuthorizeHandler = new CodeAuthorizeHandler(oauthRequest, response);
LOG.debug("Go to response_type = 'code' handler: {}", codeAuthorizeHandler);
codeAuthorizeHandler.handle();
} else if (oauthRequest.isToken()) {
TokenAuthorizeHandler tokenAuthorizeHandler = new TokenAuthorizeHandler(oauthRequest, response);
LOG.debug("Go to response_type = 'token' handler: {}", tokenAuthorizeHandler);
tokenAuthorizeHandler.handle();
} else {
unsupportResponseType(oauthRequest, response);
}
}
}
首先拿到這個請求以後根據請求的引數將其封裝成一個OAuthAuthxRequest
,基本就是把請求過來的引數,方法繫結便於使用。這是由oltu提供的OAuthRequest
的進一步封裝。
然後判斷這個請求的授權的型別是否是code,也就是判斷下請求引數的response_type
是否為code,可以看到目前製作了兩種型別的授權。
然後根據對應的授權型別,構造對應的方法處理器。下面是handle的實現介面:
public void handle() throws OAuthSystemException, ServletException, IOException {
//驗證請求是否合法,主要是針對引數做基本的校驗,重定向連結,客戶端ID授權範圍等這些資訊與註冊的是否相同。
if (validateFailed()) {
return;
}
//判斷使用者是否登入過,根據session判斷。因此多個應用使用同一個授權服務的話,是可以直接跳過登入步驟的也就實現了單點登入的效果。如果沒有登入的話,這一步的請求會被重定向至登入頁面。(登入也得隱藏域會帶上這些引數)
if (goLogin()) {
return;
}
//這個請求如果是從登入頁面提交過來的,那麼就提交使用者的登入,這個框架中交給shiro去做登入相關的操作。
if (submitLogin()) {
return;
}
// 本系統中把登入和授權放在兩個步驟中完成,有點像新浪微博的方式,QQ是一步完成授權。使用者未授權則跳轉授權頁面
if (goApproval()) {
return;
}
//與登入類似,也是提交使用者批准或拒絕了許可權請求
if (submitApproval()) {
return;
}
//以上任意一步沒有通過都是授權失敗會進行相應處理,如果都通過了就發放Code碼。
handleResponse();
}
如果以上步驟都通過的話,認證伺服器會轉向這個會調地址,帶上發放的Code碼,類似如下:
http://localhost:8000/login?code=bca654ab6133ab3cbc55bb751da93b1c&state=09876999
可以看到帶回了返回的引數,以及原樣返回的狀態碼。
應用伺服器這時候拿到返回的code去換token,發起如下的一個請求:
localhost:8090/oauth/token?client_id=test&client_secret=test&grant_type=authorization_code&code=bca654ab6133ab3cbc55bb751da93b1c&redirect_uri=http://localhost:8000/login&scope=read%20write&state=09876999
與之前請求類似只是多了一個code欄位,去驗證客戶端的合法性。
驗證伺服器會在收到code以後去查詢是否有支援這種code的處理器,如果有則發放token。
for (OAuthTokenHandler handler : handlers) {
if (handler.support(tokenRequest)) {
LOG.debug("Found '{}' handle OAuthTokenxRequest: {}", handler, tokenRequest);
handler.handle(tokenRequest, response);
return;
}
}
初始化支援的handler
private void initialHandlers() {
handlers.add(new AuthorizationCodeTokenHandler());
handlers.add(new PasswordTokenHandler());
handlers.add(new RefreshTokenHandler());
handlers.add(new ClientCredentialsTokenHandler());
}
驗證通過後應用伺服器會接受到包含token的一個json資料:
{
"access_token": "23e003b5e4b9b7eda228b845532d8336",
"refresh_token": "d6b49710f398c405a62f31a6676c5830",
"token_type": "Bearer",
"expires_in": 43199
}
這個token是有一定的有效期的,在服務端會快取這個token以便下一次查詢,應用客戶端也應該保留這個token,訪問受限資源時候需要帶上這個token去驗證身份。
比如請求一個API如下:
curl -i -X GET \
-H "Authorization:Bearer 33dbfc80f5659c6fdec73a044ff724c3" \
'http://localhost:8090/api/test'
資源伺服器上使用shiro做安全驗證,配置OAuth2對應的realms即可:
<property name="realms">
<list>
<bean id="systemAuthorizingRealm" class="me.kbiao.example.modules.sys.security.SystemAuthorizingRealm"/>
<bean id="oAuth2Realm" class="me.kbiao.example..modules.sys.security.OAuth2Realm"/>
</list>
</property>
在這個reamls中根據token去查到使用者資訊,再去分發對應的資源。
自此便完成了整個oauth2的流程。
這個流程中認證服務系統需要配置三張資料表:
- client_details表中存放註冊的客戶端資料。如回撥地址,授權型別,是否信任,許可權資訊等
- code中存放發放給客戶端應用的code,使用後失效,以保證安全性
- access_token中存放使用者資訊、客戶端和token的對應關係。
專案是基於Shiro+ALTU實現,參考方案mkk/oauth2-shiro - 碼雲 - 開源中國 ,更詳細的內容,可以去讀讀Shengzhao Li
開源的程式碼
總結
本文簡單介紹了幾種統一認證的解決方案,然後詳細介紹了OAuth2的認證流程,並結合例項詳細介紹了CODE授權的流程。儘管OAuth2被廣泛用於多平臺登入解決方案,我覺得在設定cookie、session共享之後也可以被應用於單點登入的解決方案。
在使用Oauth2做前後端分離時遇到的兩個跨域問題的解決方案可以參考我的兩篇部落格