為什么是 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
public class JwtIssuerOptions { /// <summary> /// 4.1.1. "iss" (Issuer) Claim - The "iss" (issuer) claim identifies the principal that issued the JWT. /// </summary> public string Issuer { get; set; } /// <summary> /// 4.1.2. "sub" (Subject) Claim - The "sub" (subject) claim identifies the principal that is the subject of the JWT. /// </summary> public string Subject { get; set; } /// <summary> /// 4.1.3. "aud" (Audience) Claim - The "aud" (audience) claim identifies the recipients that the JWT is intended for. /// </summary> public string Audience { get; set; } /// <summary> /// 4.1.4. "exp" (Expiration Time) Claim - The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. /// </summary> public DateTime Expiration => IssuedAt.Add(ValidFor); /// <summary> /// 4.1.5. "nbf" (Not Before) Claim - The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. /// </summary> public DateTime NotBefore => DateTime.UtcNow; /// <summary> /// 4.1.6. "iat" (Issued At) Claim - The "iat" (issued at) claim identifies the time at which the JWT was issued. /// </summary> public DateTime IssuedAt => DateTime.UtcNow; /// <summary> /// Set the timespan the token will be valid for (default is 120 min) /// </summary> public TimeSpan ValidFor { get; set; } = TimeSpan.FromMinutes(120); /// <summary> /// "jti" (JWT ID) Claim (default ID is a GUID) /// </summary> public Func<Task<string>> JtiGenerator => () => Task.FromResult(Guid.NewGuid().ToString()); /// <summary> /// The signing key to use when generating tokens. /// </summary> public SigningCredentials SigningCredentials { get; set; } } -
定義的幫助類 JwtFactory.cs, 主要是用於生成Token
public interface IJwtFactory { Task<string> GenerateEncodedToken(string userName, ClaimsIdentity identity); ClaimsIdentity GenerateClaimsIdentity(User user); } public class JwtFactory : IJwtFactory { private readonly JwtIssuerOptions _jwtOptions; public JwtFactory(IOptions<JwtIssuerOptions> jwtOptions) { _jwtOptions = jwtOptions.Value; ThrowIfInvalidOptions(_jwtOptions); } public async Task<string> GenerateEncodedToken(string userName, ClaimsIdentity identity) { var claims = new List<Claim> { new Claim(JwtRegisteredClaimNames.Sub, userName), new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()), new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64), identity.FindFirst(ClaimTypes.Name), identity.FindFirst("id") }; claims.AddRange(identity.FindAll(ClaimTypes.Role)); // Create the JWT security token and encode it. var jwt = new JwtSecurityToken( issuer: _jwtOptions.Issuer, audience: _jwtOptions.Audience, claims: claims, notBefore: _jwtOptions.NotBefore, expires: _jwtOptions.Expiration, signingCredentials: _jwtOptions.SigningCredentials); var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); var response = new { auth_token = encodedJwt, expires_in = (int)_jwtOptions.ValidFor.TotalSeconds, token_type = "Bearer" }; return JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented }); } public ClaimsIdentity GenerateClaimsIdentity(User user) { var claimsIdentity = new ClaimsIdentity(new GenericIdentity(user.UserName, "Token")); claimsIdentity.AddClaim(new Claim("id", user.Id.ToString())); claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, user.UserName)); foreach (var role in user.Roles) { claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role)); } return claimsIdentity; } /// <returns>Date converted to seconds since Unix epoch (Jan 1, 1970, midnight UTC).</returns> private static long ToUnixEpochDate(DateTime date) => (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)) .TotalSeconds); private static void ThrowIfInvalidOptions(JwtIssuerOptions options) { if (options == null) throw new ArgumentNullException(nameof(options)); if (options.ValidFor <= TimeSpan.Zero) { throw new ArgumentException("Must be a non-zero TimeSpan.", nameof(JwtIssuerOptions.ValidFor)); } if (options.SigningCredentials == null) { throw new ArgumentNullException(nameof(JwtIssuerOptions.SigningCredentials)); } if (options.JtiGenerator == null) { throw new ArgumentNullException(nameof(JwtIssuerOptions.JtiGenerator)); } } } -
在 Startup.cs 里面添加相關代碼:
讀取配置:
var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions)); services.Configure<JwtIssuerOptions>(options => { options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)]; options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)]; options.SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256); });JwtBearer驗證:
public class Startup { private const string SecretKey = "iNivDmHLpUA223sqsfhqGbMRdRj1PVkH"; // todo: get this from somewhere secure private readonly SymmetricSecurityKey _signingKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(SecretKey)); public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IJwtFactory, JwtFactory>(); services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(configureOptions => { configureOptions.ClaimsIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)]; configureOptions.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)], ValidateAudience = true, ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)], ValidateIssuerSigningKey = true, IssuerSigningKey = _signingKey, RequireExpirationTime = false, ValidateLifetime = true, ClockSkew = TimeSpan.Zero }; configureOptions.SaveToken = true; }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseAuthentication(); app.UseMvc(); } }Swagger相關:
services.AddSwaggerGen(options => { var security = new Dictionary<string, IEnumerable<string>> { { "Bearer", new string[] { } }, }; options.AddSecurityRequirement(security); options.AddSecurityDefinition("Bearer", new Swashbuckle.AspNetCore.Swagger.ApiKeyScheme { Description = "Format: Bearer {auth_token}", Name = "Authorization", In = "header" }); }); -
創建一個控制器 AuthController.cs,用來提供簽發 Token 的 API
[Route("api/[controller]")] [ApiController] public class AuthController : ControllerBase { private readonly IJwtFactory _jwtFactory; private readonly JwtIssuerOptions _jwtOptions; public AuthController(IJwtFactory jwtFactory, IOptions<JwtIssuerOptions> jwtOptions) { _jwtFactory = jwtFactory; _jwtOptions = jwtOptions.Value; } /// <summary> /// Log in /// </summary> /// <param name="request"></param> /// <returns></returns> [HttpPost("[action]")] public async Task<IActionResult> Login([FromBody]LoginRequest request) { var users = TestUsers.Users.Where(r => r.UserName.Equals(request.UserName)); if (users.Count() <= 0) { ModelState.AddModelError("login_failure", "Invalid username."); return BadRequest(ModelState); } var user = users.First(); if (!request.Password.Equals(user.Password)) { ModelState.AddModelError("login_failure", "Invalid password."); return BadRequest(ModelState); } var claimsIdentity = _jwtFactory.GenerateClaimsIdentity(user.UserName, user.Id.ToString()); var token = await _jwtFactory.GenerateEncodedToken(user.UserName, claimsIdentity); return new OkObjectResult(token); } /// <summary> /// Get User Info /// </summary> /// <returns></returns> [HttpGet("[action]")] [Authorize] public IActionResult GetUserInfo() { var claimsIdentity = User.Identity as ClaimsIdentity; return Ok(claimsIdentity.Claims.ToList().Select(r=> new { r.Type, r.Value})); } } -
為需要保護的API添加
[Authorize]特性[Route("api/[controller]")] [ApiController] [Authorize] public class ValuesController : ControllerBase { // GET api/values [HttpGet] public ActionResult<IEnumerable<string>> Get() { return new string[] { "value1", "value2" }; } } -
使用 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 一起返回.
public async Task<string> GenerateEncodedToken(string userName, string refreshToken, ClaimsIdentity identity) { var response = new { auth_token = encodedJwt, refresh_token = refreshToken, expires_in = (int)_jwtOptions.ValidFor.TotalSeconds, token_type = "Bearer" }; return JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented }); } -
修改 AuthController 中的 Login Action, 在每次客戶端請求 JWT Token 的時候, 同時生成一個 GUID 的 refreshToken. 這個 refreshToken 需要保存在數據庫或者緩存里. 這里方便演示放入了 MemoryCache 里面. 緩存的過期時間要比JWT Token的過期時間稍微長一點.
string refreshToken = Guid.NewGuid().ToString(); var claimsIdentity = _jwtFactory.GenerateClaimsIdentity(user); _cache.Set(refreshToken, user.UserName, TimeSpan.FromMinutes(11)); var token = await _jwtFactory.GenerateEncodedToken(user.UserName, refreshToken, claimsIdentity); return new OkObjectResult(token); -
添加一個RefreshToken的接口, 接收參數 refresh_token, 然后檢查 refresh_token 的有效性, 如果有效生成一個新的 auth_token 和 refresh_token 並返回. 同時需要刪除掉原來 refresh_token 的緩存.
這里只是簡單的利用緩存的過期時間和auth_token的過期時間相近從而默認 refresh_token 是有效的, 精確期間需要把對應的 auth_token過期時間一起放入緩存, 在刷新Token的時候驗證這個時間./// <summary> /// RefreshToken /// </summary> /// <param name="request"></param> /// <returns></returns> [HttpPost("[action]")] public async Task<IActionResult> RefreshToken([FromBody]RefreshTokenRequest request) { string userName; if (!_cache.TryGetValue(request.RefreshToken, out userName)) { ModelState.AddModelError("refreshtoken_failure", "Invalid refreshtoken."); return BadRequest(ModelState); } if (!request.UserName.Equals(userName)) { ModelState.AddModelError("refreshtoken_failure", "Invalid userName."); return BadRequest(ModelState); } var user = _userService.GetUserByName(request.UserName); string newRefreshToken = Guid.NewGuid().ToString(); var claimsIdentity = _jwtFactory.GenerateClaimsIdentity(user); _cache.Remove(request.RefreshToken); _cache.Set(newRefreshToken, user.UserName, TimeSpan.FromMinutes(11)); var token = await _jwtFactory.GenerateEncodedToken(user.UserName, newRefreshToken, claimsIdentity); return new OkObjectResult(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特性跳過授權.
//所有用戶都可以Login, 但只有授權的用戶才可以Logout.
public class AccountController : Controller
{
[AllowAnonymous]
public ActionResult Login()
{
}
[Authorize]
public ActionResult Logout()
{
}
}
基於固定角色的授權
適用於系統中的角色是固定的,每種角色可以訪問的Controller和Action也是固定的情景.
//可以指定多個角色, 以逗號分隔
[Authorize(Roles = "Administrator")]
public class AdministrationController : Controller
{
}
基於策略的授權
在ASP.NET Core中,重新設計了一種更加靈活的授權方式:基於策略的授權, 它是授權的核心.
在使用基於策略的授權時,首先要定義授權策略,而授權策略本質上就是對Claims的一系列斷言。
基於角色的授權和基於Scheme的授權,只是一種語法上的便捷,最終都會生成授權策略。
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization(options =>
{
//options.AddPolicy("Administrator", policy => policy.RequireRole("administrator"));
options.AddPolicy("Administrator", policy => policy.RequireClaim(ClaimTypes.Role, "administrator"));
//options.AddPolicy("Founders", policy => policy.RequireClaim("EmployeeNumber", "1", "2", "3", "4", "5"));
});
}
[Authorize(Policy = "Administrator")]
public ActionResult<IEnumerable<string>> GetValueByAdminPolicy()
{
return new string[] { "GetValueByAdminPolicy" };
}
自定義策略授權
基於策略的授權中有一個很重要的概念是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等幾張表保存這些數據.public static class TestUsers { public static List<User> Users = new List<User> { new User{ Id = Guid.NewGuid(), UserName = "Paul", Password = "Paul123", Roles = new List<string>{ "administrator", "api_access" }, Urls = new List<string>{ "/api/values/getadminvalue", "/api/values/getguestvalue" }}, new User{ Id = Guid.NewGuid(), UserName = "Young", Password = "Young123", Roles = new List<string>{ "api_access" }, Urls = new List<string>{ "/api/values/getguestvalue" }}, new User{ Id = Guid.NewGuid(), UserName = "Roy", Password = "Roy123", Roles = new List<string>{ "administrator" }, Urls = new List<string>{ "/api/values/getadminvalue" }}, }; } public class User { public Guid Id { get; set; } public string UserName { get; set; } public string Password { get; set; } public List<string> Roles { get; set; } public List<string> Urls { get; set; } } -
創建類 UserService 用於獲取用戶已授權的功能列表.
public interface IUserService { List<string> GetFunctionsByUserId(Guid id); } public class UserService : IUserService { public List<string> GetFunctionsByUserId(Guid id) { var user = TestUsers.Users.SingleOrDefault(r => r.Id.Equals(id)); return user?.Urls; } } -
創建 PermissionRequirement
public class PermissionRequirement : IAuthorizationRequirement { } -
創建 PermissionHandler
獲取當前的URL, 並去當前用戶已授權的URL List里查看. 如果匹配就驗證成功.public class PermissionHandler : AuthorizationHandler<PermissionRequirement> { private readonly IUserService _userService; public PermissionHandler(IUserService userService) { _userService = userService; } protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) { var httpContext = (context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).HttpContext; var isAuthenticated = httpContext.User.Identity.IsAuthenticated; if (isAuthenticated) { Guid userId; if (!Guid.TryParse(httpContext.User.Claims.SingleOrDefault(s => s.Type == "id").Value, out userId)) { return Task.CompletedTask; } var functions = _userService.GetFunctionsByUserId(userId); var requestUrl = httpContext.Request.Path.Value.ToLower(); if (functions != null && functions.Count > 0 && functions.Contains(requestUrl)) { context.Succeed(requirement); } } return Task.CompletedTask; } } -
在Startup.cs 的 ConfigureServices 里面注冊 PermissionHandler 並添加 Policy.
services.AddAuthorization(options => { options.AddPolicy("Permission", policy => policy.Requirements.Add(new PermissionRequirement())); }); services.AddSingleton<IAuthorizationHandler, PermissionHandler>(); -
添加測試代碼並測試
注意這里Controller, Action需要和用戶功能表里的URL一致[Route("api/[controller]")] [ApiController] public class ValuesController : ControllerBase { [HttpGet("[action]")] [Authorize(Policy = "Permission")] public ActionResult<IEnumerable<string>> GetAdminValue() { return new string[] { "use Policy = Permission" }; } [HttpGet("[action]")] [Authorize(Policy = "Permission")] public ActionResult<IEnumerable<string>> GetGuestValue() { return new string[] { "use Policy = Permission" }; } }使用我們的模擬數據, 用戶 Paul 兩個Action GetAdminValue 和 GetGuestValue 都可以訪問; Young 只有權限訪問 GetGuestValue; 而 Roy 只可以訪問 GetAdminValue.
基於資源的授權
有些時候, 授權需要依賴於要訪問的資源, 比如:只允許作者自己編輯和刪除所寫的博客.
這種場景是無法通過Authorize特性來指定授權的, 因為授權過濾器會在MVC的模型綁定之前執行,無法確定所訪問的資源。此時,我們需要使用基於資源的授權。
在基於資源的授權中, 我們要判斷的是用戶是否具有針對該資源的某項操作, 而系統預置的OperationAuthorizationRequirement就是用於這種場景中的.
public class OperationAuthorizationRequirement : IAuthorizationRequirement
{
public string Name { get; set; }
}
-
定義一些常用操作, 方便業務調用.
public static class ResourceOperations { public static OperationAuthorizationRequirement Create = new OperationAuthorizationRequirement { Name = "Create" }; public static OperationAuthorizationRequirement Read = new OperationAuthorizationRequirement { Name = "Read" }; public static OperationAuthorizationRequirement Update = new OperationAuthorizationRequirement { Name = "Update" }; public static OperationAuthorizationRequirement Delete = new OperationAuthorizationRequirement { Name = "Delete" }; } -
我們是根據資源的創建者來判斷用戶是否具有操作權限,因此,定義一個資源實體的接口, 包含一個字段 Creator
public interface IResourceWithCreator { string Creator { get; set; } } -
定義測試數據用於模擬
public static class TestBlogs { public static List<Blog> Blogs = new List<Blog> { new Blog{ Id = Guid.Parse("CA4A3FC9-42CA-47F4-B651-36A863023E75"), Name = "Paul_Blog_1", BlogUrl = "blogs/paul/1", Creator = "Paul" }, new Blog{ Id = Guid.Parse("9C03EDA8-FBCD-4C33-B5C8-E4DFC40258D7"), Name = "Paul_Blog_2", BlogUrl = "blogs/paul/2", Creator = "Paul" }, new Blog{ Id = Guid.Parse("E05E3625-1885-49A5-87D0-54F7EAF90C88"), Name = "Young_Blog_1", BlogUrl = "blogs/young/1", Creator = "Young" }, new Blog{ Id = Guid.Parse("E97D5DF4-AE50-4258-84F8-0B3052EB2CB8"), Name = "Roy_Blog_1", BlogUrl = "blogs/roy/1", Creator = "Roy" }, }; } public class Blog : IResourceWithCreator { public Guid Id { get; set; } public string Name { get; set; } public string BlogUrl { get; set; } public string Creator { get; set; } } -
定義 ResourceAuthorizationHandler
允許任何人創建或查看資源, 有只有資源的創建者才可以修改和刪除資源.public class ResourceAuthorizationHandler : AuthorizationHandler<OperationAuthorizationRequirement, IResourceWithCreator> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, IResourceWithCreator resource) { if (requirement == ResourceOperations.Create || requirement == ResourceOperations.Read) { context.Succeed(requirement); } else { if (context.User.Identity.Name == resource.Creator) { context.Succeed(requirement); } } return Task.CompletedTask; } } -
在ConfigureServices里注冊Handler.
services.AddSingleton<IAuthorizationHandler, ResourceAuthorizationHandler>(); -
添加控制器並引入IAuthorizationService進行驗證
[Authorize] public class BlogsController : ControllerBase { private readonly IAuthorizationService _authorizationService; private readonly IBlogService _blogService; public BlogsController(IAuthorizationService authorizationService, IBlogService blogService) { _authorizationService = authorizationService; _blogService = blogService; } [HttpGet("{id}", Name = "Get")] public async Task<ActionResult<Blog>> Get(Guid id) { var blog = _blogService.GetBlogById(id); if ((await _authorizationService.AuthorizeAsync(User, blog, ResourceOperations.Read)).Succeeded) { return Ok(blog); } else { return Forbid(); } } [HttpPut("{id}")] public async Task<ActionResult> Put(Guid id, [FromBody] Blog newBlog) { var blog = _blogService.GetBlogById(id); if ((await _authorizationService.AuthorizeAsync(User, blog, ResourceOperations.Update)).Succeeded) { bool result = _blogService.Update(newBlog); return Ok(result); } else { return Forbid(); } } }在實際使用當中, 可以通過EF Core攔截或AOP來實現授權驗證與業務代碼的分離。
