1. 程式人生 > 其它 >asp.net core 整合JWT

asp.net core 整合JWT

asp.net core 整合JWT(一)

【什麼是JWT】

  JSON Web Token(JWT)是目前最流行的跨域身份驗證解決方案。

  JWT的官網地址:https://jwt.io/

  通俗地來講,JWT是能代表使用者身份的令牌,可以使用JWT令牌在api介面中校驗使用者的身份以確認使用者是否有訪問api的許可權。

  JWT中包含了身份認證必須的引數以及使用者自定義的引數,JWT可以使用祕密(使用HMAC演算法)或使用RSAECDSA的公鑰/私鑰對進行簽名

【什麼時候應該使用JSON Web令牌?】

  1. 授權:這是使用JWT的最常見方案。一旦使用者登入,每個後續請求將包括JWT,允許使用者訪問該令牌允許的路由,服務和資源。Single Sign On是一種現在廣泛使用JWT的功能,因為它的開銷很小,並且能夠在不同的域中輕鬆使用。

  2. 資訊交換:JSON Web令牌是在各方之間安全傳輸資訊的好方法。因為JWT可以簽名 - 例如,使用公鑰/私鑰對 - 您可以確定發件人是他們所說的人。此外,由於使用標頭和有效負載計算簽名,您還可以驗證內容是否未被篡改。

【JWT有什麼優勢?】

  我們先看我們傳統的身份校驗方式

  1. 使用者向伺服器傳送使用者名稱和密碼。
  2. 伺服器驗證通過後,在當前對話(session)裡面儲存相關資料,比如使用者角色、登入時間等等。
  3. 伺服器向用戶返回一個 session_id,寫入使用者的 Cookie。
  4. 使用者隨後的每一次請求,都會通過 Cookie,將 session_id 傳回伺服器。
  5. 伺服器收到 session_id,找到前期儲存的資料,由此得知使用者的身份。

  這種模式的問題在於,擴充套件性(scaling)不好。單機當然沒有問題,如果是伺服器叢集,或者是跨域的服務導向架構,就要求 session 資料共享,每臺伺服器都能夠讀取 session。如果session儲存的節點掛了,那麼整個服務都會癱瘓,體驗相當不好,風險也很高。

  相比之下,JWT的實現方式是將使用者資訊儲存在客戶端,服務端不進行儲存。每次請求都把令牌帶上以校驗使用者登入狀態,這樣服務就變成了無狀態的,伺服器叢集也很好擴充套件。

【JWT令牌結構】

  在緊湊的形式中,JSON Web Tokens由dot(.分隔的三個部分組成,它們是:

  • Header 頭
  • Payload 有效載荷
  • Signature 簽名

  因此,JWT通常如下所示:

  xxxxx.yyyyy.zzzzz

  1.Header 頭

  標頭通常由兩部分組成:令牌的型別,即JWT,以及正在使用的簽名演算法,例如HMAC SHA256或RSA。

  例如:

{
  "alg": "HS256",
  "typ": "JWT"
}

  然後,這個JSON被編碼Base64Url,形成JWT的第一部分。

  2.Payload有效載荷

  Payload 部分也是一個 JSON 物件,用來存放實際需要傳遞的資料。JWT 規定了7個官方欄位,供選用。

  • iss (issuer):簽發人

  • exp (expiration time):過期時間

  • sub (subject):主題

  • aud (audience):受眾

  • nbf (Not Before):生效時間

  • iat (Issued At):簽發時間

  • jti (JWT ID):編號

  除了官方欄位,你還可以在這個部分定義私有欄位,下面就是一個例子。例如:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

  注意,JWT 預設是不加密的,任何人都可以讀到,所以不要把祕密資訊放在這個部分。這個 JSON 物件也要使用 Base64URL 演算法轉成字串。

  3.Signature 簽名

  Signature 部分是對前兩部分的簽名,防止資料篡改。

  首先,需要指定一個金鑰(secret)。這個金鑰只有伺服器才知道,不能洩露給使用者。然後,使用 Header 裡面指定的簽名演算法(預設是 HMAC SHA256),按照下面的公式產生簽名。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

  簽名用於驗證訊息在此過程中未被更改,並且,在使用私鑰簽名的令牌的情況下,它還可以驗證JWT的發件人是否是它所聲稱的人。  

  把他們三個全部放在一起

  輸出是三個由點分隔的Base64-URL字串,可以在HTML和HTTP環境中輕鬆傳遞,而與基於XML的標準(如SAML)相比更加緊湊。

  下面顯示了一個JWT,它具有先前的頭和​​有效負載編碼,並使用機密簽名。

  

  如果您想使用JWT並將這些概念付諸實踐,您可以使用jwt.io Debugger來解碼,驗證和生成JWT。

  

【JSON Web令牌如何工作?】

  在身份驗證中,當用戶使用其憑據成功登入時,將返回JSON Web令牌。由於令牌是憑證,因此必須非常小心以防止出現安全問題。一般情況下,您不應該將令牌保留的時間超過要求。

  每當使用者想要訪問受保護的路由或資源時,使用者代理應該使用承載模式傳送JWT,通常在Authorization標頭中標題的內容應如下所示:

  Authorization: Bearer <token>

  在某些情況下,這可以是無狀態授權機制。伺服器的受保護路由將檢查Authorization標頭中的有效JWT,如果存在,則允許使用者訪問受保護資源。如果JWT包含必要的資料,則可以減少查詢資料庫以進行某些操作的需要,儘管可能並非總是如此。

  如果在標Authorization頭中傳送令牌,則跨域資源共享(CORS)將不會成為問題,因為它不使用cookie。

  下圖顯示瞭如何獲取JWT並用於訪問API或資源:

  

  1. 應用程式向授權伺服器請求授權
  2. 校驗使用者身份,校驗成功,返回token
  3. 應用程式使用訪問令牌訪問受保護的資源

【ASP.Net Core 整合JWT】

  前面我們介紹了JWT的原理,下面我們在asp.net core實際專案中整合JWT。

  首先我們新建一個Demo asp.net core 空web專案

  

  新增資料訪問模擬api,ValuesController

  其中api/value1是可以直接訪問的,api/value2添加了許可權校驗特性標籤 [Authorize]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Demo.Jwt.Controllers
{
    [ApiController]
    public class ValuesController : ControllerBase
    {
        [HttpGet]
        [Route("api/value1")]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value1", "value1" };
        }

        [HttpGet]
        [Route("api/value2")]
        [Authorize]
        public ActionResult<IEnumerable<string>> Get2()
        {
            return new string[] { "value2", "value2" };
        }
    }
}

  新增模擬登陸,生成Token的api,AuthController

  這裡模擬一下登陸校驗,只驗證了使用者密碼不為空即通過校驗,真實環境完善校驗使用者和密碼的邏輯。

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;

namespace Demo.Jwt.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AuthController : ControllerBase
    {
        [AllowAnonymous]
        [HttpGet]
        public IActionResult Get(string userName, string pwd)
        {
            if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(pwd))
            {
                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.Name, userName)
                };
                var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey));
                var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
                var token = new JwtSecurityToken(
                    issuer: Const.Domain,
                    audience: Const.Domain,
                    claims: claims,
                    expires: DateTime.Now.AddMinutes(30),
                    signingCredentials: creds);

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

  Startup新增JWT驗證的相關配置

using Microsoft.AspNetCore.Authentication.JwtBearer;
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.Text;


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)
        {
            //新增jwt驗證:
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options => {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,//是否驗證Issuer
                        ValidateAudience = true,//是否驗證Audience
                        ValidateLifetime = true,//是否驗證失效時間
                        ClockSkew = TimeSpan.FromSeconds(30),
                        ValidateIssuerSigningKey = true,//是否驗證SecurityKey
                        ValidAudience = Const.Domain,//Audience
                        ValidIssuer = Const.Domain,//Issuer,這兩項和前面簽發jwt的設定一致
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到SecurityKey
                    };
                });

            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?}");
            });
        }
    }
}

  最後把程式碼裡面用到的一些相關常量也貼上過來,Const.cs

namespace Demo.Jwt
{
    public class Const
    {
        /// <summary>
        /// 這裡為了演示,寫死一個金鑰。實際生產環境可以從配置檔案讀取,這個是用網上工具隨便生成的一個金鑰
        /// </summary>
        public const string SecurityKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDI2a2EJ7m872v0afyoSDJT2o1+SitIeJSWtLJU8/Wz2m7gStexajkeD+Lka6DSTy8gt9UwfgVQo6uKjVLG5Ex7PiGOODVqAEghBuS7JzIYU5RvI543nNDAPfnJsas96mSA7L/mD7RTE2drj6hf3oZjJpMPZUQI/B1Qjb5H3K3PNwIDAQAB";
        public const string Domain = "http://localhost:5000";
    }
}

  到這裡,已經是我們專案的所有程式碼了。

  如果需要完整的專案程式碼,Github地址:https://github.com/sevenTiny/Demo.Jwt

【JWT測試】

  我們找一個趁手的工具,比如fiddler,然後把我們的web站點執行起來

  首先呼叫無許可權的介面:http://localhost:5000/api/value1

  

  

  正確地返回了資料,那麼接下來我們測試JWT的流程

  1. 無許可權

  首先我們什麼都不加呼叫介面:http://localhost:5000/api/value2

  

  

  返回了狀態碼401,也就是未經授權:訪問由於憑據無效被拒絕。 說明JWT校驗生效了,我們的介面收到了保護。

  2.獲取Token

  呼叫模擬登陸授權介面:http://localhost:5000/api/Auth?userName=zhangsan&pwd=123

  這裡的使用者密碼是隨便寫的,因為我們模擬登陸只是校驗了下非空,因此寫什麼都能通過

  

  成功得到了響應

  

  

  然後我們得到了一個xxx.yyy.zzz 格式的 token 值。我們把token複製出來

  3.在剛才401的介面請求HEADER中新增JWT的引數,把我們的token加上去

  再次呼叫我們的模擬資料介面,但是這次我們加了一個HEADER:http://localhost:5000/api/value2

  

  把內容粘出來

User-Agent: Fiddler
Host: localhost:5000
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNTYwMzQ1MDIxIiwiZXhwIjoxNTYwMzQ2ODIxLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiemhhbmdzYW4iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.x7Slk4ho1hZc8sR8_McVTB6VEYLz_v-5eaHvXtIDS-o

  這裡需要注意Bearer 後面是有一個空格的,然後就是我們上一步獲取到的token

  

  

  嗯,沒有401了,成功返回了資料

  4.JWT的Token過期

  我們且倒一杯開水,坐等30分鐘(我們程式碼中設定的過期時間),然後再次呼叫資料介面:http://localhost:5000/api/value2

  

  

  又變成了401,我們看下詳細的返回資料

  

  這裡有標註,錯誤描述 token過期,說明我們設定的token過期時間生效了

  5.JWT新增自定義的引數(比如帶上使用者資訊)

  假如我們想在認證通過的時候,直接從jwt的token中獲取到登陸的使用者名稱,該怎麼操作呢?

  首先在我們的獲取token 的api接口裡面新增一個Claim節點,key可以隨便給,也可以使用已經提供好的一些預置Key,value是我們登陸的userName(僅作為演示)

  

  然後在我們的模擬資料介面獲取自定義引數

  

  這裡使用HttpContext的授權擴充套件方法,拿到認證的資訊,我們來看下結果

  

  

  請求成功返回,並且也拿到了我們一開始寫入的userName

【評論區的一些問題】

  1.token過期了怎麼辦?

  token過期了說明登陸資訊已經過期,需要重新登陸,跳轉到登入頁重新登陸獲取新的token。(當然自動重新整理token除外)

  2.如何交換新的token

  如果要保證token長期有效,可以前端在過期前呼叫登陸介面重新整理token。或者使用SignalR輪詢,定期重新整理token。

  3.如何強制token失效?

  我們有個ValidAudience(接收人),可以利用這個標準引數,登陸時候生成一個GUID,在資料庫/Redis/xxx存一份,然後驗證介面的時候再把這個值拿出來去一起校驗。如果值變了校驗就失敗了,當然,重新登陸就會重新整理這個值,所以只要重新登陸,舊的token也就失效了。

  4.如何應用到叢集模式

  當前Demo裡面,我們驗證jwt的所有引數都是Const常量寫死的,但是在真實生產環境都是可以走統一的配置中心,所以叢集場景下,一個token可以在多個服務上被驗證通過,因為校驗token正確的金鑰和相關引數都是從配置中心獲取的。

【結束】

  到這裡,我們JWT的簡介以及asp.net core 整合JWT已經完美完成,當然了這只是一個demo,在實際的應用中需要補充和完善的地方還有很多。

  這一篇文章中評論區的一些疑問我放在了下一篇文章逐一解決,有興趣的朋友請移步下文:asp.net core 整合JWT(二)token的強制失效,基於策略模式細化api許可權

  如果想要完整專案原始碼的,可以參考地址:https://github.com/sevenTiny/Demo.Jwt

  如果有幸能幫助到你,高抬貴手點個star吧~

出處:https://www.cnblogs.com/7tiny/p/11012035.html

=======================================================================================

asp.net core 整合JWT(二)token的強制失效,基於策略模式細化api許可權

【前言】

  上一篇我們介紹了什麼是JWT,以及如何在asp.net core api專案中整合JWT許可權認證。傳送門:https://www.cnblogs.com/7tiny/p/11012035.html

  很多博友在留言中提出了疑問:

  1. 如何結合jwt認證對使用者進行API授權?
  2. token過期了怎麼辦?
  3. 如何自動重新整理token?
  4. 如何強制token失效?
  5. 如何應用到叢集模式?

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

【一、如何結合JWT認證對使用者進行API授權】

  場景:我們有多個API介面,我們希望細化地控制哪個使用者可以訪問哪些API(可能是在某個授權介面進行API授權)

  還是我們上一篇中的Demo專案:https://github.com/sevenTiny/Demo.Jwt

  

  我們添加了兩個類: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>,泛型是我們上一步剛定義的型別。

  在這個類裡面,我們實現了抽象方法 Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement),這個方法裡面明確瞭如何具體地校驗使用者是否有API許可權,並且根據校驗結果控制應該跳轉到提示API,還是繼續執行有許可權的API。

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

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。

【二、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”中遺留的問題已經全部解釋完畢了,當然了,如果有新的問題也非常歡迎各路朋友在評論區留下您寶貴的意見。

  上一篇傳送門:https://www.cnblogs.com/7tiny/p/11012035.html

  如果想要完整專案原始碼的,可以參考地址:https://github.com/sevenTiny/Demo.Jwt

  如果有幸能幫助到你,高抬貴手點個star吧~

出處:https://www.cnblogs.com/7tiny/p/11019698.html

您的資助是我最大的動力!
金額隨意,歡迎來賞!
款後有任何問題請給我留言。

如果,您認為閱讀這篇部落格讓您有些收穫,不妨點選一下右下角的推薦按鈕。
如果,您希望更容易地發現我的新部落格,不妨點選一下綠色通道的關注我。(●'◡'●)

如果你覺得本篇文章對你有所幫助,請給予我更多的鼓勵,求打 付款後有任何問題請給我留言!!!

因為,我的寫作熱情也離不開您的肯定支援,感謝您的閱讀,我是【Jack_孟】!