一、實現
1、Permission文件
代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Blog.Jwt { /// <summary> /// 用戶或角色或其他憑據實體 /// </summary> public class Permission { /// <summary> /// 用戶或角色或其他憑據名稱 /// </summary> public virtual string RoleName{ get; set; } /// <summary> /// 請求Url /// </summary> public virtual string Url{ get; set; } } }
如圖所示:
2、PermissionHandler.cs
代碼如下:
using Blog.Jwt; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Security.Claims; using System.Threading.Tasks; namespace Blog.Jwt { /// <summary> /// 權限授權Handler /// </summary> public class PermissionHandler : AuthorizationHandler<PermissionRequirement>//我們自定義用戶屬性,並自己驗證 { /// <summary> /// 驗證方案提供對象 /// </summary> public IAuthenticationSchemeProvider Schemes { get; set; } private readonly IHttpContextAccessor _accessor; /// <summary> /// 構造函數注入 /// </summary> /// <param name="schemes"></param> /// <param name="accessor"></param> public PermissionHandler(IAuthenticationSchemeProvider schemes, IHttpContextAccessor accessor) { Schemes = schemes; _accessor = accessor; } /// <summary> /// 摘要:根據特定需求決定是否允許授權 特定就是add參數,但是未包含在創建token屬性內 /// /// 二點注意: /// 一、沒必要重寫加參數,我們可以從上下文中獲取到token(老張完全多此一句) 我們完全可以從token獲取用戶信息並對比 requirement完全多余 /// 二、HandleRequirementAsync用了 這個方法 沒有辦法HttpContext.Response.WriteAsync,不然會報錯報錯{StatusCode cannot be set because the response has already started.}因為響應已經開始 /// 所以我們盡量不在此方法里面寫,我們只需要知道時間過期則請求頭部 context.Response.Headers.Add("Token-Expired", "true"); 即可,其他一律為未授權 /// 其實我很想 提示 未授權和認證上失敗的 區分下 /// /// 第二次回顧 /// 1、具體提示信息既然無法返回 body內容,就返回狀態碼(這個是可行的),有個全局攔截來根據返回自定義狀態碼處理,狀態碼狀態信息是對外一致(也是全局攔截的統一狀態碼信息的好處) /// 2、我之前認為說為什么add參數呢,甚至你不用系統提供的auth,直接寫個中間件一樣能實現。微軟這么設計,是為了更復雜的抽象層而已。 /// /// 我還是選擇不繼承 AuthorizationHandler ,不添加add 參數,直接用 /// /// reqirement 用來定義授權認證的參數 拿他來驗證的 /// /// </summary> /// <param name="context"></param> /// <param name="requirement"></param> /// <returns></returns> protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) { #region 想返回具體提示 /* if (!_accessor.HttpContext.Response.HasStarted)//先判斷context.Response.HasStarted { var results = JsonConvert.SerializeObject(new ApiResultModels { Success = false, Message = "測試返回結果", Code = "406" }); await _accessor.HttpContext.Response.WriteAsync(results); //寫入后報錯{StatusCode cannot be set because the response has already started.}因為響應已經開始 if (true) { context.Fail(); return CompletedTask; } } //想返回想要的返回具體結果比如 過期時間提示token已過期、Audience頒布者則提示token無效! 只能通過設置狀態碼解決 _accessor.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;//403 context.Fail(); if (true) */ #endregion ////賦值用戶權限 //從AuthorizationHandlerContext轉成HttpContext,以便取出表求信息 //var httpContext = (context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).HttpContext; var httpContext = _accessor.HttpContext; /*//動態獲取權限項目 if (!requirement.Permissions.Any()) { var data = await _role.GetPermissions(); var list = (from item in data where item.IsDeleted = false orderby item.Id select new Permission { Url = "", Name = "" }).ToList(); requirement.Permissions = list; }*/ //請求Url var questUrl = httpContext.Request.Path.Value.ToLower(); #region 判斷請求是否停止 //判斷請求是否停止 var handlers = httpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();// IAuthenticationSchemeProvider 用來提供對Scheme的注冊和查詢 foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())//按請求處理的優先級順序返回方案(Scheme)。 { //來獲取指定的Scheme的Hander var handler = await handlers.GetHandlerAsync(httpContext, scheme.Name) as IAuthenticationRequestHandler; if (handler != null && await handler.HandleRequestAsync()) //handler.HandleRequestAsync 如果請求處理應停止,則返回true { context.Fail(); return; } } //后台 怎么根據 HttpContext判斷請求是否停止?(保留疑問) #endregion //判斷請求是否擁有憑據,即有沒有登錄 GetDefaultAuthenticateSchemeAsync 是獲取默認的授權方案信息 var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();//public IAuthenticationSchemeProvider Schemes 驗證方案提供對象 1、獲取認證方案實例對象不為null if (defaultAuthenticate == null) throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found."); else { //擴展方法 public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme) 2、context上下文通過身份驗證方案的名稱,獲取認證方案信息 //判斷使用是否授權 var result = await httpContext.AuthenticateAsync(defaultAuthenticate.Name);//身份認證 defaultAuthenticate.Name==Bearer(header里面有Bearer) //result?.Principal不為空即登錄成功 if (result?.Principal != null) { httpContext.User = result.Principal;//3、token字符串 var strToken = result.Properties.Items.FirstOrDefault().Value;//token字符串 //權限中是否存在請求的url if (requirement.Permissions.GroupBy(g => g.Url).Where(w => w.Key?.ToLower() == questUrl).Count() > 0) { // 獲取當前用戶的角色信息 var currentUserRoles = (from item in httpContext.User.Claims where item.Type == requirement.ClaimType select item.Value).ToList(); //驗證權限 失敗則 if (currentUserRoles.Count <= 0 || requirement.Permissions.Where(w => currentUserRoles.Contains(w.RoleName) && w.Url.ToLower() == questUrl).Count() <= 0) { // 可以在這里設置跳轉頁面,不過還是會訪問當前接口地址的 //httpContext.Response.Redirect(requirement.DeniedAction); _accessor.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;//401 未授權 context.Fail(); //if (true) return; } } else { //context.Fail(); //return; _accessor.HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;//404 此url不存在 context.Fail(); //if (true) return; } //判斷過期時間 if ((httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.Expiration)?.Value) != null && DateTime.Parse(httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.Expiration)?.Value) >= DateTime.UtcNow) { context.Succeed(requirement); } else { context.Fail(); return; } return; } else { _accessor.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;//403 禁止訪問 context.Fail(); //if (true) 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); } } }
3、PermissionRequirement.cs如下
代碼如下:
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Blog.Jwt { /// <summary> /// https://blog.csdn.net/qq_25086397/article/details/103765090 /// 必要參數類 繼承 IAuthorizationRequirement 方便從PermissionRequirement將屬性取出賦值道PermissionRequirement類成員上 /// </summary> public class PermissionRequirement : IAuthorizationRequirement { /// <summary> /// 用戶權限集合 /// </summary> public List<Permission> Permissions { get; private set; } /// <summary> /// 無權限action /// </summary> public string DeniedAction { get; set; } /// <summary> /// 認證授權類型 /// </summary> public string ClaimType { internal get; set; } /// <summary> /// 請求路徑 /// </summary> public string LoginPath { get; set; } = "/Api/Login"; /// <summary> /// 發行人 /// </summary> public string Issuer { get; set; } /// <summary> /// 訂閱人 /// </summary> public string Audience { get; set; } /// <summary> /// 過期時間 /// </summary> //public TimeSpan Expiration { get; set; } = TimeSpan.FromMinutes(5000); public TimeSpan Expiration { get; set; } = TimeSpan.FromMinutes(1); /// <summary> /// 簽名驗證 /// </summary> public SigningCredentials SigningCredentials { get; set; } /// <summary> /// 構造函數 /// </summary> /// <param name="deniedAction">拒約請求的url</param> /// <param name="permissions">權限集合</param> /// <param name="claimType">聲明類型</param> /// <param name="issuer">發行人</param> /// <param name="audience">訂閱人</param> /// <param name="signingCredentials">簽名驗證實體</param> public PermissionRequirement(string deniedAction, List<Permission> permissions, string claimType, string issuer, string audience, SigningCredentials signingCredentials) { ClaimType = claimType; DeniedAction = deniedAction; Permissions = permissions; Issuer = issuer; Audience = audience; SigningCredentials = signingCredentials; //DI容器,注冊到容器內,此時無New實例化,僅構造函數用到的時候才會new,內存才會有該實例對象 /*//沒有權限則跳轉到這個路由 DeniedAction = new PathString("/api/nopermission"); //用戶有權限訪問的路由配置,當然可以從數據庫獲取 Permissions = new List<Permission> { new Permission { Url="/api/value3", Name="admin"}, };*/ } } }
JwtMiddlewareExtensions.cs
using Common; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text; using System.Threading.Tasks; namespace Blog.Jwt { //1、自定義授權策略驗證 2、頒發者token和刷新token驗證 public static class JwtMiddlewareExtensions { public static IServiceCollection AddJwtMiddleware(this IServiceCollection services, IConfiguration Configuration) { #region 注冊Jwt驗證 //在ConfigureServices中注入驗證(Authentication),授權(Authorization),和JWT(JwtBearer) var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:SecretKey"])); //↓Token 的信息配置 var tokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true,////是否驗證SecurityKey IssuerSigningKey = signingKey,//拿到SecurityKey ValidateIssuer = true,//是否驗證Issuer ValidIssuer = Configuration["Jwt:Issuer"],//Issuer,這兩項和前面簽發jwt的設置一致 ValidateAudience = false,//是否驗證Audience //為了驗證token和刷新token兩套Audience,后續處理 ValidAudience = Configuration["Jwt:Audience"],//Audience,這兩項和前面簽發jwt的設置一致 ValidateLifetime = true,//是否驗證超時 當設置exp和nbf時有效 同時啟用ClockSkew ClockSkew = TimeSpan.Zero //這里采用動態驗證的方式,在重新登陸時,刷新token,舊token就強制失效了 //https://www.cnblogs.com/7tiny/p/11019698.html /*,AudienceValidator = (m, n, z) => { return m != null && m.FirstOrDefault().Equals(Const.ValidAudience); },*/ }; // var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); //這個集合模擬用戶權限表,可從數據庫中查詢出來 // 如果要數據庫動態綁定,這里先留個空,后邊處理器里動態賦值 var permission = new List<Permission> { new Permission { Url="/", RoleName="Admin"}, new Permission { Url="/api/home/values", RoleName="Admin"}, new Permission { Url="/", RoleName="SysTem"}, new Permission { Url="/api/home/values1", RoleName="Admin"}, new Permission { Url="/api/home/values2", RoleName="Admin"}, new Permission { Url="/api/home/values1", RoleName="SysTem"}, new Permission { Url="/api/home/values2", RoleName="SysTem"} }; //var permission = new List<Permission>(); //New一個PermissionRequirement實體類,是為了從擴展額外參數,先注入,然后在PermissionHandler驗證使用這些參數,這個參數我們自己定義的,方便驗證的時候使用 //如果第三個參數,是ClaimTypes.Role,上面集合的每個元素的Name為角色名稱,如果ClaimTypes.Name,即上面集合的每個元素的Name為用戶名 var permissionRequirement = new PermissionRequirement("/api/denied", permission, ClaimTypes.Role, Configuration["Jwt:Issuer"], Configuration["Jwt:Audience"], signingCredentials); services.AddAuthorization(options => //↓導入角色身份授權策略 { options.AddPolicy("Permission", policy => policy.Requirements.Add(permissionRequirement)); }).AddAuthentication(options => //↓身份認證類型 { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(o =>//↓Jwt 認證配置 { //不使用https o.RequireHttpsMetadata = false; o.TokenValidationParameters = tokenValidationParameters; o.Events = new JwtBearerEvents { //此處為權限驗證失敗后觸發的事件 OnChallenge = context => { //此處代碼為終止.Net Core默認的返回類型和數據結果,這個很重要哦,必須 context.HandleResponse(); //自定義自己想要返回的數據結果,我這里要返回的是Json對象,通過引用Newtonsoft.Json庫進行轉換 var result = new ApiResultModels(); result.Code = false; result.Message = "很抱歉,您無權訪問該接口!"; //自定義返回的數據類型 context.Response.ContentType = "application/json"; //自定義返回狀態碼,默認為401 我這里改成 200 context.Response.StatusCode = StatusCodes.Status200OK; //context.Response.StatusCode = StatusCodes.Status401Unauthorized; //輸出Json數據結果 context.Response.WriteAsync(JsonConvert.SerializeObject(result)); return Task.FromResult(0); } ,OnAuthenticationFailed = context => { //如果過期,則把<是否過期>添加到,返回頭信息中 if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) { context.Response.Headers.Add("Token-Expired", "true"); } return Task.CompletedTask; } }; }); //注入授權Handler services.AddScoped<IAuthorizationHandler, PermissionHandler>(); services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.AddSingleton(permissionRequirement); #endregion return services; } } }
方便在Startup.cs使用
二、 只驗證 角色的
PermissionRequirement requiremen 相當於new 一個實體吧(雖然以前是構造方法的寫法)
從數據庫查出權限列表給requiremen,然后在去比較
角色都不需要