OAuth2.0 協議定義了授權詳細流程,並最終以 token 的形式作爲用戶授權的憑證下發給客戶端,客戶端後續可以帶着 token 去請求資源服務器,獲取 token 權限範圍內的用戶資源。
對於 token 的描述,OAuth 2.0 協議只是一筆帶過的說它是一個字符串,用於表示特定的權限、生命週期等,但是卻沒有明確闡述 token 的生成策略,以及如何去驗證一個 token。RFC6749 對於 access token 的描述:
The client obtains an access token – a string denoting a specific scope, lifetime, and other access attributes.
協議不去詳細闡述 token 的生成和驗證過程,個人覺得是因爲這一塊各個業務都有自己的特點,無法完全做到抽象,並且在這一塊去做詳細的規定,其意義並不大。Token 本質上就是對用戶授權這一操作在時間和權限範圍兩個維度上的一個表徵,協議可以對 token 的傳遞和基本驗證做相應規定,但是具體的一個 token 包含哪些元素,採用什麼樣的生成算法還是需要由自己去把握。
本文主要講解自己對於 token 生成的一些思考,以及介紹兩種類型:BEARER 和 MAC。
一. TOKEN 的基本構成
Token 表徵了用戶授權這一操作,授權服務器通過下發 token 來給客戶端頒發獲取用戶受保護資源的資格,且不會因此而泄露用戶的登錄憑證信息。Token 對於客戶端應該是非透明的,客戶端只知道這是一個字符串,能夠用它來獲取用戶的受保護資源,對於字符串內部所含的信息應該無從知曉,也不能通過其它方法去解密其中的信息。所以 token 應該是一類對稱加密得到的字符串,並且只有授權服務器持有對稱密鑰,用於對生成的 token 進行加密和驗證。
對於構成token的元素,各個業務都有自己的需求,不過仍然存在一些基本通用的元素,比如:
- clientId:客戶端 ID,當前 token 隸屬的客戶端
- userId:用戶的 ID,表示當前 token 來自哪個用戶授權
- scope: 權限範圍,該 token 允許換取的用戶受保護資源範圍
- issueTime: 下發時間,用於控制 token 的生命週期
- tokenType: token 的類型,不同類型可能會採用不同的驗證措施
以上是個人根據經驗總結的一些基礎的 token 組成元素,具體業務還可以根據具體的需求添加一些其他的元素。
二. Bearer Type Access Token
BEARER 類型的 token 是在 RFC6750 中定義的一種 token 類型,OAuth 2.0 協議 RFC6749 對其也有所提及,算是對 RFC6749 的一個補充。BEARER 類型 token 是建立在 HTTP/1.1 版本之上的 token 類型,需要 TLS(Transport Layer Security) 提供安全支持,該協議主要規定了BEARER類型token的客戶端請求和服務端驗證的具體細節。
2.1 客戶端請求
客戶端在攜帶token請求用戶的受保護資源時,需要保證token的安全性,以防止token被竊取或篡改,從而損害用戶數據安全。BEARER類型token定義了三種token傳遞策略,客戶端在傳遞token時必須使用其中的一種,且最多一種。
2.1.1 放在Authorization
請求首部
Authorization首部說明
Authorization首部是由客戶端發送,以向服務器迴應自己的身份驗證信息,客戶端在收到服務器的401 Authentication Required響應之後,需要在請求中包含該首部。
基本用法:Authorization:
在傳輸時,Authorization
首部的 authentication-scheme
需要設置爲 Bearer
,請求示例:
GET /resource HTTP/1.1 Host: server.example.com Authorization: Bearer mF_9.B5f-4.1JqM |
2.1.2 放在請求實體中
Token需放置在 access_token
參數後面,且 Content-Type
需要設置爲 application/x-www-form-urlencoded
,請求示例如下:
POST /resource HTTP/1.1 Host: server.example.com Content-Type: application/x-www-form-urlencoded access_token=mF_9.B5f-4.1JqM |
協議推薦使用第一種方式,對於該請求方式,必須在滿足如下條件時才允許使用:
- The HTTP request entity-header includes the “Content-Type” header field set to “application/x-www-form-urlencoded”.
- The entity-body follows the encoding requirements of the “application/x-www-form-urlencoded” content-type as defined by HTML 4.01.
- The HTTP request entity-body is single-part.
- The content to be encoded in the entity-body MUST consist entirely of ASCII characters.
- The HTTP request method is one for which the request-body has defined semantics. In particular, this means that the “GET” method MUST NOT be used.
2.1.3 放在URI請求參數中
該方式通過在請求URl後面添加 access_token
參數來傳遞token,請求示例如下:
GET /resource?access_token=mF_9.B5f-4.1JqM HTTP/1.1 Host: server.example.com |
客戶端在請求時需要設置 Cache-Control: no-store
,服務端在成功響應時也需要設置 Cache-Control: private
。
由於很多服務都會以日誌方式去記錄用戶的請求,此類方式存在較大的安全隱患,所以一般不推薦使用,除非前兩種方案均不可用。
2.2 服務端驗證
如果服務端拒絕客戶端的訪問請求,則需要在響應中添加 WWW-Authenticate
首部,響應示例如下:
HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer realm="example" |
WWW-Authenticate首部說明
WWW-Authenticate首部用於401 Unauthorized響應,用於向客戶端發送一個質詢認證方案。
基本用法:WWW-Authenticate:
這裏的響應,其中 auth-scheme
必須設置爲 Bearer
,如果客戶端攜帶了無效的token,那麼按照上一篇《OAuth 2.0 協議原理與實現:協議原理》 講解的,OAuth 2.0 協議要求錯誤響應中必須攜帶 error
字段,並選擇性攜帶 error_description
和 error_uri
,具體釋義請參考上一篇,響應示例如下:
HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token expired" |
三. MAC Type Access Token
前面介紹了BEARER類型的token,RFC6750明確說明該類型token需要TLS(Transport Layer Security)提供安全支持。雖然現今大部分站點都已經或正在由HTTP向HTTPS遷移,但是仍然會有站點繼續在使用HTTP,在這類站點中BEARER類型的token存在安全隱患,這個時候MAC類型的token正是用武之地,MAC類型的token設計的主要目的就是爲了應對不可靠的網絡環境。
MAC類型相對於BEARER類型對於用戶資源請求的區別在於,BEARER類型只需要攜帶授權服務器下發的token即可,而對於MAC類型來說,除了攜帶授權服務器下發的token,客戶端還要攜帶時間戳,nonce,以及在客戶端計算得到的mac值等信息,並通過這些額外的信息來保證傳輸的可靠性。
3.1 下發 MAC 類型令牌
OAuth2.0協議在規定下發accessToken時,包含 access_token
,token_type
,expires_in
、refresh_token
,以及 scope
字段,其中部分字段可選,具體參見上一篇《OAuth 2.0 協議原理與實現:協議原理》,示例如下:
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", "example_parameter":"example_value" } |
響應字段是可擴展的,對於MAC類型token則增加了 mac_key
和 mac_algorithm
兩個字段,mac_key
是一個客戶端和服務端共享的對稱密鑰,mac_algorithm
則指明瞭加密算法(比如hmac-sha-1,hmac-sha-256),示例響應如下:
HTTP/1.1 200 OK Content-Type: application/json Cache-Control: no-store Pragma: no-cache { "access_token":"SlAV32hkKG", "token_type":"mac", "expires_in":3600, "refresh_token":"8xLOxBtZp8", "mac_key":"adijq39jdlaska9asud", "mac_algorithm":"hmac-sha-256" } |
3.2 構造 MAC 類型請求
一些開放API接口可能會強制要求以MAC類型令牌來請求,這個時候就需要在客戶端構造合法的請求,一個標準的請求示例如下:
GET /resource/1?b=1&a=2 HTTP/1.1 Host: example.com Authorization: MAC id="h480djs93hd8", ts="1336363200", nonce="dj83hs9s", mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM=" |
請求參數說明:
參數名 | 必須 | 描述信息 |
---|---|---|
id | 必須 | 訪問令牌 |
ts | 必須 | 時間戳 |
nonce | 必須 | 客戶端生成的字符串,對於相同token和timespan的請求nonce必須相同 |
ext | 可選 | 擴展信息 |
mac | 必須 | 根據MAC key和MAC algorithm計算出來的值 |
通過添加 id、ts、nonce、mac
字段到 Authorization
請求首部以發起對用戶資源的請求,這裏的 id
就是授權服務器下發的 accessToken;ts
則是時間戳,由客戶端生成,以秒爲單位;nonce
是客戶端生成的一個字符串形式的簽名,是對 ts 和 id 兩個維度的唯一、可重複性標識;而 mac
則是整個客戶端構造最核心和複雜的部分,可以看做是對本次請求參數的一個簽名,1.2.3 小節專門講解。此外客戶端還以用 ext
字段來攜帶一些擴展數據。
3.3 mac 值算法
mac 值可以看作是對本次請求參數的一個簽名,通過對請求數據進行本地加密計算得到,用於防止請求過程中參數被更改。服務器端收到請求之後,會以相同的算法和密鑰重新計算一遍 mac值,並與客戶端傳遞過來的作比較,如果不一致則拒絕該請求。因爲密鑰僅保存在客戶端和服務端本地,所以無需擔心mac值被更改或僞造,從而確保在沒有TLS保證的環境下可靠傳輸,實際上這裏可以看做是 MAC 類型請求自己實現了一遍 TLS。
mac值對於相同的請求參數必須是一致和可再計算的,對於參與計算元素的選擇,協議選取了如下元素:
- The timestamp value calculated for the request.
- The nonce value generated for the request.
- The HTTP request method in upper case. For example: HEAD, GET, POST, etc.
- The HTTP request-URI as defined by RFC2616 section 5.1.2.
- The hostname included in the HTTP request using the Host request header field in lower case.
- The port as included in the HTTP request using the Host request header field. If the header field does not include a port, the default value for the scheme MUST be used (e.g. 80 for HTTP and 443 for HTTPS).
- The value of the ext Authorization request header field attribute if one was included in the request, otherwise, an empty string.
通過對這些元素按照順序組織,並以換行符 \n
作分隔(最後一行也需要包含一個 \n
),利用 mac_algorithm
指定的算法和 mac_key
指定的密鑰對組織好的數據進行加密計算得到 mac 值。
計算示例:
假設有一個請求:
POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q HTTP/1.1 Host: example.com Hello World! |
其中 ts=264095:7d8f3e4a
,nonce=7d8f3e4a
,ext=a,b,c
。
對該請求按照之前的說明進行組織,以\n
分隔得到:
264095\n 7d8f3e4a\n POST\n /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q\n example.com\n 80\n a,b,c\n |
其中 \n
僅僅是爲了展示,實際中以 ASCII 碼 %x0A
的意義表示,不要忘了最後一行的 \n
。假設授權服務器指定的 mac_algorithm
爲 hmac-sha-1,令 text
表示上面的字符串,那麼最後的 mac 值得計算方式如下:
mac = hmac-sha-1(mac_key, text)
3.4 服務端驗證
服務器端在收到客戶端的請求之後,需要做如下驗證:
- 重新計算mac值,並與客戶端傳遞的值進行比較
- 確保(timestam, nonce, token)三個維度之前沒有被請求過,以防止重放攻擊
- 驗證scope,以及token
如果服務端拒絕客戶端的請求,則需要指定 WWW- Authenticate
響應首部,例如客戶端攜帶了無效的授權信息,則服務器響應示例如下:
HTTP/1.1 401 Unauthorized WWW-Authenticate: MAC error="The MAC credentials expired" |
四. 本篇小結
本篇主要介紹了兩種 token 類型,基本可以覆蓋實際應用中的各種場景。Token 是對用戶授權操作的一類憑證,一旦下發到客戶端,其安全性就需要客戶端去保證,爲了儘量在保護用戶數據和提升用戶體驗上尋找一個平衡點,token 的生命週期不應該設置的太短或太長。
本篇和上一篇《OAuth 2.0 協議原理與實現:協議原理》 介紹了 OAuth 2.0 協議涉及到的理論知識,相關實現可以參考考 oauth4j。