.net Core jwt策略參數


一、實現

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,然后在去比較

 

 角色都不需要


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM