ASP.NET Core 在 Microsoft.AspNetCore.Authentication 下實現了一系列認證, 包含 Cookie
, JwtBearer
, OAuth
, OpenIdConnect
等,nginx
Cookie 認證是一種比較經常使用本地認證方式, 它由瀏覽器自動保存並在發送請求時自動附加到請求頭中, 更適用於 MVC 等純網頁系統的本地認證.git
OAuth & OpenID Connect 一般用於運程認證, 建立一個統一的認證中心, 來統一配置和處理對於其餘資源和服務的用戶認證及受權.github
JwtBearer 認證中, 客戶端一般將 JWT(一種Token) 經過 HTTP 的 Authorization header 發送給服務端, 服務端進行驗證. 能夠方便的用於 WebAPI 框架下的本地認證.
固然, 也能夠徹底本身實現一個WebAPI下基於Token的本地認證, 好比自定義Token的格式, 本身寫頒發和驗證Token的代碼等. 這樣的話通用性並很差, 並且也須要花費更多精力來封裝代碼以及處理細節.算法
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 認證機制,優勢有:
更適用分佈式和水平擴展
在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過時以前(你絕對應該設置過時時間),你無能爲力。
定義配置類 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"
由於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