10分鐘簡單學習net core整合jwt許可權認證,快速接入專案落地使用

語言: CN / TW / HK

什麼是JWT

JSON Web Token(JWT)是目前最流行的跨域身份驗證、分散式登入、單點登入等解決方案。

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

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

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

JSON Web令牌能做什麼?

  1. 授權:這是使用JWT的最常見方案。一旦使用者登入,每個後續請求將包括JWT,允許使用者訪問該令牌允許的路由,服務和資源。Single Sign On是一種現在廣泛使用JWT的功能,因為它的開銷很小,並且能夠在不同的域中輕鬆使用。
  2. 資訊交換:JSON Web令牌是在各方之間安全傳輸資訊的好方法。因為JWT可以簽名 - 例如,使用公鑰/私鑰對 - 您可以確定發件人是他們所說的人。此外,由於使用標頭和有效負載計算簽名,您還可以驗證內容是否未被篡改。

JSON Web令牌如何工作?

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

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

Authorization: Bearer <token>

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

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

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

1、應用程式向授權伺服器請求授權;

2、校驗使用者身份,校驗成功,返回token;

3、應用程式使用訪問令牌訪問受保護的資源。

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

更多理論知識可以檢視官網,或者檢視相關網友的文章,如下推薦文章:

net core 整合jwt程式碼實現

新建專案

首先我們新建一個 ASP.NET Core Web API 專案,命名為 jwtWebAPI ,選擇目標框架.NET Core3.1,注意,如果勾選了https配置,postman請求的時候要設定去除ssl認證才能使用,建議不配置https。

在nuget裡面引用jwt整合的程式包,這裡需要注意的是,如果你用的是.NET Core 3.1的框架的話,程式包版本選擇3.1.10

Microsoft.AspNetCore.Authentication.JwtBearer

新增資料訪問模擬api,新建控制器ValuesController

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

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

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

        /**
         * 該介面用Authorize特性做了許可權校驗,如果沒有通過許可權校驗,則http返回狀態碼為401
         * 呼叫該介面的正確姿勢是:
         * 1.登陸,呼叫api/Auth介面獲取到token
         * 2.呼叫該介面 api/value2 在請求的Header中新增引數 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNTYwMzM1MzM3IiwiZXhwIjoxNTYwMzM3MTM3LCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiemhhbmdzYW4iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.1S-40SrA4po2l4lB_QdzON_G5ZNT4P_6U25xhTcl7hI
         * Bearer後面有空格,且後面是第一步中介面返回的token值
         * */
        [HttpGet]
        [Route("api/value2")]
        [Authorize]
        public ActionResult<IEnumerable<string>> value2()
        {
            //這是獲取自定義引數的方法
            var auth = HttpContext.AuthenticateAsync().Result.Principal.Claims;
            var userName = auth.FirstOrDefault(t => t.Type.Equals(ClaimTypes.NameIdentifier))?.Value;
            return new string[] { "訪問成功:這個介面登陸過的使用者都可以訪問", $"userName={userName}" };
        }

     
    }
}

新增模擬登陸生成Token的api, 新建控制器 AuthController

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

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
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;

namespace jwtWebAPI.Controllers
{
    [ApiController]
    public class AuthController : Controller
    {
        /// <summary>
        /// 通過賬號+密碼獲取Token
        /// </summary>
        /// <param name="userName"></param>
        /// <param name="pwd"></param>
        /// <returns>Token</returns>
        [AllowAnonymous]
        [HttpGet]
        [Route("api/auth")]
        public IActionResult GetToken(string userName, string pwd)
        {
            if (!string.IsNullOrEmpty(userName))
            {
                //每次登陸動態重新整理
                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(3)).ToUnixTimeSeconds()}"),
                    new Claim(ClaimTypes.NameIdentifier, userName)
                };
                //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,
                    //過期時間(可自行設定,注意和上面的claims內部Exp引數保持一致)
                    expires: DateTime.Now.AddMinutes(3),
                    //簽名證書
                    signingCredentials: creds,
                    //自定義引數
                    claims: claims
                    );

                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.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace jwtWebAPI
{
    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
                    {
                        ValidateLifetime = true,//是否驗證失效時間
                        ClockSkew = TimeSpan.FromSeconds(30),  //時間偏移量(允許誤差時間)
                        ValidateAudience = true,//是否驗證Audience(驗證之前的token是否失效)
                        //ValidAudience = Const.GetValidudience(),//Audience
                        //這裡採用動態驗證的方式,在重新登陸時,重新整理token,舊token就強制失效了
                        AudienceValidator = (m, n, z) =>
                        {
                            return m != null && m.FirstOrDefault().Equals(Const.ValidAudience);
                        },
                        ValidateIssuer = true,//是否驗證Issuer(頒發者)
                        ValidAudience = Const.Domain,//Audience    【Const是新建的一個常量類】  接收者 
                        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;
                        }
                    };
                });

            services.AddControllers();
        }

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

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

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

建立常量類Const

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace jwtWebAPI
{
    public class Const
    {
        /// <summary>
        /// 這裡為了演示,寫死一個金鑰。實際生產環境可以從配置檔案讀取,這個是用網上工具隨便生成的一個金鑰(md5或者其他都可以)
        /// </summary>
        public const string SecurityKey = "48754F4C58F9EA428FE09D714E468211";

        /// <summary>
        /// 站點地址(頒發者、接受者),這裡測試和當前本地執行網站相同,實際發到正式環境應為域名地址
        /// </summary>
        public const string Domain = "http://localhost:44345";

        /// <summary>
        /// 受理人,之所以弄成可變的是為了用介面動態更改這個值以模擬強制Token失效
        /// 真實業務場景可以在資料庫或者redis存一個和使用者id相關的值,生成token和驗證token的時候獲取到持久化的值去校驗
        /// 如果重新登陸,則重新整理這個值
        /// </summary>
        public static string ValidAudience;
    }
}

JWT登入授權測試成功

把程式編譯執行起來,開啟postman,輸入地址,首先測試不需要任何授權的

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

首先我們什麼都不加呼叫介面:http://localhost:44345/api/values2,注意,我建立的時候是https的,大家注意看是http還是https

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

呼叫模擬登陸授權介面:http://localhost:44345/api/auth?userName=xiongze&pwd=123456

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

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

在剛才401的介面( http://localhost:44345/api/values2 )請求header中新增JWT的引數,把我們的token加上去

再次呼叫我們的模擬資料介面,但是這次我們加了一個header,KEY: Authorization      Value:Bearer Tokne的值

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

得到返回值,正確授權成功,我們是支援自定義返回引數的,上面程式碼裡面有相關內容,比如使用者名稱這些不敏感的資訊可以帶著返回。

等token設定的過期時間到了,或者重新生成了新的Token,沒有及時更新,那麼我們的授權也到期,401,

升級操作:介面許可權隔離

上面的操作是所有登入授權成功的角色都可以進行呼叫所有介面,那麼我們現在想要進行介面隔離限制,

也就是說,雖然授權登入了,但是我這個介面是指定許可權訪問的。

比如說:刪除介面只能管理員角色操作,那麼其他角色雖然授權登入了,但是沒有許可權呼叫刪除介面。

我們在原來的操作進行改造升級看一下。

新增類

新建一個 AuthManagement 資料夾,新增 PolicyRequirement類PolicyHandler類

PolicyRequirement類:

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

namespace jwtWebAPI.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/values3", UserName="admin"},
                          };
        }
    }

    /// <summary>
    /// 使用者許可權承載實體
    /// </summary>
    public class UserPermission
    {
        /// <summary>
        /// 使用者名稱
        /// </summary>
        public string UserName { get; set; }
        /// <summary>
        /// 請求Url
        /// </summary>
        public string Url { get; set; }
    }
}

PolicyHandler類(注意2.x和3.x的區別)

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

namespace jwtWebAPI.AuthManagement
{
    public class PolicyHandler : AuthorizationHandler<PolicyRequirement>
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
        public PolicyHandler(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
        {
            //賦值使用者許可權
            var userPermissions = requirement.UserPermissions;
            var httpContext = _httpContextAccessor.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);
                        return Task.CompletedTask;
                    }
                }
                else
                {
                    context.Succeed(requirement);
                }
            }
            return Task.CompletedTask;
        }
    }
}

新增指定角色

在  AuthController 控制器的GetToken授權加入自定義的引數,如下

new Claim("Role", userName)  //這裡是角色,我使用登入賬號admin代替

在  AuthController  控制器裡面新增無許可權訪問的方法

[AllowAnonymous]
[HttpGet]
[Route("api/nopermission")]
public IActionResult NoPermission()
{
     return Forbid("No Permission!");
}

修改Startup配置

在startup.cs的ConfigureServices 方法裡面 新增策略鑑權模式、 新增JWT Scheme、注入授權Handler

修改後的檔案如下

using jwtWebAPI.AuthManagement;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace jwtWebAPI
{
    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()));
                })
                //新增JWT Scheme
                .AddAuthentication(s =>
                {
                    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(驗證之前的token是否失效)
                        //ValidAudience = Const.GetValidudience(),//Audience
                        //這裡採用動態驗證的方式,在重新登陸時,重新整理token,舊token就強制失效了
                        AudienceValidator = (m, n, z) =>
                        {
                            return m != null && m.FirstOrDefault().Equals(Const.ValidAudience);
                        },
                        ValidateIssuer = true,//是否驗證Issuer(頒發者)
                        ValidAudience = Const.Domain,//Audience    【Const是新建的一個常量類】  接收者 
                        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>();
            //注入獲取HttpContext
            services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.AddControllers();
        }

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

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

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
            
        }
    }
}

新增api訪問的方法

在  ValuesController 控制器新增指定許可權訪問的方法,如下:

        /**
        * 這個介面必須用admin
        **/
        [HttpGet]
        [Route("api/values3")]
        [Authorize("Permission")]
        public ActionResult<IEnumerable<string>> values3()
        {
            //這是獲取自定義引數的方法
            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}" };
        }

不同許可權測試訪問

我們同樣的方法去模擬登入, http://localhost:44345/api/auth?userName=xiongze&pwd=123

注意,賬號先不用admin登入,然後用返回的token去請求我們剛剛新增的指定許可權訪問的介面,這個時候是沒有許可權訪問的,因為這個是admin許可權訪問。

我們同樣的方法去模擬登入, http://localhost:44345/api/auth?userName=admin&pwd=123

訪問成功。

原始碼下載地址

Gitee: http://gitee.com/xiongze/jwtWebAPI.git

參考文獻

歡迎關注訂閱微信公眾號【熊澤有話說】,更多好玩易學知識等你來取

作者:熊澤-學習中的苦與樂

公眾號:熊澤有話說

出處:  http://www.cnblogs.com/xiongze520/p/15540035.html

您可以隨意轉載、摘錄,但請在文章內註明作者和原文連結。