為什么是 JWT Bearer
ASP.NET Core 在 Microsoft.AspNetCore.Authentication 下實現了一系列認證, 包含 Cookie
, JwtBearer
, OAuth
, OpenIdConnect
等,
-
Cookie 認證是一種比較常用本地認證方式, 它由瀏覽器自動保存並在發送請求時自動附加到請求頭中, 更適用於 MVC 等純網頁系統的本地認證.
-
OAuth & OpenID Connect 通常用於運程認證, 創建一個統一的認證中心, 來統一配置和處理對於其他資源和服務的用戶認證及授權.
-
JwtBearer 認證中, 客戶端通常將 JWT(一種Token) 通過 HTTP 的 Authorization header 發送給服務端, 服務端進行驗證. 可以方便的用於 WebAPI 框架下的本地認證.
當然, 也可以完全自己實現一個WebAPI下基於Token的本地認證, 比如自定義Token的格式, 自己寫頒發和驗證Token的代碼等. 這樣的話通用性並不好, 而且也需要花費更多精力來封裝代碼以及處理細節.
什么是 JWT
JWT (JSON Web Token) 是一種基於JSON的、用於在網絡上聲明某種主張的令牌(token)。
作為一個開放的標准(RFC 7519),定義了一種簡潔的、自包含的方法,從而使通信雙方實現以JSON對象的形式安全的傳遞信息。
JWT通常由三部分組成: 頭信息(header), 消息體(payload)和簽名(signature)。
頭信息指定了該JWT使用的簽名算法:
header = {"alg": "HS256", "typ": "JWT"}
消息體包含了JWT的意圖:
payload = {"sub": "1234567890", "name": "John Doe", "iat": 1516239022}
未簽名的令牌由base64url編碼的頭信息和消息體拼接而成(使用"."分隔),簽名則通過私有的key計算而成:
key = "secretkey" unsignedToken = encodeBase64(header) + '.' + encodeBase64(payload) signature = HMAC-SHA256(key, unsignedToken)
最后在尾部拼接上base64url編碼的簽名(同樣使用"."分隔)就是JWT了:
token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature)
JWT常常被用作保護服務端的資源,客戶端通常將JWT通過HTTP的Authorization header發送給服務端,服務端使用自己保存的key計算、驗證簽名以判斷該JWT是否可信。
Authorization: Bearer <token>
JWT 的優缺點
相比於傳統的 cookie-session 認證機制,優點有:
-
更適用分布式和水平擴展
在cookie-session方案中,cookie內僅包含一個session標識符,而諸如用戶信息、授權列表等都保存在服務端的session中。如果把session中的認證信息都保存在JWT中,在服務端就沒有session存在的必要了。當服務端水平擴展的時候,就不用處理session復制(session replication)/ session黏連(sticky session)或是引入外部session存儲了。 -
適用於多客戶端(特別是移動端)的前后端解決方案
移動端使用的往往不是網頁技術,使用Cookie驗證並不是一個好主意,因為你得和Cookie容器打交道,而使用Bearer驗證則簡單的多。 -
無狀態化
JWT 是無狀態化的,更適用於 RESTful 風格的接口驗證。
它的缺點也很明顯:
-
更多的空間占用
JWT 由於Payload里面包含了附件信息,占用空間往往比SESSION ID大,在HTTP傳輸中會造成性能影響。所以在設計時候需要注意不要在JWT中存儲太多的claim,以避免發生巨大的,過度膨脹的請求。 -
無法作廢已頒布的令牌
所有的認證信息都在JWT中,由於在服務端沒有狀態,即使你知道了某個JWT被盜取了,你也沒有辦法將其作廢。在JWT過期之前(你絕對應該設置過期時間),你無能為力。
在 WebAPI 中使用 JWT 認證
-
定義配置類 JwtIssuerOptions.cs
在 Startup.cs 里面添加相關代碼:
讀取配置:
JwtBearer驗證:
創建一個控制器 AuthController.cs,用來提供簽發 Token 的 API
為需要保護的API添加 [Authorize]
特性
-
使用 Swagger UI 或者 PostMan 等工具測試
獲取Token:
curl -X POST "http://localhost:5000/api/Auth/Login" -H "accept: application/json" -H "Content-Type: application/json-patch+json" -d "{ \"userName\": \"Paul\", \"password\": \"Paul123\"}"
返回值:
"{\r\n \"auth_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJQYXVsIiwianRpIjoiM2I1YzEyMzMtZTI1YS00ZWU5LWJkNjYtY2Y0NjU2YWMzM2QzIiwiaWF0IjoxNTQ0NTg5ODY5LCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiUGF1bCIsImlkIjoiZDM3ZjI3Y2UtODc4MC00NDI1LTkxMzUtYjY4OGE3NmM0YzBmIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjpbImFkbWluaXN0cmF0b3IiLCJhcGlfYWNjZXNzIl0sIm5iZiI6MTU0NDU4OTg2OCwiZXhwIjoxNTQ0NTk3MDY4LCJpc3MiOiJTZWN1cml0eURlbW8uQXV0aGVudGljYXRpb24uSldUIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwLyJ9.UAWLYQ5lA6xWofWIjGsPGWtAMHEtqZSfrfVaBui2mKI\",\r\n \"expires_in\": 7200,\r\n \"token_type\": \"Bearer\"\r\n}"
在 https://jwt.io/ 上解析 Token 如下:
{ "sub": "Paul", "jti": "3b5c1233-e25a-4ee9-bd66-cf4656ac33d3", "iat": 1544589869, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Paul", "id": "d37f27ce-8780-4425-9135-b688a76c4c0f", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": ["administrator","api_access"], "nbf": 1544589868, "exp": 1544597068, "iss": "SecurityDemo.Authentication.JWT", "aud": "http://localhost:5000/"}
使用 Token 訪問受保護的 API
curl -X GET "http://localhost:5000/api/Values" -H "accept: text/plain" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJQYXVsIiwianRpIjoiM2I1YzEyMzMtZTI1YS00ZWU5LWJkNjYtY2Y0NjU2YWMzM2QzIiwiaWF0IjoxNTQ0NTg5ODY5LCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiUGF1bCIsImlkIjoiZDM3ZjI3Y2UtODc4MC00NDI1LTkxMzUtYjY4OGE3NmM0YzBmIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjpbImFkbWluaXN0cmF0b3IiLCJhcGlfYWNjZXNzIl0sIm5iZiI6MTU0NDU4OTg2OCwiZXhwIjoxNTQ0NTk3MDY4LCJpc3MiOiJTZWN1cml0eURlbW8uQXV0aGVudGljYXRpb24uSldUIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwLyJ9.UAWLYQ5lA6xWofWIjGsPGWtAMHEtqZSfrfVaBui2mKI"
刷新 Token
因為JWT在服務端是沒有狀態的, 無論用戶注銷, 修改密碼還是Token被盜取, 你都無法將其作廢. 所以給JWT設置有效期並且盡量短是很有必要的. 但我們不可能讓用戶每次Token過期后都重新輸入一次用戶名和密碼為了生成新的Token. 最好是有種方式在用戶無感知的情況下完成Token刷新. 所以這里引入了Refresh Token.
修改 JwtFactory 中的 GenerateEncodedToken 方法, 新加一個參數 refreshToken, 並在包含在 response 里和 auth_token 一起返回.
修改 AuthController 中的 Login Action, 在每次客戶端請求 JWT Token 的時候, 同時生成一個 GUID 的 refreshToken. 這個 refreshToken 需要保存在數據庫或者緩存里. 這里方便演示放入了 MemoryCache 里面. 緩存的過期時間要比JWT Token的過期時間稍微長一點.
添加一個RefreshToken的接口, 接收參數 refresh_token, 然后檢查 refresh_token 的有效性, 如果有效生成一個新的 auth_token 和 refresh_token 並返回. 同時需要刪除掉原來 refresh_token 的緩存.
這里只是簡單的利用緩存的過期時間和auth_token的過期時間相近從而默認 refresh_token 是有效的, 精確期間需要把對應的 auth_token過期時間一起放入緩存, 在刷新Token的時候驗證這個時間.
-
測試
獲取Token:
curl -X POST "http://localhost:5000/api/Auth/Login" -H "accept: application/json" -H "Content-Type: application/json-patch+json" -d "{ \"userName\": \"Paul\", \"password\": \"Paul123\"}"
返回值:
"{\r\n \"auth_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJQYXVsIiwianRpIjoiNzA5Y2VkNjEtNWQ2ZS00N2RlLTg4NjctNzVjZGM0N2U0MWZiIiwiaWF0IjoxNTQ0NjgxOTA0LCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiUGF1bCIsImlkIjoiZmE3NjMxYzEtMzk0NS00MzUwLThjM2YtOWYxZDRhODU0MDFhIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjpbImFkbWluaXN0cmF0b3IiLCJhcGlfYWNjZXNzIl0sIm5iZiI6MTU0NDY4MTkwMywiZXhwIjoxNTQ0NjgyNTAzLCJpc3MiOiJTZWN1cml0eURlbW8uQXV0aGVudGljYXRpb24uSldUIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwLyJ9.tEJ-EuaI-BalW4lJEL8aeJzdryKfE440EC4cAVOW1PY\",\r\n \"refresh_token\": \"3093f839-fd3c-47a3-97a9-c0324e4e6b7e\",\r\n \"expires_in\": 600,\r\n \"token_type\": \"Bearer\"\r\n}"
請求RefreshToken:
curl -X POST "http://localhost:5000/api/Auth/RefreshToken" -H "accept: application/json" -H "Content-Type: application/json-patch+json" -d "{ \"userName\": \"Paul\", \"refreshToken\": \"3093f839-fd3c-47a3-97a9-c0324e4e6b7e\"}"
返回新的 auth_token 和 refresh_token
"{\r\n \"auth_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJQYXVsIiwianRpIjoiMjI2M2Y4NGEtZjlmMC00ZTM1LWI1YTUtMDdhYmI0M2UzMWQ5IiwiaWF0IjoxNTQ0NjgxOTIxLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiUGF1bCIsImlkIjoiZmE3NjMxYzEtMzk0NS00MzUwLThjM2YtOWYxZDRhODU0MDFhIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjpbImFkbWluaXN0cmF0b3IiLCJhcGlfYWNjZXNzIl0sIm5iZiI6MTU0NDY4MTkyMSwiZXhwIjoxNTQ0NjgyNTIxLCJpc3MiOiJTZWN1cml0eURlbW8uQXV0aGVudGljYXRpb24uSldUIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwLyJ9.A1hXNVmkqD80GqfF69LwvarpNf5QedPvKFDcB5xA4Z0\",\r\n \"refresh_token\": \"b33de8ff-5213-4d37-be0b-7b561553e0f7\",\r\n \"expires_in\": 600,\r\n \"token_type\": \"Bearer\"\r\n}"
使用授權
在認證階段我們通過用戶令牌獲取到了用戶的Claims,而授權便是對這些Claims進行驗證, 比如是否擁有某種角色,年齡是否大於18歲(如果Claims里有年齡信息)等。
簡單授權
ASP.NET Core中使用Authorize
特性授權, 使用AllowAnonymous
特性跳過授權.
基於固定角色的授權
適用於系統中的角色是固定的,每種角色可以訪問的Controller和Action也是固定的情景.
基於策略的授權
在ASP.NET Core中,重新設計了一種更加靈活的授權方式:基於策略的授權, 它是授權的核心.
在使用基於策略的授權時,首先要定義授權策略,而授權策略本質上就是對Claims的一系列斷言。
基於角色的授權和基於Scheme的授權,只是一種語法上的便捷,最終都會生成授權策略。
自定義策略授權
基於策略的授權中有一個很重要的概念是Requirements
,每一個Requirement都代表一個授權條件。
Requirement需要繼承接口IAuthorizationRequirement。
在 ASP.NET Core 中已經內置了一些常用的實現:
-
AssertionRequirement :使用最原始的斷言形式來聲明授權策略。
-
DenyAnonymousAuthorizationRequirement :用於表示禁止匿名用戶訪問的授權策略,並在AuthorizationOptions中將其設置為默認策略。
-
ClaimsAuthorizationRequirement :用於表示判斷Cliams中是否包含預期的Claims的授權策略。
-
RolesAuthorizationRequirement :用於表示使用ClaimsPrincipal.IsInRole來判斷是否包含預期的Role的授權策略。
-
NameAuthorizationRequirement:用於表示使用ClaimsPrincipal.Identities.Name來判斷是否包含預期的Name的授權策略。
-
OperationAuthorizationRequirement:用於表示基於操作的授權策略。
除了OperationAuthorizationRequirement外,都有對應的快捷添加方法,比如RequireClaim
,RequireRole
,RequireUserName
等。
當內置的Requirement不能滿足需求時,可以定義自己的Requirement. 下面基於圖中所示的用戶-角色-功能權限設計來實現一個自定義的驗證策略。
-
添加一個靜態類 TestUsers 用於模擬用戶數據
這里只是模擬, 實際使用當中肯定是從數據庫取數據, 同時也應該有類似於User, Role, Function, UserRole, RoleFunction等幾張表保存這些數據.
創建類 UserService 用於獲取用戶已授權的功能列表.
創建 PermissionRequirement
創建 PermissionHandler
獲取當前的URL, 並去當前用戶已授權的URL List里查看. 如果匹配就驗證成功.
在Startup.cs 的 ConfigureServices 里面注冊 PermissionHandler 並添加 Policy.
添加測試代碼並測試
注意這里Controller, Action需要和用戶功能表里的URL一致
-
使用我們的模擬數據, 用戶 Paul 兩個Action GetAdminValue 和 GetGuestValue 都可以訪問; Young 只有權限訪問 GetGuestValue; 而 Roy 只可以訪問 GetAdminValue.
基於資源的授權
有些時候, 授權需要依賴於要訪問的資源, 比如:只允許作者自己編輯和刪除所寫的博客.
這種場景是無法通過Authorize特性來指定授權的, 因為授權過濾器會在MVC的模型綁定之前執行,無法確定所訪問的資源。此時,我們需要使用基於資源的授權。
在基於資源的授權中, 我們要判斷的是用戶是否具有針對該資源的某項操作, 而系統預置的OperationAuthorizationRequirement
就是用於這種場景中的.
-
在實際使用當中, 可以通過EF Core攔截或AOP來實現授權驗證與業務代碼的分離。
源代碼
github
參考
-
Overview of ASP.NET Core Security
-
AngularASPNETCore2WebApiAuth
-
ASP.NET Core 認證與授權[1]:初識認證
-
asp.net core策略授權
-
ASP.NET Core 使用 JWT 搭建分布式無狀態身份驗證系統
-
JWT權限驗證
-
Handle Refresh Token Using ASP.NET Core 2.0 And JSON Web Token