asp.net core 集成JWT(二)token的強制失效,基於策略模式細化api權限

【前言】

  上一篇咱們介紹了什麼是JWT,以及如何在asp.net core api項目中集成JWT權限認證。傳送門:http://www.javashuo.com/article/p-udxvpans-bq.htmlhtml

  不少博友在留言中提出了疑問:前端

  1. 如何結合jwt認證對用戶進行API受權?
  2. token過時了怎麼辦?
  3. 如何自動刷新token?
  4. 如何強制token失效?
  5. 如何應用到集羣模式?

  那麼,便有了本篇。本篇在上一篇的基礎上繼續完善JWT的使用,並陸續回答上面的疑問。固然Demo中沒有體現的也會提供思路供博友參考。git

【1、如何結合JWT認證對用戶進行API受權】

  場景:咱們有多個API接口,咱們但願細化地控制哪一個用戶能夠訪問哪些API(多是在某個受權界面進行API受權)github

  仍是咱們上一篇中的Demo項目:https://github.com/sevenTiny/Demo.Jwtredis

  

  咱們添加了兩個類:PolicyHandler.cs和PolicyRequirement.cs數據庫

  首先是:PolicyRequirement.cs,這個類文件中定義了一個用戶名和url的對應實體,UserPermission用戶權限承載實體。而後實現了微軟自帶的接口IAuthorizationRequirement,裏面構造方法賦值了若是沒有權限將要跳轉的接口和某用戶全部有權限的接口的配置集合,由於只寫了一個接口,這裏只配置了一條做爲Demo,固然了,在實際應用的時候,全部的這些配置咱們均可以寫在數據庫中持久化,須要的時候讀取出來便可。後端

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System.Collections.Generic;

namespace Demo.Jwt.AuthManagement
{
    /// <summary>
    /// 權限承載實體
    /// </summary>
    public class PolicyRequirement : IAuthorizationRequirement
    {
        /// <summary>
        /// 用戶權限集合
        /// </summary>
        public List<UserPermission> UserPermissions { get; private set; }
        /// <summary>
        /// 無權限action
        /// </summary>
        public string DeniedAction { get; set; }
        /// <summary>
        /// 構造
        /// </summary>
        public PolicyRequirement()
        {
            //沒有權限則跳轉到這個路由
            DeniedAction = new PathString("/api/nopermission");
            //用戶有權限訪問的路由配置,固然能夠從數據庫獲取
            UserPermissions = new List<UserPermission> {
                              new UserPermission {  Url="/api/value3", UserName="admin"},
                          };
        }
    }

    /// <summary>
    /// 用戶權限承載實體
    /// </summary>
    public class UserPermission
    {
        /// <summary>
        /// 用戶名
        /// </summary>
        public string UserName { get; set; }
        /// <summary>
        /// 請求Url
        /// </summary>
        public string Url { get; set; }
    }
}

  PolicyHandler 這個類繼承了微軟提供的類型AuthorizationHandler<PolicyRequirement>,泛型是咱們上一步剛定義的類型。api

  在這個類裏面,咱們實現了抽象方法 Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement),這個方法裏面明確瞭如何具體地校驗用戶是否有API權限,而且根據校驗結果控制應該跳轉到提示API,仍是繼續執行有權限的API。app

  這裏的校驗邏輯比較簡單,Demo級別的,可是提供了校驗的入口,具體業務場景根據需求進行適當替換便可。asp.net

using Microsoft.AspNetCore.Authorization;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;

namespace Demo.Jwt.AuthManagement
{
    public class PolicyHandler : AuthorizationHandler<PolicyRequirement>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
        {
            //賦值用戶權限
            var userPermissions = requirement.UserPermissions;
            //從AuthorizationHandlerContext轉成HttpContext,以便取出表求信息
            var httpContext = (context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).HttpContext;
            //請求Url
            var questUrl = httpContext.Request.Path.Value.ToUpperInvariant();
            //是否通過驗證
            var isAuthenticated = httpContext.User.Identity.IsAuthenticated;
            if (isAuthenticated)
            {
                if (userPermissions.GroupBy(g => g.Url).Any(w => w.Key.ToUpperInvariant() == questUrl))
                {
                    //用戶名
                    var userName = httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.NameIdentifier).Value;
                    if (userPermissions.Any(w => w.UserName == userName && w.Url.ToUpperInvariant() == questUrl))
                    {
                        context.Succeed(requirement);
                    }
                    else
                    {
                        //無權限跳轉到拒絕頁面
                        httpContext.Response.Redirect(requirement.DeniedAction);
                    }
                }
                else
                {
                    context.Succeed(requirement);
                }
            }
            return Task.CompletedTask;
        }
    }
}

  而後咱們改造一下模擬數據的API,添加一個 api/value3 不一樣的是,這個action咱們添加了一個帶有策略名稱的權限特性標籤:[Authorize("Permission")] 經過這個特性標籤制定了這個action 會走咱們自定義的策略方法。咱們在返回值裏面提示了「這個接口只有管理員才能訪問到」,而且返回了登錄用戶的用戶名和角色信息。

[HttpGet]
[Route("api/value3")]
[Authorize("Permission")]
public ActionResult<IEnumerable<string>> Get3()
{
    //這是獲取自定義參數的方法
    var auth = HttpContext.AuthenticateAsync().Result.Principal.Claims;
    var userName = auth.FirstOrDefault(t => t.Type.Equals(ClaimTypes.NameIdentifier))?.Value;
    var role = auth.FirstOrDefault(t => t.Type.Equals("Role"))?.Value;
    return new string[] { "這個接口有管理員權限才能夠訪問", $"userName={userName}",$"Role={role}" };
}

  上文中獲取token的方法咱們也微微進行了調整,對不一樣的登錄用戶返回不一樣的角色名,讓演示更加直觀一些,由於改動較小,這裏不粘貼代碼,有想看詳情的請下載代碼查看。

  而後咱們改造一下Startup,主要改造的地方是添加了策略模式的配置

services.AddAuthorization(options =>
{
    options.AddPolicy("Permission", policy => policy.Requirements.Add(new PolicyRequirement()));
})

  還有添加了策略模式控制類的依賴注入

//注入受權Handler
services.AddSingleton<IAuthorizationHandler, PolicyHandler>();

  下面是完整的Startup.cs代碼

using Demo.Jwt.AuthManagement;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Demo.Jwt
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            //添加策略鑑權模式
            services.AddAuthorization(options =>
            {
                options.AddPolicy("Permission", policy => policy.Requirements.Add(new PolicyRequirement()));
            })
            .AddAuthentication(s =>
            {
                //添加JWT Scheme
                s.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                s.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                s.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            //添加jwt驗證:
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateLifetime = true,//是否驗證失效時間
                    ClockSkew = TimeSpan.FromSeconds(30),

                    ValidateAudience = true,//是否驗證Audience
                    //ValidAudience = Const.GetValidudience(),//Audience
                    //這裏採用動態驗證的方式,在從新登錄時,刷新token,舊token就強制失效了
                    AudienceValidator = (m, n, z) =>
                    {
                        return m != null && m.FirstOrDefault().Equals(Const.ValidAudience);
                    },
                    ValidateIssuer = true,//是否驗證Issuer
                    ValidIssuer = Const.Domain,//Issuer,這兩項和前面簽發jwt的設置一致

                    ValidateIssuerSigningKey = true,//是否驗證SecurityKey
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到SecurityKey
                };
                options.Events = new JwtBearerEvents
                {
                    OnAuthenticationFailed = context =>
                    {
                        //Token expired
                        if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                        {
                            context.Response.Headers.Add("Token-Expired", "true");
                        }
                        return Task.CompletedTask;
                    }
                };
            });

            //注入受權Handler
            services.AddSingleton<IAuthorizationHandler, PolicyHandler>();

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            ///添加jwt驗證
            app.UseAuthentication();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                        template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

  咱們完成了這些工做之後,咱們明確咱們的目標:

  1. api/value1 接口咱們不登錄就能夠直接進行訪問
  2. api/value2 接口只有登錄用戶能夠訪問,不登陸的用戶是沒有權限的
  3. api/value3 接口只有admin帳號登錄(代碼裏寫死的帳號admin,也只爲admin配置了權限)才能夠訪問,普通用戶是不能訪問的

  明確了上面的幾個目標後,下面咱們進行測試,依然是運行起來咱們的項目:

  1.api/value1 接口咱們不登錄就能夠直接進行訪問

  

  

  咱們沒有登錄即可以訪問到api/value1接口

  2.api/value2 接口只有登錄用戶能夠訪問,不登陸的用戶是沒有權限的

  2.1. 咱們先直接訪問api/value2接口

  

  

  返回了狀態碼:401 無權限

  2.2. 那麼咱們調用登錄接口獲取token

  

  

  2.3. 成功返回了token,咱們拿該token去訪問 api/value2 接口

  

  

  能夠看到,咱們成功拿到了數據,足以證實,api/value2 接口是須要登錄權限的

  3. 那麼,咱們用這個token去訪問 api/value3 又會怎樣呢?

  

  

  返回了403,訪問錯誤。這個403是怎麼來的呢?

  咱們上文說過的PolicyHandler.cs文件中若是校驗接口沒有權限呢,咱們會走下面這段邏輯:

//無權限跳轉到拒絕頁面
httpContext.Response.Redirect(requirement.DeniedAction);

  

  requirement.DeniedAction是咱們PolicyRequirement.cs文件中配置死的地址:"/api/nopermission"

  

  這個地址返回的就是403 Forbid,固然這裏能夠根據須要修改返回內容,再也不贅述。

  4. 咱們換一個admin帳號從新登錄,而後訪問 api/value3 接口

  4.1 首先咱們調用獲取token接口進行token獲取

  

  

  4.2 咱們拿到一個新的token,而後用這個新的token去訪問剛纔沒權限的接口

  

  

  成功地獲取到告終果,說明咱們的配置策略生效了,只有admin帳號纔有權限獲取到這個接口。

  上面就是咱們完整的策略模式的實現方案,完整的代碼能夠在github地址中進行下載或clone。

【2、Token的使用策略】

  1.token過時了怎麼辦?

  關於token過時這個話題呢,有不少應用場景,對應不一樣的處理方式。

  好比:token過時能夠提示用戶從新登錄,常見的有登錄一段時間後要從新登錄校驗密碼;

  好比:token過時可使用其餘手段進行「偷偷」刷新,用戶感受不到,可是token已是新的了;

  2.如何自動刷新token

  那麼token偷偷刷新有什麼實現方式呢?

  好比:約定好失效的時間,前端在失效前進行從新調用登錄接口進行獲取;

  好比:使用SignalR,保持先後端通信也能夠必定時間輪詢刷新token;

  好比:後端執行策略,定時任務刷新token,若是持續請求接口,就能夠拿到最新的token進行「續命」,若是長時間不訪問任意接口,那麼token也就失效了;

  3.如何強制token失效?

  什麼場景要強制token失效呢?好比咱們只容許帳號一個地方登錄一次,異地登錄會將帳號擠下線。這種時候咱們就要將舊token失效,僅僅讓新的token生效。

  下面咱們在Demo中體現如何讓舊token強制失效。

  3.1  在咱們以前說過的Const.cs類中添加一個靜態變量(不是const,const是隻讀的),讓咱們在程序中能夠直接修改值。固然又是爲了模擬,真實場景這個值應該持久化或者存在redis裏面,這裏咱們爲了代碼簡潔易懂就不集成太多的組件了。

  3.2 稍微修改一下咱們的獲取token的action,在密碼驗證成功以後,修改靜態變量的值。

  變量值採用帳號密碼加當前時間字符串,以保證每次登錄都是不同的值。

//每次登錄動態刷新
Const.ValidAudience = userName + pwd + DateTime.Now.ToString();

  而後咱們在生成token的時候,讓接收者=咱們靜態變量的值,audience: Const.ValidAudience

  完整的代碼以下:

[AllowAnonymous]
        [HttpGet]
        [Route("api/auth")]
        public IActionResult Get(string userName, string pwd)
        {
            if (CheckAccount(userName, pwd, out string role))
            {
                //每次登錄動態刷新
                Const.ValidAudience = userName + pwd + DateTime.Now.ToString();
                // push the user’s name into a claim, so we can identify the user later on.
                //這裏能夠隨意加入自定義的參數,key能夠本身隨便起
                var claims = new[]
                {
                    new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") ,
                    new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddMinutes(30)).ToUnixTimeSeconds()}"),
                    new Claim(ClaimTypes.NameIdentifier, userName),
                    new Claim("Role", role)
                };
                //sign the token using a secret key.This secret will be shared between your API and anything that needs to check that the token is legit.
                var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey));
                var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
                //.NET Core’s JwtSecurityToken class takes on the heavy lifting and actually creates the token.
                var token = new JwtSecurityToken(
                    //頒發者
                    issuer: Const.Domain,
                    //接收者
                    audience: Const.ValidAudience,
                    //過時時間
                    expires: DateTime.Now.AddMinutes(30),
                    //簽名證書
                    signingCredentials: creds,
                    //自定義參數
                    claims: claims
                    );

                return Ok(new
                {
                    token = new JwtSecurityTokenHandler().WriteToken(token)
                });
            }
            else
            {
                return BadRequest(new { message = "username or password is incorrect." });
            }
        }

  3.3 而後改造一下StartUp.cs

  咱們僅僅須要關心改動的地方,也就是AddJwtBearer這個驗證token的方法,咱們不用原先的固定值的校驗方式,而提供一個代理方法進行運行時執行校驗

.AddJwtBearer(options =>

options.TokenValidationParameters = new TokenValidationParameters
{
    ValidateLifetime = true,//是否驗證失效時間
    ClockSkew = TimeSpan.FromSeconds(30),
    ValidateAudience = true,//是否驗證Audience
    //ValidAudience = Const.GetValidudience(),//Audience
    //這裏採用動態驗證的方式,在從新登錄時,刷新token,舊token就強制失效了
    AudienceValidator = (m, n, z) =>
    {
        return m != null && m.FirstOrDefault().Equals(Const.ValidAudience);
    },
    ValidateIssuer = true,//是否驗證Issuer
    ValidIssuer = Const.Domain,//Issuer,這兩項和前面簽發jwt的設置一致
    ValidateIssuerSigningKey = true,//是否驗證SecurityKey
    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到SecurityKey
};

  這裏邏輯是這樣的:由於從新登錄將原來的變量更改了,因此這裏校驗的時候也一併修改爲了新的變量值,那麼舊的token固然就不匹配了,也就是舊的token被強制失效了。

   3.4 咱們實際驗證一下

  3.4.1 首先咱們用admin帳號獲取token

  

  

  3.4.2 而後用該token訪問有權限的 api/value3 接口

  

  

  意料之中,咱們成功訪問到了值,並且在有效期內訪問屢次都是能夠訪問成功的。

  3.4.3 那麼咱們用admin帳號從新獲取token

  

  

  拿到一個新的token

  3.4.4 咱們不更換token,再用舊的token調用一下 api/value3

  

  

  返回狀態碼401了,說明沒有權限了

  

  同時headers裏面有錯誤描述時接收人蔘數錯誤,說明一切盡在咱們的預期之中。

  3.4.5 那麼咱們使用咱們第二次登錄用的新的token進行訪問api/value3

  

  

  又成功地獲取到了數據,代表咱們新的token佔有了當前寶座,老國王已經被擠下臺了!

  4. 如何應用到集羣模式

  這個問題其實在測試過Demo,而後再結合咱們平常應用的話,答案很容易獲得。如下幾種參考:

  1. 咱們這個Demo其實相關參數都是從Const.cs常量文件中獲取的,文中也說了,實際應用中應從數據庫或redis中獲取。這些信號都代表了實際應用中不少都是走的配置中心或者是數據庫,這些中間件本就自然支持集羣模式,所以部署多套服務和部署一套服務是同樣的,一個接口能經過的驗證,多個接口也一樣能經過驗證。
  2. 第二種場景在大項目中或者微服務場景中比較常見,那就是微服務網關,咱們徹底能夠將JWT集成在微服務網關上,而不用關心具體的下游服務。只要網關能經過認證就能夠訪問到下游的服務節點。

【結尾】

  到這裏,咱們在上一篇中「JWT的簡介以及asp.net core 集成JWT」中遺留的問題已經所有解釋完畢了,固然了,若是有新的問題也很是歡迎各路朋友在評論區留下您寶貴的意見。

  上一篇傳送門:http://www.javashuo.com/article/p-udxvpans-bq.html

  若是想要完整項目源碼的,能夠參考地址:https://github.com/sevenTiny/Demo.Jwt

  若是有幸能幫助到你,高擡貴手點個star吧~

相關文章
相關標籤/搜索