ASP.NET Core使用JWT+標識框架(identity)實現登錄驗證

来源:https://www.cnblogs.com/luoxiwen/archive/2023/08/26/17035879.html
-Advertisement-
Play Games

最近閱讀了《ASP.NET Core 技術內幕與項目實戰——基於DDD與前後端分離》(作者楊中科)的第八章,對於Core入門的我來說體會頗深,整理相關筆記。 JWT:全稱“JSON web toke”,目前流行的跨域身份驗證解決方案; 標識框架(identity):由ASP.NET Core提供的框 ...


最近閱讀了《ASP.NET Core 技術內幕與項目實戰——基於DDD與前後端分離》(作者楊中科)的第八章,對於Core入門的我來說體會頗深,整理相關筆記。

JWT:全稱“JSON web toke”,目前流行的跨域身份驗證解決方案;

標識框架(identity):由ASP.NET Core提供的框架,它採用RBAC(role-based access control)策略,內置了對用戶、角色等表的管理即相關介面,從而簡化了系統開發,使用EF Core對資料庫進行操作。

註意:本書全篇採用“模型驅動開發”

一、JWT 實現登錄的流程如下:

1、使用標識框架(identity)生成資料庫

2、客戶端向伺服器端發送用戶名、密碼等請求登錄

3、伺服器端校驗用戶名、密碼,如果校驗成功,則從資料庫中取出這個用戶的 ID、角色等用戶相關信息。

4、伺服器端採用只有伺服器端才知道的密鑰來對用戶信息的JSON字元串進行簽名,形成簽名數據。

5、伺服器端把用戶信息的JSON 字元串和簽名拼接到一起形成JWT,然後發送給客戶端。

6、客戶端保存伺服器端返回的 JWT,並且在客戶端每次向伺服器端發送請求的時候都帶上這個JWT。每次伺服器端收到瀏覽器請求中攜帶的JWT後,伺服器端用密鑰對JWT 的簽名進行校驗,如果校驗成功,伺服器端則從JWT 中的JSON 字元串中讀取出用戶的信息。這樣伺服器端就知道這個請求對應的用戶了,也就實現了登錄的功能。

二、實現過程及代碼如下:

1、通過nuget安裝必須的包:

Microsoft.AspNetCore.Identity.EntityFrameworkCore  ---如果該包安裝報錯,請切換低版本

Microsoft.AspNetCore.EntityFrameworkCore.SqlServer

Microsoft.AspNetCore.EntityFrameworkCore.Tools

Microsoft.AspNetCore.Authentication.JwtBearer

2、通過標識框架(identity)配置生成資料庫

(1)創建User類和Role類分別再繼承IdentityUser<long>和IdentityRole<long>

public class User:IdentityUser<long>
    {
        //創建時間
        public DateTime CreationTime { get; set; }
        //昵稱
        public string? NickName { get; set; }
        //JWT版本(解決JWT撤回問題)
        public long JWTVersion { get; set; }
    }
public class Role:IdentityRole<long>{}

(2)新建IdContext類,通過該類操作資料庫

public class IdDbContext : IdentityDbContext<User, Role, long>
    {
        public IdDbContext(DbContextOptions<IdDbContext> options) : base(options)
        {

        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
        }
    }

(3)在Program.cs中向依賴註入容器中註冊與標識框架相關的服務,並對其選項進行配置(該代碼參考了文章:https://www.cnblogs.com/nullcodeworld/p/16717260.html)

IServiceCollection services = builder.Services;
//對IdDbContext進行配置
services.AddDbContext<IdDbContext>(opt =>
{
    string connStr = builder.Configuration.GetConnectionString("Default");
    Console.WriteLine("字元連接:"+connStr);
    opt.UseSqlServer(connStr);
});
services.AddDataProtection();
//調用AddIdentityCore添加標識框架的一些重要的基礎服務
//(我們沒有調用AddIdentity方法,因為AddIdentity方法實現的初始化
// 比較適合傳統的MVC模式的項目,而現在我們推薦用前後端分離開發模式。)
services.AddIdentityCore<User>(options =>
{
    // 對密碼複雜度苛刻設置
    options.Password.RequireDigit = false;
    options.Password.RequireLowercase = false;
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequireUppercase = false;
    options.Password.RequiredLength = 6;
    options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider;
    options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider;
});
var idBuilder = new IdentityBuilder(typeof(User), typeof(Role), services);
//因為UserManager、RoleManager等服務被創建的時候需要註入非常多的服務,
//所以我們在使用標識框架的時候也需要註入和初始化非常多的服務
idBuilder.AddEntityFrameworkStores<IdDbContext>()
    .AddDefaultTokenProviders()
    .AddRoleManager<RoleManager<Role>>()
    .AddUserManager<UserManager<User>>();

(4)我們不要忘記配置在appsetting.json中配置資料庫連接字元串(這裡我們在連接字元串後面加上了:Encrypt=False;以解決下一步資料庫遷移中出現的報錯:“.....證書鏈是由不受信任的頒發機構頒發的”)

"ConnectionStrings": {
    "Default": "Data Source=.;Database=Identity;User ID=sa;Password=123456;MultipleActiveResultSets=True;Encrypt=False"
  }

(5)在【程式包管理器控制台】中執行命令:Add-Migration Init,再執行Update-Database執行資料庫遷移代碼,如果在這一步中出現錯誤請先仔細檢查以上步驟(可能會提示“No Dbcontext was found in assembly”等錯誤),檢查確定沒有步驟上設置問題,我們新建一個MyDesignTimeDbContextFactory類繼承IDesignTimeDbContextFactory<IdDbContext>,具體代碼如下

class MyDesignTimeDbContextFactory : IDesignTimeDbContextFactory<IdDbContext>
{
    public IdDbContext CreateDbContext(string[] args)
    {
        DbContextOptionsBuilder<IdDbContext> builder = new();
        string connStr = Environment.GetEnvironmentVariable("ConnectionStrings:Default");
        builder.UseSqlServer(connStr);
        return new IdDbContext(builder.Options);
    }
}

到這我們已經完成了標識框架的配置

3、使用JWT實現登錄操作

(1)在配置appsetting.json中創建JWt的密鑰:SigningKey、過期時間:ExpireSeconds兩個配置項;再創建一個對應該節點的配置類JWTOptions

"JWT": {
    "SigningKey": "這裡請自定義輸入一串複雜的密鑰",
    "ExpireSeconds": "86400"
  }
public class JWTOptions
    {
        public string SigningKey { get; set; }
        public int ExpireSeconds { get; set; }
    }

(2)在Program.cs中對JWT進行配置(註意該代碼請添加在builder.Build之前)

//jwt驗證授權
services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT"));//獲取配置文件的JWT的key和過期時間放到JWTOptions類中
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
    var jwtOpt = builder.Configuration.GetSection("JWT").Get<JWTOptions>();
    byte[] keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SigningKey);
    var secKey = new SymmetricSecurityKey(keyBytes);
    x.TokenValidationParameters = new()
    {
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = secKey
    };
});

(3)在Program.cs中的app.UseAuthorization之前添加app.UseAuthentication

app.UseAuthentication();

(4)創建JWTController類,在其中增加登錄並且創建JWT的操作方法(PS:這裡的[FromServices]特性實現對 Controller.Action 單獨註入,當只有單個方法需要該依賴,可以採用這個特性)

[Route("api/[controller]/[action]")]
    [ApiController]
    public class JWTController : ControllerBase
    {
        private readonly UserManager<User> _userManager;

        public JWTController(UserManager<User> userManager)
        {
            _userManager = userManager;
        }
        /// <summary>
        /// 生成token的方法
        /// </summary>
        /// <param name="claims"></param>
        /// <param name="options"></param>
        /// <returns></returns>
        private static string BuildToken(IEnumerable<Claim> claims, JWTOptions options)
        {
            //token到期時間
            DateTime expires = DateTime.Now.AddSeconds(options.ExpireSeconds);
            //取出配置文件的key
            byte[] keyBytes = Encoding.UTF8.GetBytes(options.SigningKey);
            //對稱安全密鑰
            var secKey = new SymmetricSecurityKey(keyBytes);
            //加密證書
            var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature);
            //jwt安全token
            var tokenDescriptor = new JwtSecurityToken(expires: expires, signingCredentials: credentials, claims: claims);

            return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
        }
        /// <summary>
        /// 前端獲取token的介面
        /// </summary>
        /// <param name="req"></param>
        /// <param name="jwtOptions"></param>
        /// <returns></returns>
        [HttpPost]
        public async Task<IActionResult> Login2(LoginRequest req, [FromServices] IOptions<JWTOptions> jwtOptions)
        {
            string userName = req.UserName;
            string password = req.Password;
            var user = await _userManager.FindByNameAsync(userName);
            if (user == null)
            {
                return NotFound($"用戶名不存在{userName}");
            }
            var success = await _userManager.CheckPasswordAsync(user, password);
            if (!success)
            {
                return BadRequest("Failed");
            }
            user.JWTVersion++;//版本號
            await _userManager.UpdateAsync(user);//先把資料庫用戶版本號更新!!!!
            var claims = new List<Claim>();
            claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
            claims.Add(new Claim(ClaimTypes.Name, user.UserName));
            claims.Add(new Claim(ClaimTypes.Version, user.JWTVersion.ToString()));
            var roles = await _userManager.GetRolesAsync(user);
            foreach (string role in roles)
            {
                claims.Add(new Claim(ClaimTypes.Role, role));
            }
            string jwtToken = BuildToken(claims, jwtOptions.Value);
            return Ok(jwtToken);
        }
    }

(5)創建Test2Controller,實現一個測試方法,並且在控制器類上添加[Authorize],這個特性表示該控制器類下所有操作方法都需要登錄後才能訪問,也可以單獨添加在方法上表示該方法需要登錄後訪問,這是很重要的一步

[Route("api/[controller]")]
    [ApiController]
    [Authorize]
    public class Test2Controller : ControllerBase
    {
        [HttpGet]
        public IActionResult Hello()
        {
            string id = this.User.FindFirst(ClaimTypes.NameIdentifier)!.Value;
            string userName = this.User.FindFirst(ClaimTypes.NameIdentifier)!.Value;
            IEnumerable<Claim> roleClaims = this.User.FindAll(ClaimTypes.Role);
            string roleNames = string.Join(',', roleClaims.Select(c => c.Value));
            return Ok($"id={id},userName={userName},roleNames ={roleNames}");
        }
    }

(6)Swagger沒有提供設置自定義HTTP請求報文頭(也就是JWT生成的token)的方式,因此傳遞Authoriation報文介面,我們可以通過對OpenAPI進行配置,使其可以傳遞Authoriation報文,至此也可以使用Postman這種軟體工具調試

builder.Services.AddSwaggerGen(c =>
{
    var scheme = new OpenApiSecurityScheme()
    {
        Description = "Authorization header. \r\nExample: 'Bearer 12345abcdef'",
        Reference = new OpenApiReference
        {
            Type = ReferenceType.SecurityScheme,
            Id = "Authorization"
        },
        Scheme = "oauth2",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
    };
    c.AddSecurityDefinition("Authorization", scheme);
    var requirement = new OpenApiSecurityRequirement();
    requirement[scheme] = new List<string>();
    c.AddSecurityRequirement(requirement);
});

重啟項目,Swagger界面右上角增加了一個【Authorize】按鈕,在對話框中輸入“Bearer 登錄生成獲取的token”(註意:Bearer後面加一個空格再粘貼token)

 

 到這個時候,我們再去請求Test2介面,順利獲取到內容

 

 (7)解決JWT無法提取撤回的問題。我們解決方案思路採用書上的原方案:在用戶表中增加一個整數類型的列JWTVersion,它代表最後次發放出去的令牌的版本號;每次登錄、發放令牌的時候,我們都讓JWTVersion 的值自增,同時將JWTVersion 的值也放到JWT 的負載中當執行禁用用戶、撤回用戶的令牌等操作的時候,我們讓這個用戶對應的JWTVersion的值自增,當伺服器端收到客戶端提交的JWT後,先把JWT中的JWTVersion值和資料庫中的JWTVersion值做比較,如果JWT中JWTVersion的值小於資料庫中JWTVersion的值,就說明這個JWT過期了,這樣我們就實現了JWT的撤回機制。由於我們在用戶表中保存了JWTVersion值,因此這種方案本質上仍然是在伺服器端保存狀態,這是繞不過去的,只不過這種方案是一種缺點比較少的妥協方案。

(在前面的操作中我們已經給User類新增了“JWTVersion”這個版本欄位,在前面“JWTController ”控制器中的“Login2”方法中也完成了方案相應操作)

接下來新增一個操作過濾器JWTValidationFilter並且繼承IAsyncActionFilter,實現對所有操作方法中JWT的檢查操作

public class JWTValidationFilter : IAsyncActionFilter
    {
        private IMemoryCache memCache;
        private UserManager<User> userMgr;

        public JWTValidationFilter(IMemoryCache memCache, UserManager<User> userMgr)
        {
            this.memCache = memCache;
            this.userMgr = userMgr;
        }

        public async Task   OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            var claimUserId = context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
            //對於登錄介面等沒有登錄的,直接跳過
            if (claimUserId == null)
            {
                await next();
                return;
            }
            long userId = long.Parse(claimUserId!.Value);
            string cacheKey = $"JWTValidationFilter.UserInfo.{userId}";
            User user = await memCache.GetOrCreateAsync(cacheKey, async e => {
                e.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(5);
                return await userMgr.FindByIdAsync(userId.ToString());
            });
            if (user == null)
            {
                var result = new ObjectResult($"UserId({userId}) not found");
                result.StatusCode = (int)HttpStatusCode.Unauthorized;
                context.Result = result;
                return;
            }
            //jwt資料庫中保存的版本號
            var claimVersion = context.HttpContext.User.FindFirst(ClaimTypes.Version);            
            long jwtVerOfReq = long.Parse(claimVersion!.Value);
            //由於記憶體緩存等導致的併發問題,
            //假如集群的A伺服器中緩存保存的還是版本為5的數據,但客戶端提交過來的可能已經是版本號為6的數據。因此只要是客戶端提交的版本號>=伺服器上取出來(可能是從Db,也可能是從緩存)的版本號,那麼也是可以的
            if (jwtVerOfReq >= user.JWTVersion)
            {
                await next();
            }
            else
            {
                var result = new ObjectResult($"JWTVersion mismatch");
                result.StatusCode = (int)HttpStatusCode.Unauthorized;
                context.Result = result;
                return;
            }
        }
    }

(8)把過濾器JWTValidationFilter註冊到Program.cs中的全局過濾器中,並且不要忘記註冊記憶體緩存

//過濾器
builder.Services.Configure<MvcOptions>(ops =>
{
    ops.Filters.Add<JWTValidationFilter>();
});

//記憶體緩存
builder.Services.AddMemoryCache();

到這裡基本的使用就完成了,如有錯誤歡迎指正!!

(該代碼參考了文章:https://www.cnblogs.com/nullcodeworld/p/16717260.html)

(參考了書籍《ASP.NET Core 技術內幕與項目實戰——基於DDD與前後端分離》(作者楊中科)的第八章)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 閱讀本文前,需要儲備的知識點如下,點擊鏈接直接跳轉。 [java線程詳解](https://www.cnblogs.com/star95/p/17583193.html) [Java不能操作記憶體?Unsafe瞭解一下](https://www.cnblogs.com/star95/p/1761943 ...
  • 項目架構:Spring5+SpringMVC+Mybatis 項目伺服器:Tomcat 9.0.71 整合SSM啟動時,啟動失敗,Tomcat控制台報錯:Artifact “xxx - xxxx“:war exploded:部署工件時出錯。請參閱伺服器日誌瞭解詳細信息 查看Tomcat日誌:嚴重 [ ...
  • # if if if 判斷 和 if elif elif 判斷有什麼區別 ## 在Python中,if語句和if-elif-else語句都用於條件控制,但它們在處理條件和執行邏輯上有一些區別。 ### if語句:if語句用於執行一系列條件之一的代碼塊。 - 你可以使用多個if語句來檢查多個條件,但每 ...
  • 之前給大家推薦了很多後臺模版,有讀者希望推薦一些跟通用的好看組件,畢竟出了後臺還有很多其他場景嘛。所以,今天繼續給大家推薦一個廣受好評的UI組件庫:[**NextUI**](https://blog.didispace.com/tj-opensource-nextui/) ![NextUI](htt ...
  • 來源:https://heapdump.cn/article/1859160 通過這一個多月的努力,將 FullGC 從 40 次/天優化到近 10 天才觸發一次,而且 YoungGC 的時間也減少了一半以上,這麼大的優化,有必要記錄一下中間的調優過程。 對於 JVM 垃圾回收,之前一直都是處於理論 ...
  • ## 教程簡介 AOP為Aspect Oriented Programming的縮寫,意為:面向切麵編程,通過預編譯方式和運行期間動態代理實現程式功能的統一維護的一種技術。AOP是OOP的延續,是軟體開發中的一個熱點,也是Spring框架中的一個重要內容,是函數式編程的一種衍生範型。利用AOP可以對 ...
  • 上一篇介紹了`DataFrame`的顯示參數,主要是對`DataFrame`中值進行調整。 本篇介紹`DataFrame`的顯示樣式的調整,顯示樣式主要是對錶格本身的調整,比如顏色,通過顏色可以突出顯示重要的值,觀察數據時可以更加高效的獲取主要信息。 下麵介紹一些針對單個數據和批量數據的樣式調整方式 ...
  • 在Java語言中,創建線程並不像創建對象一樣簡單。雖然只需要使用new Thread()即可創建線程,但實際上創建線程比創建對象複雜得多。創建對象只需在JVM的堆中分配記憶體,而創建線程需要調用操作系統內核的API,併為線程分配一系列資源,這個成本相對較高。因此,線程被視為重量級的對象,應儘量避免頻繁... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...