ASP.NET Core WebAPI中使用JWT Bearer認證和受權

爲何是 JWT Bearer

ASP.NET Core 在 Microsoft.AspNetCore.Authentication 下實現了一系列認證, 包含 CookieJwtBearerOAuthOpenIdConnect 等,nginx

  • Cookie 認證是一種比較經常使用本地認證方式, 它由瀏覽器自動保存並在發送請求時自動附加到請求頭中, 更適用於 MVC 等純網頁系統的本地認證.git

  • OAuth & OpenID Connect 一般用於運程認證, 建立一個統一的認證中心, 來統一配置和處理對於其餘資源和服務的用戶認證及受權.github

  • 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使用的簽名算法:json

header = {"alg": "HS256", "typ": "JWT"}

消息體包含了JWT的意圖:後端

payload = {"sub": "1234567890", "name": "John Doe", "iat": 1516239022}

未簽名的令牌由base64url編碼的頭信息和消息體拼接而成(使用"."分隔),簽名則經過私有的key計算而成:api

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 認證機制,優勢有:

  1. 更適用分佈式和水平擴展
    在cookie-session方案中,cookie內僅包含一個session標識符,而諸如用戶信息、受權列表等都保存在服務端的session中。若是把session中的認證信息都保存在JWT中,在服務端就沒有session存在的必要了。當服務端水平擴展的時候,就不用處理session複製(session replication)/ session黏連(sticky session)或是引入外部session存儲了。

  2. 適用於多客戶端(特別是移動端)的先後端解決方案
    移動端使用的每每不是網頁技術,使用Cookie驗證並非一個好主意,由於你得和Cookie容器打交道,而使用Bearer驗證則簡單的多。

  3. 無狀態化
    JWT 是無狀態化的,更適用於 RESTful 風格的接口驗證。

它的缺點也很明顯:

  1. 更多的空間佔用
    JWT 因爲Payload裏面包含了附件信息,佔用空間每每比SESSION ID大,在HTTP傳輸中會形成性能影響。因此在設計時候須要注意不要在JWT中存儲太多的claim,以免發生巨大的,過分膨脹的請求。

  2. 沒法做廢已頒佈的令牌
    全部的認證信息都在JWT中,因爲在服務端沒有狀態,即便你知道了某個JWT被盜取了,你也沒有辦法將其做廢。在JWT過時以前(你絕對應該設置過時時間),你無能爲力。

在 WebAPI 中使用 JWT 認證
  1. 定義配置類 JwtIssuerOptions.cs

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

在 Startup.cs 裏面添加相關代碼:

讀取配置:

640?wx_fmt=png

JwtBearer驗證:

640?wx_fmt=png

640?wx_fmt=png

建立一個控制器 AuthController.cs,用來提供簽發 Token 的 API

640?wx_fmt=png

爲須要保護的API添加 [Authorize] 特性

640?wx_fmt=png

  1. 使用 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 一塊兒返回.

640?wx_fmt=png

修改 AuthController 中的 Login Action, 在每次客戶端請求 JWT Token 的時候, 同時生成一個 GUID 的 refreshToken. 這個 refreshToken 須要保存在數據庫或者緩存裏. 這裏方便演示放入了 MemoryCache 裏面. 緩存的過時時間要比JWT Token的過時時間稍微長一點.

640?wx_fmt=png

添加一個RefreshToken的接口, 接收參數 refresh_token, 而後檢查 refresh_token 的有效性, 若是有效生成一個新的 auth_token 和 refresh_token 並返回. 同時須要刪除掉原來 refresh_token 的緩存.
這裏只是簡單的利用緩存的過時時間和auth_token的過時時間相近從而默認 refresh_token 是有效的, 精確期間須要把對應的 auth_token過時時間一塊兒放入緩存, 在刷新Token的時候驗證這個時間.

640?wx_fmt=png

  1. 測試

    獲取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特性跳過受權.

640?wx_fmt=png

基於固定角色的受權

適用於系統中的角色是固定的,每種角色能夠訪問的Controller和Action也是固定的情景.

640?wx_fmt=png

基於策略的受權

在ASP.NET Core中,從新設計了一種更加靈活的受權方式:基於策略的受權, 它是受權的核心.
在使用基於策略的受權時,首先要定義受權策略,而受權策略本質上就是對Claims的一系列斷言。
基於角色的受權和基於Scheme的受權,只是一種語法上的便捷,最終都會生成受權策略。

640?wx_fmt=png

自定義策略受權

基於策略的受權中有一個很重要的概念是Requirements,每個Requirement都表明一個受權條件。
Requirement須要繼承接口IAuthorizationRequirement。
在 ASP.NET Core 中已經內置了一些經常使用的實現:

  • AssertionRequirement :使用最原始的斷言形式來聲明受權策略。

  • DenyAnonymousAuthorizationRequirement :用於表示禁止匿名用戶訪問的受權策略,並在AuthorizationOptions中將其設置爲默認策略。

  • ClaimsAuthorizationRequirement :用於表示判斷Cliams中是否包含預期的Claims的受權策略。

  • RolesAuthorizationRequirement :用於表示使用ClaimsPrincipal.IsInRole來判斷是否包含預期的Role的受權策略。

  • NameAuthorizationRequirement:用於表示使用ClaimsPrincipal.Identities.Name來判斷是否包含預期的Name的受權策略。

  • OperationAuthorizationRequirement:用於表示基於操做的受權策略。

除了OperationAuthorizationRequirement外,都有對應的快捷添加方法,好比RequireClaimRequireRoleRequireUserName等。

當內置的Requirement不能知足需求時,能夠定義本身的Requirement. 下面基於圖中所示的用戶-角色-功能權限設計來實現一個自定義的驗證策略。
640?wx_fmt=png

  1. 添加一個靜態類 TestUsers 用於模擬用戶數據
    這裏只是模擬, 實際使用當中確定是從數據庫取數據, 同時也應該有相似於User, Role, Function, UserRole, RoleFunction等幾張表保存這些數據.

 

640?wx_fmt=png

建立類 UserService 用於獲取用戶已受權的功能列表.

640?wx_fmt=png

建立 PermissionRequirement

640?wx_fmt=png

建立 PermissionHandler
獲取當前的URL, 並去當前用戶已受權的URL List裏查看. 若是匹配就驗證成功.

640?wx_fmt=png

在Startup.cs 的 ConfigureServices 裏面註冊 PermissionHandler 並添加 Policy.

640?wx_fmt=png

添加測試代碼並測試
注意這裏Controller, Action須要和用戶功能表裏的URL一致

640?wx_fmt=png

  1. 使用咱們的模擬數據, 用戶 Paul 兩個Action GetAdminValue 和 GetGuestValue 均可以訪問; Young 只有權限訪問 GetGuestValue; 而 Roy 只能夠訪問 GetAdminValue.

基於資源的受權

有些時候, 受權須要依賴於要訪問的資源, 好比:只容許做者本身編輯和刪除所寫的博客.
這種場景是沒法經過Authorize特性來指定受權的, 由於受權過濾器會在MVC的模型綁定以前執行,沒法肯定所訪問的資源。此時,咱們須要使用基於資源的受權。
在基於資源的受權中, 咱們要判斷的是用戶是否具備針對該資源的某項操做, 而系統預置的OperationAuthorizationRequirement就是用於這種場景中的.

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

  1. 在實際使用當中, 能夠經過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

相關文章
相關標籤/搜索