JWT(json web token)是一種基於json的身份驗證機制,流程以下:git
經過登陸,來獲取Token,再在以後每次請求的Header中追加Authorization爲Token的憑據,服務端驗證經過便可能獲取想要訪問的資源。關於JWT的技術,可參考網絡上文章,這裏不做詳細說明,github
這篇博文,主要說明在asp.net core 2.0中,基於jwt的web api的權限設置,即在asp.net core中怎麼用JWT,再次就是不一樣用戶或角色由於權限問題,即便援用Token,也不能訪問不應訪問的資源。web
基本思路是咱們自定義一個策略,來驗證用戶,和驗證用戶受權,PermissionRequirement是驗證傳輸受權的參數。在Startup的ConfigureServices注入驗證(Authentication),受權(Authorization),和JWT(JwtBearer)數據庫
自定義策略:json
已封閉成AuthorizeRolicy.JWT nuget包,併發布到nuget上:api
https://www.nuget.org/packages/AuthorizePolicy.JWT/網絡
源碼以下:併發
JwtToken.csasp.net
/// <summary> /// 獲取基於JWT的Token /// </summary> /// <param name="username"></param> /// <returns></returns> public static dynamic BuildJwtToken(Claim[] claims, PermissionRequirement permissionRequirement) { var now = DateTime.UtcNow; var jwt = new JwtSecurityToken( issuer: permissionRequirement.Issuer, audience: permissionRequirement.Audience, claims: claims, notBefore: now, expires: now.Add(permissionRequirement.Expiration), signingCredentials: permissionRequirement.SigningCredentials ); var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); var response = new { Status = true, access_token = encodedJwt, expires_in = permissionRequirement.Expiration.TotalMilliseconds, token_type = "Bearer" }; return response; }
Permission.csasync
/// <summary> /// 用戶或角色或其餘憑據實體 /// </summary> public class Permission { /// <summary> /// 用戶或角色或其餘憑據名稱 /// </summary> public virtual string Name { get; set; } /// <summary> /// 請求Url /// </summary> public virtual string Url { get; set; } }
PermissionRequirement.cs
1 using Microsoft.AspNetCore.Authorization; 2 using Microsoft.IdentityModel.Tokens; 3 using System; 4 using System.Collections.Generic; 5 6 namespace AuthorizePolicy.JWT 7 { 8 /// <summary> 9 /// 必要參數類 10 /// </summary> 11 public class PermissionRequirement : IAuthorizationRequirement 12 { 13 /// <summary> 14 /// 用戶權限集合 15 /// </summary> 16 public List<Permission> Permissions { get; private set; } 17 /// <summary> 18 /// 無權限action 19 /// </summary> 20 public string DeniedAction { get; set; } 21 22 /// <summary> 23 /// 認證受權類型 24 /// </summary> 25 public string ClaimType { internal get; set; } 26 /// <summary> 27 /// 請求路徑 28 /// </summary> 29 public string LoginPath { get; set; } = "/Api/Login"; 30 /// <summary> 31 /// 發行人 32 /// </summary> 33 public string Issuer { get; set; } 34 /// <summary> 35 /// 訂閱人 36 /// </summary> 37 public string Audience { get; set; } 38 /// <summary> 39 /// 過時時間 40 /// </summary> 41 public TimeSpan Expiration { get; set; } 42 /// <summary> 43 /// 簽名驗證 44 /// </summary> 45 public SigningCredentials SigningCredentials { get; set; } 46 47 /// <summary> 48 /// 構造 49 /// </summary> 50 /// <param name="deniedAction">無權限action</param> 51 /// <param name="userPermissions">用戶權限集合</param> 52 53 /// <summary> 54 /// 構造 55 /// </summary> 56 /// <param name="deniedAction">拒約請求的url</param> 57 /// <param name="permissions">權限集合</param> 58 /// <param name="claimType">聲明類型</param> 59 /// <param name="issuer">發行人</param> 60 /// <param name="audience">訂閱人</param> 61 /// <param name="signingCredentials">簽名驗證明體</param> 62 public PermissionRequirement(string deniedAction, List<Permission> permissions, string claimType, string issuer, string audience, SigningCredentials signingCredentials, TimeSpan expiration) 63 { 64 ClaimType = claimType; 65 DeniedAction = deniedAction; 66 Permissions = permissions; 67 Issuer = issuer; 68 Audience = audience; 69 Expiration = expiration; 70 SigningCredentials = signingCredentials; 71 } 72 } 73 }
自定義策略類PermissionHandler.cs
/// <summary> /// 權限受權Handler /// </summary> public class PermissionHandler : AuthorizationHandler<PermissionRequirement> { /// <summary> /// 驗證方案提供對象 /// </summary> public IAuthenticationSchemeProvider Schemes { get; set; } /// <summary> /// 自定義策略參數 /// </summary> public PermissionRequirement Requirement { get; set; } /// <summary> /// 構造 /// </summary> /// <param name="schemes"></param> public PermissionHandler(IAuthenticationSchemeProvider schemes) { Schemes = schemes; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) { ////賦值用戶權限 Requirement = requirement; //從AuthorizationHandlerContext轉成HttpContext,以便取出表求信息 var httpContext = (context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).HttpContext; //請求Url var questUrl = httpContext.Request.Path.Value.ToLower(); //判斷請求是否中止 var handlers = httpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>(); foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync()) { var handler = await handlers.GetHandlerAsync(httpContext, scheme.Name) as IAuthenticationRequestHandler; if (handler != null && await handler.HandleRequestAsync()) { context.Fail(); return; } } //判斷請求是否擁有憑據,即有沒有登陸 var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync(); if (defaultAuthenticate != null) { var result = await httpContext.AuthenticateAsync(defaultAuthenticate.Name); //result?.Principal不爲空即登陸成功 if (result?.Principal != null) { httpContext.User = result.Principal; //權限中是否存在請求的url if (Requirement.Permissions.GroupBy(g => g.Url).Where(w => w.Key.ToLower() == questUrl).Count() > 0) { var name = httpContext.User.Claims.SingleOrDefault(s => s.Type == requirement.ClaimType).Value; //驗證權限 if (Requirement.Permissions.Where(w => w.Name == name && w.Url.ToLower() == questUrl).Count() <= 0) { //無權限跳轉到拒絕頁面 httpContext.Response.Redirect(requirement.DeniedAction); } } //判斷過時時間 if (DateTime.Parse(httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.Expiration).Value) >= DateTime.Now) { context.Succeed(requirement); } else { context.Fail(); } return; } } //判斷沒有登陸時,是否訪問登陸的url,而且是Post請求,並助是form表單提交類型,不然爲失敗 if (!questUrl.Equals(Requirement.LoginPath.ToLower(), StringComparison.Ordinal) && (!httpContext.Request.Method.Equals("POST") || !httpContext.Request.HasFormContentType)) { context.Fail(); return; } context.Succeed(requirement); } }
新建asp.net core 2.0的web api項目,並在項目添加AuthorizePolicy.JWT如圖
先設置配置文件,用戶能夠定義密匙和發生人,訂閱人
"Audience": {
"Secret": "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890",
"Issuer": "gsw",
"Audience": "everone"
}
在ConfigureServices中注入驗證(Authentication),受權(Authorization),和JWT(JwtBearer)
Startup.cs
1 public void ConfigureServices(IServiceCollection services) 2 { 3 var urls = "http://localhost:39287/"; 4 services.AddCors(options => 5 options.AddPolicy("MyDomain", 6 builder => builder.WithOrigins(urls).AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin().AllowCredentials())); 7 8 //讀取配置文件 9 var audienceConfig = Configuration.GetSection("Audience"); 10 var symmetricKeyAsBase64 = audienceConfig["Secret"]; 11 var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64); 12 var signingKey = new SymmetricSecurityKey(keyByteArray); 13 var tokenValidationParameters = new TokenValidationParameters 14 { 15 ValidateIssuerSigningKey = true, 16 IssuerSigningKey = signingKey, 17 ValidateIssuer = true, 18 ValidIssuer = audienceConfig["Issuer"],//發行人 19 ValidateAudience = true, 20 ValidAudience = audienceConfig["Audience"],//訂閱人 21 ValidateLifetime = true, 22 ClockSkew = TimeSpan.Zero, 23 RequireExpirationTime = true, 24 25 }; 26 var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); 27 //這個集合模擬用戶權限表,可從數據庫中查詢出來 28 var permission = new List<Permission> { 29 new Permission { Url="/", Name="admin"}, 30 new Permission { Url="/api/values", Name="admin"}, 31 new Permission { Url="/", Name="system"}, 32 new Permission { Url="/api/values1", Name="system"} 33 }; 34 //若是第三個參數,是ClaimTypes.Role,上面集合的每一個元素的Name爲角色名稱,若是ClaimTypes.Name,即上面集合的每一個元素的Name爲用戶名 35 var permissionRequirement = new PermissionRequirement( 36 "/api/denied", permission, 37 ClaimTypes.Role, 38 audienceConfig["Issuer"], 39 audienceConfig["Audience"], 40 signingCredentials, 41 expiration: TimeSpan.FromSeconds(10)//設置Token過時時間 42 ); 43 services.AddAuthorization(options => 44 { 45 46 options.AddPolicy("Permission", 47 policy => policy.Requirements.Add(permissionRequirement)); 48 49 }).AddAuthentication(options => 50 { 51 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 52 options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 53 }) 54 .AddJwtBearer(o => 55 { 56 //不使用https 57 o.RequireHttpsMetadata = false; 58 o.TokenValidationParameters = tokenValidationParameters; 59 o.Events = new JwtBearerEvents 60 { 61 OnTokenValidated = context => 62 { 63 if (context.Request.Path.Value.ToString() == "/api/logout") 64 { 65 var token = ((context as TokenValidatedContext).SecurityToken as JwtSecurityToken).RawData; 66 } 67 return Task.CompletedTask; 68 } 69 }; 70 }); 71 //注入受權Handler 72 services.AddSingleton<IAuthorizationHandler, PermissionHandler>(); 73 services.AddSingleton(permissionRequirement); 74 services.AddMvc(); 75 }
在須要授的Controller上添加受權特性
[Authorize("Permission")]
PermissionController類有兩個方法,一個是登陸,驗證用戶名和密碼是否正確,若是正確就發放Token,若是失敗,驗證失敗,別一個成功登後的無權限導航action。
1 [Authorize("Permission")] 2 [EnableCors("MyDomain")] 3 public class PermissionController : Controller 4 { 5 /// <summary> 6 /// 自定義策略參數 7 /// </summary> 8 PermissionRequirement _requirement; 9 public PermissionController(PermissionRequirement requirement) 10 { 11 _requirement = requirement; 12 } 13 [AllowAnonymous] 14 [HttpPost("/api/login")] 15 public IActionResult Login(string username, string password, string role) 16 { 17 var isValidated = username == "gsw" && password == "111111"; 18 if (!isValidated) 19 { 20 return new JsonResult(new 21 { 22 Status = false, 23 Message = "認證失敗" 24 }); 25 } 26 else 27 { 28 //若是是基於用戶的受權策略,這裏要添加用戶;若是是基於角色的受權策略,這裏要添加角色 29 var claims = new Claim[] { new Claim(ClaimTypes.Name, username), new Claim(ClaimTypes.Role, role), new Claim(ClaimTypes.Expiration ,DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString())}; 30 //用戶標識 31 var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme); 32 identity.AddClaims(claims); 33 34 var token = JwtToken.BuildJwtToken(claims, _requirement); 35 return new JsonResult(token); 36 } 37 } 38 39 [HttpPost("/api/logout")] 40 public IActionResult Logout() 41 { 42 return Ok(); 43 } 44 45 [AllowAnonymous] 46 [HttpGet("/api/denied")] 47 public IActionResult Denied() 48 { 49 return new JsonResult(new 50 { 51 Status = false, 52 Message = "你無權限訪問" 53 }); 54 } 55 }
下面定義一個控制檯(.NetFramewrok)程序,用RestSharp來訪問咱們定義的web api,其中1爲admin角色登陸,2爲system角色登陸,3爲錯誤用戶密碼登陸,4是一個查詢功能,在startup.cs中,admin角色是具備查詢/api/values的權限的,因此用admin登陸是能正常訪問的,用system登陸,能成功登陸,但沒有權限訪問/api/values,用戶名密碼錯誤,訪問/aip/values,直接是沒有受權的
class Program { /// <summary> /// 訪問Url /// </summary> static string _url = "http://localhost:39286"; static void Main(string[] args) { dynamic token = null; while (true) { Console.WriteLine("一、登陸【admin】 二、登陸【system】 三、登陸【錯誤用戶名密碼】 四、查詢數據 "); var mark = Console.ReadLine(); var stopwatch = new Stopwatch(); stopwatch.Start(); switch (mark) { case "1": token = AdminLogin(); break; case "2": token = SystemLogin(); break; case "3": token = NullLogin(); break; case "4": AdminInvock(token); break; } stopwatch.Stop(); TimeSpan timespan = stopwatch.Elapsed; Console.WriteLine($"間隔時間:{timespan.TotalSeconds}"); } } static dynamic NullLogin() { var loginClient = new RestClient(_url); var loginRequest = new RestRequest("/api/login", Method.POST); loginRequest.AddParameter("username", "gswaa"); loginRequest.AddParameter("password", "111111"); //或用用戶名密碼查詢對應角色 loginRequest.AddParameter("role", "system"); IRestResponse loginResponse = loginClient.Execute(loginRequest); var loginContent = loginResponse.Content; Console.WriteLine(loginContent); return Newtonsoft.Json.JsonConvert.DeserializeObject(loginContent); } static dynamic SystemLogin() { var loginClient = new RestClient(_url); var loginRequest = new RestRequest("/api/login", Method.POST); loginRequest.AddParameter("username", "gsw"); loginRequest.AddParameter("password", "111111"); //或用用戶名密碼查詢對應角色 loginRequest.AddParameter("role", "system"); IRestResponse loginResponse = loginClient.Execute(loginRequest); var loginContent = loginResponse.Content; Console.WriteLine(loginContent); return Newtonsoft.Json.JsonConvert.DeserializeObject(loginContent); } static dynamic AdminLogin() { var loginClient = new RestClient(_url); var loginRequest = new RestRequest("/api/login", Method.POST); loginRequest.AddParameter("username", "gsw"); loginRequest.AddParameter("password", "111111"); //或用用戶名密碼查詢對應角色 loginRequest.AddParameter("role", "admin"); IRestResponse loginResponse = loginClient.Execute(loginRequest); var loginContent = loginResponse.Content; Console.WriteLine(loginContent); return Newtonsoft.Json.JsonConvert.DeserializeObject(loginContent); } static void AdminInvock(dynamic token) { var client = new RestClient(_url); //這裏要在獲取的令牌字符串前加Bearer string tk = "Bearer " + Convert.ToString(token?.access_token); client.AddDefaultHeader("Authorization", tk); var request = new RestRequest("/api/values", Method.GET); IRestResponse response = client.Execute(request); var content = response.Content; Console.WriteLine($"狀態:{response.StatusCode} 返回結果:{content}"); } }
運行結果:
源碼:https://github.com/axzxs2001/AuthorizePolicy.JWT