本來打算這篇直接上代碼的,但是在看書的過程中發現有很多理論性的東西需要深挖,所以接下來我會先依次介紹OAuth中的授權服務器、客戶端、受保護資源這三大服務器具體要做的事情和細節,本篇先來說說授權服務器。
授權服務器在OAuth中主要有以下四個作用:
- 管理已注冊的客戶端
- 用戶對客戶端授權
- 為獲得授權的客戶端頒發令牌
- 頒發刷新令牌並響應令牌刷新請求
其中前兩者由授權端點完成,后兩者由令牌端點完成,如下圖:
👆👆👆 上面這張圖梳理出了每個端點在完成每一步的過程中需要做的事情,接下來對每一步進行詳細解讀。
一、授權端點
1.1 管理客戶端
主要提供客戶端注冊功能,用下面的請求來表示:
curl --location --request POST 'http://authendpoint.com/createClient' \ --header 'Content-Type: application/json' \ --data-raw '{ "clientName": "客戶端名稱", "redirectUris": [ "http://client.com/callback1", "http://client.com/callback2", "http://client.com/callback3" ], "grantTypes": [ "authorization_code", "implicit", "password", "refresh_token" ], "responseTypes": [ "code", "token" ], "scopes": [ "read", "write" ] }'
- clientName:客戶端名稱
- reirectUris:客戶端重定向地址,可以是多個,用來處理不同場景的請求
- grantTypes:許可類型,像上一篇中介紹的授權碼模式就是其中的authorization_code,其他四種許可類型會在后面的文章中進行詳細講解
- responseTypes:請求授權服務器時的響應類型,一般有兩種:授權碼code和令牌token
- scopes:授權的范圍,比如讀權限、寫權限等
當客戶端注冊成功后,授權服務器會返回如下信息給客戶端:
{ "clientId": "daffdafd3554543dafdaf", "clientSecret": "DAAGAS34fdsf" }
- clientId:授權服務器為客戶端分配的唯一標識
- clientSecret:授權服務器用來對客戶端進行身份認證的密鑰
以上兩者都是為了在客戶端請求授權服務器時進行身份認證使用的必要參數,客戶端必須好好保管,且clientSecret不能被泄露!!!
1.2 客戶端授權
授權有兩步:請求授權和用戶確認授權,請求授權會渲染一個授權頁面,用戶在頁面上選擇是否進行授權。
1.2.1 請求授權
用戶在OAuth授權過程中的第一站是授權端點。授權端點是一個前端信道端點,客戶端會將用戶瀏覽器重定向至該端點,以發出授權請求。授權請求通常是一個GET請求,請求示例如下:
http://authendpoint.com/authorize?client_id=daffdafd3554543dafdaf&redirect_uri=http://client.com/callback1&state=dfdgjudbetrdd&response_type=code&scope=read write
- client_id:客戶端唯一標識,客戶端授權服務器注冊時由注冊服務器為之分配,必傳項!
- redirect_uri:客戶端重定向地址,客戶端注冊時提供給授權服務器的多個之中的一個,最好是精確匹配,否則會出現授權碼劫持漏洞(后面介紹),必傳項!
- state:客戶端生成的隨機串,用來在回調函數(重定向地址所請求的方法)中進行驗證是否是同一個請求對應的授權碼,必傳項!
- response_type:請求授權服務器響應的類型,OAuth2中有兩種:授權碼code和令牌token,前者適用於授權碼許可類型,后者適用於隱式許可類型(后面介紹)必傳項!
- scope:資源擁有者委托客戶端請求的權限范圍,非必傳項
授權端點接收到請求之后需要做以下幾件事:
1.從請求參數中獲取到client_id
2.檢查客戶端
- 檢查該client_id對應的客戶端是否存在。如果不存在,則不能授予任何訪問權限,並要向用戶顯示錯誤信息
- 檢查請求的redirect_uri是否在客戶端注冊時提供的redirect_uris里面。OAuth規范允許客戶端注冊時信息中包含多個redirect_uri值,這樣就可以讓客戶端在不同場景下使用不同的URL提供服務,有利於功能聚合。
3.當客戶端檢查通過之后需要渲染出一個頁面來請求用戶進行授權
- 用戶需要與這個頁面交互,並向授權服務器提交授權決策,這將需要瀏覽器向授權服務器發送一個HTTP請求。在這一步,授權端點需要生成一個requestId返回給確認授權頁面,並保存該值,以便在用戶提交授權決策時能夠找到兩次請求之間的關系。
1.2.2 確認授權
用戶在授權頁面選擇授權結果,以及要授權客戶端的權限范圍,並發送確認/拒絕授權的請求給授權端點,授權端點需要提供處理確認授權的方法,請求示例如下:
curl --location --request GET 'http://authendpoint.com/approve?request_id=%E8%AF%B7%E6%B1%82%E6%8E%88%E6%9D%83%E6%97%B6%E8%BF%94%E5%9B%9E%E7%9A%84requestId&approve=Approve%28Deny%29'
- request_id:在請求授權時由授權端點返回的標識,透傳即可
- approve:確認授權的結果,Approve或Deny
授權端點接受到請求之后需要做下面的這些事情:
1.Deny
如果客戶拒絕授權,則需要告訴客戶端實際情況,由於使用前端信道進行通信,無法直接向客戶端發送信息,但是可以采用客戶端向授權服務器發送請求的方式:從客戶端托管的redirect_uri中選擇一個,在該URL上添加一些特殊的查詢參數,然后將用戶的瀏覽器重定向至這個經過改造地址!
2.Approve
如果客戶端同意授權,則首先檢查在1.2.1中請求的響應類型是什么。
如果是授權碼,則授權端點需要生成一個授權碼並保存起來,以便進行后續流程的處理,同時將這個授權碼和在1.2.1中的請求參數state一起拼接到1.2.1中的請求參數redirect_uri后面,一起重定向至客戶端提供的地址,這樣客戶端就能拿到對應的授權請求的授權碼進行令牌換取的操作。
如果是令牌,則直接返回一個令牌返回給客戶端(后面介紹)
二、令牌端點
在請求授權中,授權服務器已經將用戶重定向到客戶端提供的地址,並且攜帶了授權碼,客戶端拿到授權碼並完成自己的state校驗之后,就向授權服務器的令牌端點請求令牌token,該請求為POST類型,且屬於后端信道通信,實在客戶端和授權服務器之間進行的,不需要瀏覽器參與。
請求如下:
curl --location --request POST 'http://tokenendpoint.com/token' \ --header 'Content-Type: application/json' \ --header 'Authorization: basic encodeCredentials(clientId,clientSecret)' \ --data-raw '{ "grantType":"authorization_code", "code":"56abcgrh", "redirect_uri":"http://client.com/callback1" }'
2.1 頒發令牌
令牌端點接收到上面的請求之后,需要做兩件事:對客戶端進行身份認證和處理授權許可請求
2.1.1 對客戶端進行身份認證
- 解析請求HEADER,獲取clientId和clientSecret。傳遞客戶端的方式有兩種:使用HTTP基本認證方式和表單參數方式,為了遵循良好的服務端編程原則,支持多種輸入類型,允許客戶端按照自己的方式傳遞憑據。上面的請求示例中使用的是HTTP基本方式,將clientId和clientSecret用冒號分割之后進行BASE64編碼。
- 驗證接受到的clientId與clientSecret是否與客戶端注冊時的數據一致
2.1.2 處理授權許可請求
當令牌端點完成身份認證之后(驗證過程中令牌端點會向授權端點請求客戶端的注冊信息),就要對請求體中的參數進行下面的校驗:
- 首先檢查grantType參數,以保證客戶端請求的許可類型是授權服務器所支持的
- 然后檢查授權碼,如果是authorization_code類型,則要獲取授權碼code,並查詢該code是否存在,如果找不到,則返回錯誤信息;如果能找到,則要確定它是否是為該客戶端頒發的。(注意:授權碼是一次性有效,只要使用過了就會將其移除,當客戶端請求的授權碼與客戶端本身的信息不一致時,說明該授權碼已經泄露,同樣需要被丟棄)
- 當授權碼驗證通過之后,令牌端點需要生成一個訪問令牌accessToken,並將其保存起來以便后續使用。(注意:令牌內容可以具有內部結構,比如JWT或SAML斷言,這些令牌可以被簽名、加密,或者既被簽名又被加密,而且在使用時仍然對客戶端不透明,客戶端只要能拿到並用之,無需知道其內容)
完成上面的處理之后,令牌端點就可以將生成的訪問令牌以JSON的形式返回給客戶端,一般情況下為了防止令牌泄露,最好給該令牌設置一個比較短的有效期,即指定過期時間,響應格式如下:
{ "accessToken": "68bjh4543", "tokenType": "Bearer ", "expiration": "2021-11-07 9:33:00", "expiresInSeconds": "3600", "scope": "write read", "state": "teststate" }
- accessToken:訪問令牌,用來獲取受保護的資源
- tokenType:令牌使用的方式,該響應體中是Bearer 的方式,使用時將HTTP HEADER Authorization的值設為【Bearer 68bjh4543】即可
- expiration:過期時間
- expiresInSecond:在這個值之后過期(單位:秒)
- scope:該token適用的權限范圍
- state:客戶端的state,這些都是在之前的請求中,將狀態值state和授權碼code一起保存在授權服務器,在這一步中透傳給客戶端,方便客戶端校驗是否為同一個請求對應的token
至此,授權服務器就完成了對一次令牌請求的基本處理,但是訪問令牌是有有效期的,意味着當它過期之后,如果用戶還要繼續授權客戶端請求資源,就要重新走一遍授權流程,這對用戶來說是非常糟糕的體驗,於是OAuth提供了刷新令牌的機制來解決這種尷尬。
2.2 刷新令牌
刷新令牌是當訪問令牌過期之后,用戶無感知從授權服務器請求一個新的訪問令牌的機制。刷新令牌和訪問令牌都是在第一次獲取訪問令牌時,由令牌端點一起生成並返回給客戶端的,而且它也有一個有效期,只不過這個有效期會比較長,可能是一年或者更長,當過了這個有效期之后,再請求訪問令牌,就需要重新走完整的授權流程了,但是這個在用戶體驗上是完全可以接受的,包含刷新令牌時令牌端點的返回結構如下:
{
"accessToken": "68bjh4543",
"tokenType": "Bearer ",
"expiration": "2021-11-07 9:33:00",
"expiresInSeconds": 3600,
"scope": "write read",
"state": "teststate",
"refreshToken": "68bjh4543fdsfsfrrer",
"refreshTokenExpiration": "2022-11-07 9:33:00",
"refreshTokenExpiresInSeconds": 86765
}
在2.1.2的基礎上增加刷新令牌的值、過期時間和過期時長
- refreshToken:刷新token的值,這個值的長度一般比訪問令牌長,與訪問令牌進行視覺上的區分
- refreshTokenExpiration:刷新令牌的過期時間
- refreshTokenExpiresInSeconds: 在這個值之后過期(單位:秒)
既然支持刷新令牌,令牌端點當然還要支持對刷新令牌請求的處理,一般客戶端的刷新令牌請求為下面的格式
curl --location --request GET 'http://tokenendpoint.com/refreshToken?refreshToken=68bjh4543fdsfsfrrer&grant_type=refresh_token' \ --header 'Content-Type: application/json' \ --header 'Authorization: basic BASE64(clientId:clientSecret)'
刷新token的請求可以新增方法,也可以在之前的/token中實現,增加一種認證類型為refreshToken即可
- 參數grantType:刷新令牌請求中的該值為refresh_token
- 參數refreshToken:為頒發訪問令牌時返回的refreshToken的值
- header Authorization:與請求訪問令牌時的header一致
當授權端點接受到上面的請求之后,需要做如下的處理:
- 首先,檢查該訪問令牌是否存在,如果存在,還要檢查該刷新令牌是否是頒發給該客戶端的,如果這項校驗不通過,則認為該刷新令牌已經泄露,需要直接刪除
- 當所有的檢查都通過之后,可以基於該刷新令牌生成一個新的訪問令牌,存儲並返回給客戶端。當刷新令牌被使用之后,令牌端點可以決定是否將該刷新令牌刪除並頒發新的刷新令牌給客戶端。
至此,OAuth2授權碼模式中,授權服務器必須要完成的流程都已介紹完畢,下面介紹一些附加的功能。
2.3 授權范圍的支持
OAuth 2.0中一個很重要的機制就是權限范圍,權限范圍標識與特定授權相關聯的訪問權限的子集。當客戶端的請求中包含權限范圍時,授權服務器需要做如下處理:
- 首先,校驗注冊請求中scope。在1.1中客戶端注冊時,提供了授權范圍,當授權服務器接收到該參數后,不能直接將其保存,而是需要檢查客戶端請求的權限范圍是否是受保護資源所能提供服務的子集,甚至還要限制客戶端只能申請特定的權限范圍
- 然后,在客戶端向授權服務器請求授權時,需要確保客戶端請求的權限范圍沒有超出被允許的范圍
- 其次,當所有校驗通過之后,在頒發授權碼時需要將這些權限范圍與生產的授權碼保存在一起,以便在令牌端點收到請求時能查詢權限范圍
- 最后,在頒發令牌時,令牌端點需要將令牌所綁定的權限范圍告訴客戶端
可以在刷新令牌請求中指定一組權限范圍,並應用於新的訪問令牌,這樣客戶端就能使用刷新令牌請求新的訪問令牌時,新訪問令牌的權限小於其被許可的權限范圍,遵循了最小權限安全原則。
三、總結
OAuth 授權服務器無疑是OAuth系統中最復雜的部分:
- 處理前端信道和后端信道的不同請求
- 授權碼許可流程需要在多個步驟中維護數據狀態,最終才得以生成令牌
- 授權服務器上存在很多可能被攻擊的漏洞,每一處都需要進行適當的防護
- 刷新令牌隨訪問令牌一起頒發,可以避免在無用戶參與場景下(如定時任務)用於生成新的訪問令牌
- 全新范圍用於限制訪問令牌的權限
至此,授權服務器的介紹基本完成,下一篇我將詳細介紹另外兩個服務:受保護資源服務和客戶端服務。