在ASP.NET Core WebApi中使用標識框架(Identity)-CSDN博客
因為一般需要和標識框架一起使用,建議先查看標識框架用法
一.為什么需要JWT
我們的系統需要實現認證,即服務端需要知道登錄進來的客戶端的身份,管理員有管理員的權限,普通用戶有普通用戶的權限.
但服務端是基于HTTP協議的,該協議本質上是無狀態,兩次請求本質上是獨立的,也就是說該協議無法幫我們實現認證.
1、傳統的 Session 認證機制
早期認證的實現方式是Session,流程大概如下:
(1)用戶登錄,服務端驗證用戶名密碼成功后,生成一個唯一的 SessionId。
(2)這個 SessionId 存在服務端內存(或者數據庫、Redis)里,對應一個用戶狀態。
(3)服務端通過 Set-Cookie
把 SessionId 寫入瀏覽器 Cookie。
(4)瀏覽器后續請求自動攜帶 Cookie,服務端用這個 SessionId 找到用戶信息。
Session 的缺點:
問題 | 說明 |
---|---|
服務器內存壓力 | 每登錄一個用戶,服務器都要保存一份 Session 數據,用戶多了就容易撐爆內存 |
不適合分布式 | 多臺服務器集群部署時,Session 要么共享存儲(如 Redis),要么做 Session 粘性路由,增加系統復雜度 |
跨域難處理 | 前后端分離、跨域 API 調用時,Cookie 不好用或者需要復雜的 CORS 配置 |
狀態管理復雜 | 如果 Session 丟失、超時、清理,用戶體驗會很差,需要額外處理 |
2、JWT 的出現:無狀態化認證?
隨著微服務、云原生、前后端分離等架構興起,開發者開始追求一種 「無狀態」且「輕量級」 的認證方案,JWT 應運而生。
3、JWT 對比 Session 的核心區別
對比點 | Session | JWT |
---|---|---|
狀態管理 | 服務端有狀態,需要存儲每個用戶 Session | 完全無狀態,Token 自包含用戶信息 |
存儲位置 | 服務端內存/數據庫 | 客戶端自行保存(通常存在本地存儲或 Cookie) |
跨服務 | 需要共享 Session 或做負載均衡粘性 | 天然支持多服務,無需 Session 同步 |
擴展性 | 橫向擴展困難 | 服務端可任意擴容 |
性能 | 每次請求都查找 Session | 不需要查 Session,Token 自解密驗證 |
4、JWT 的工作流程概覽(無狀態認證)?
(1)用戶登錄,后端生成 JWT 返回給前端。
(2)前端保存好 JWT
(3)每次 API 請求,前端把 JWT 放到 Authorization: Bearer
頭里。
(4)后端中間件解析 JWT,驗簽,通過后即可認為該用戶已登錄。
服務端只負責「驗簽 + 解密」,不保存任何 Session 狀態。
5、JWT 的優點
優點 | 說明 |
---|---|
跨服務、跨平臺 | 多服務架構天然支持,移動 App、Web 前端、第三方系統都可以用同一個 Token |
減少服務器壓力 | 服務端無需保存登錄狀態 |
性能高 | 每次只需做一次 Token 驗證,無需 Session 查詢 |
易與 CDN、API 網關等集成 | 請求攜帶 Token,網關層即可完成鑒權 |
標準化 | 基于開放標準 RFC7519,廣泛支持,工具鏈成熟 |
6、為什么現在很多新項目都選擇 JWT??
-
適合微服務
-
適合前后端分離
-
適合跨平臺 App
-
適合無狀態、彈性伸縮的云架構
7、JWT 取代 Session,不是因為它絕對更好,而是因為它更「適應當代架構」
JWT 并不是完美無缺,它也有一些缺點,比如:
缺點 | 說明 |
---|---|
Token 無法主動失效 | 如果用戶登出或者權限變更,老 Token 依然有效(可通過 Token 黑名單、Token 版本號等方式繞過) |
容易被盜用 | 如果 Token 泄露,別人拿到 Token 就可以冒充用戶 |
Token 較長 | JWT 體積大,不適合非常高頻短連接場景 |
二.什么是 JWT?
JWT,全稱 JSON Web Token,是一種開放標準(RFC 7519),用于在不同系統之間安全地傳輸信息。它是一種基于 JSON 格式、經過數字簽名的數據令牌,主要應用于 身份認證 和 信息交換 場景。
1.JWT 的核心用途
-
身份認證(Authentication)
用戶登錄成功后,服務器生成一個包含用戶身份信息的 JWT,返回給客戶端。客戶端后續每次請求,都帶上這個 Token,服務器通過驗證 Token,確認用戶身份,無需重復登錄。 -
信息交換(Information Exchange)
系統之間可以通過 JWT 安全地交換一些加密或不可篡改的聲明信息。
2.JWT 的結構:三段式組成
一個典型的 JWT 長這樣(這是被算法處理過的):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6ImFkbWluIiwicm9sZSI6IkFkbWluIn0.NhJzHfJZKIo0FPWqGk92OukUjD0YPgXVyknzZoAW_2Y
?
被點號分隔成三段:?
(1) Header(頭部)
指定 Token 的類型(通常是 JWT)以及簽名所用的算法,比如 HS256
。
{"alg": "HS256","typ": "JWT"
}
(2) Payload(負載)
放具體的聲明信息(Claims),比如用戶 ID、用戶名、角色、過期時間等。
{"userName": "admin","role": "Admin","exp": 1719820800
}
(3) Signature(簽名)
防止篡改。由 Header、Payload 和一個 Secret 密鑰(只有服務端知道),通過指定算法生成。
簽名生成方式(以 HMAC-SHA256 為例):
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret
)
三.控制臺使用
1.環境搭建
先創建一個控制臺程序生成JWT,需要安裝JWT讀寫的NuGet包
System.IdentityModel.Tokens.Jwt
?2.生成JWT
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
static void Main(string[] args)
{// 創建用戶的 Claims 列表// Claim就代表一條用戶信息。Claim有兩個主要的屬性:Type和Value,它們都是string類型的,Type代表用戶信息的類型,Value代表用戶信息的值。// Type屬性可以是預定義的類型,如ClaimTypes.Name、ClaimTypes.Role等,也可以是自定義的類型。var claims = new List<Claim>();// 用戶唯一標識,比如用戶ID,這里用"6"做示例claims.Add(new Claim(ClaimTypes.NameIdentifier, "6"));// 用戶姓名,這里是 "ZhangSan"claims.Add(new Claim(ClaimTypes.Name, "ZhangSan"));// 用戶角色,注意:可以有多個角色聲明claims.Add(new Claim(ClaimTypes.Role, "User"));claims.Add(new Claim(ClaimTypes.Role, "Admin"));// 自定義 Claim,比如擴展字段,這里自定義了一個 "jz" 字段claims.Add(new Claim("jz", "112233"));// 定義密鑰字符串,生產環境一般放在配置文件,不要硬編碼string key = "kjdfsjffd^kjfkfkds#dsffdsdsfd@fdsufdsfo33300";// 設置 Token 的過期時間,這里是 1 天后過期DateTime expires = DateTime.Now.AddDays(1);// 把密鑰字符串轉成字節數組byte[] secBytes = Encoding.UTF8.GetBytes(key);// 根據密鑰生成對稱加密安全密鑰對象var secKey = new SymmetricSecurityKey(secBytes);// 指定簽名算法,這里使用 HMAC-SHA256var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature);// 創建 JWT Token 對象,包括:claims、過期時間、簽名憑據var tokenDescriptor = new JwtSecurityToken(claims: claims, // 載荷:用戶身份信息expires: expires, // 有效期signingCredentials: credentials // 簽名信息);// 把 JwtSecurityToken 對象序列化成最終的 Token 字符串string jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);// 輸出 TokenConsole.WriteLine(jwt);
}
eyJhbGciOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNobWFjLXNoYTI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjYiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiWmhhbmdTYW4iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOlsiVXNlciIsIkFkbWluIl0sImp6IjoiMTEyMjMzIiwiZXhwIjoxNzUwOTg5ODMwfQ.7sl9Y18uxGU-Xd9Ly3rfXKnKidBJ_ZZjPyZOwnTR_0c
3.JWT解析
Header 和 Payload 是明文的,只是做了 Base64Url 編碼,不是加密
比如一個原始 Payload:
{"userName": "admin","role": "Admin","exp": 1719820800
}
?Base64Url 編碼后就是一串字母數字:
eyJ1c2VyTmFtZSI6ImFkbWluIiwicm9sZSI6IkFkbWluIiwiZXhwIjoxNzE5ODIwODAwfQ
?JWT 三部分都是明文(Header 和 Payload 可直接 Base64Url 解碼),JWT 的重點是防篡改而不是保密
所以不難理解,別人拿到你的JWT就可以冒充你.
jwt在線解密/加密 - JSON中文網json中文網致力于在中國推廣json,json Web Tokens 是目前流行的跨域認證解決方案,json中文網提供jwt解密/加密工具,提供HS256、HS384和HS512等簽名算法的編碼和校驗。https://www.json.cn/jwt?可以將得到的JWT直接放到jwt解析網站上就能解析出前兩部分的信息.
或者使用下面這個方法
string jwt = Console.ReadLine()!;
string[] segments = jwt.Split('.');
string head = JwtDecode(segments[0]); // 頭部
string payload = JwtDecode(segments[1]); // 負載
Console.WriteLine("--------head--------");
Console.WriteLine(head);
Console.WriteLine("--------payload--------");
Console.WriteLine(payload);string JwtDecode(string s)
{s = s.Replace('-', '+').Replace('_', '/');switch (s.Length % 4){case 2:s += "==";break;case 3:s += "=";break;}var bytes = Convert.FromBase64String(s); // 解碼return Encoding.UTF8.GetString(bytes);
}
?可以看到信息被解析出來了,由于JWT會被發送到客戶端,而負載中的內容是以明文形式保存的,因此一定不要把不能被客戶端知道的信息放到負載中。
JWT的編碼和解碼規則都是公開的,而且負載部分的Claim信息也是明文的,因此惡意攻擊者可以對負載部分中的用戶ID等信息進行修改,從而冒充其他用戶的身份來訪問服務器上的資源。因此,服務器端需要對簽名部分進行校驗,從而檢查JWT是否被篡改了。
// 從控制臺讀取用戶輸入的 JWT 字符串
string jwt = Console.ReadLine()!; // 注意:加了 "!" 是為了告訴編譯器:這里不會是 null// 定義密鑰字符串(要與生成 JWT 時用的密鑰保持一致,否則驗證會失敗)
string secKey = "kjdfsjffd^kjfkfkds#dsffdsdsfd@fdsufdsfo33300";// 創建一個 JWT Token 解析器
JwtSecurityTokenHandler tokenHandler = new();// 定義 Token 驗證參數
TokenValidationParameters valParam = new();// 設置簽名驗證的密鑰,必須和生成時一致
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secKey));
valParam.IssuerSigningKey = securityKey;// 不驗證簽發者 (Issuer),這里簡化處理(生產環境可以啟用校驗)
valParam.ValidateIssuer = false;// 不驗證接收者 (Audience),同樣為了簡化
valParam.ValidateAudience = false;// 開始驗證 Token
// ValidateToken 方法會做:
// 1. 驗證簽名
// 2. 驗證 Token 是否過期
// 3. 返回解析后的 ClaimsPrincipal 對象(包含用戶身份信息)
// out 參數會返回原始的 SecurityToken 對象
ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwt, valParam, out SecurityToken secToken);// 遍歷解析出來的 Claim 列表,并輸出每個 Claim 的類型和值
foreach (var claim in claimsPrincipal.Claims)
{Console.WriteLine($"{claim.Type}={claim.Value}");
}
如果篡改JWT,程序運行時就會拋出內容為“Signature validation failed”的異常。exp值是過期時間,如果收到過期的JWT,即使簽名校驗成功,ValidateToken方法也會拋出異常
四.WebApi中使用
1.環境準備
"JWT": {"SigningKey": "kjdfsjffd^kjfkfkds#dsffdsdsfd@fdsufdsfo33300EXTRA","ExpireSeconds": "3600"}
public class JwtSetting{public string SigningKey { get; set; }public int ExpireSeconds { get; set; }}
我們先在配置系統appsettings.json
中配置一個名字為JWT的節點,并在節點下創建SigningKey、ExpireSeconds兩個配置項,分別代表JWT的密鑰和過期時間(單位為秒)。
我們再創建一個對應JWT節點的配置類JwtSetting,類中包含SigningKey、ExpireSeconds這兩個屬性。
安裝Microsoft.AspNetCore.Authentication.JwtBearer
包,這個包封裝了簡化ASP.NET Core中使用JWT的操作
?2.注冊服務
// 將配置文件中的 JWT 部分綁定到 JwtSetting 配置類
builder.Services.Configure<JwtSetting>(builder.Configuration.GetSection("JWT"));// 注冊 JWT Bearer 身份認證服務
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(x =>{// 從配置中讀取 JWT 設置對象(比如密鑰等信息)var jwtOpt = builder.Configuration.GetSection("JWT").Get<JwtSetting>();// 把密鑰字符串轉為字節數組byte[] keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SigningKey);// 用密鑰生成對稱安全密鑰對象var secKey = new SymmetricSecurityKey(keyBytes);// 配置 Token 驗證參數x.TokenValidationParameters = new TokenValidationParameters(){// 是否驗證 Token 的簽發者(Issuer),這里關閉ValidateIssuer = false,// 是否驗證 Token 的接收方(Audience),這里關閉ValidateAudience = false,// 是否驗證 Token 的過期時間,生產環境一般要打開,這里關閉是為了開發方便ValidateLifetime = false,// 是否驗證 Token 的簽名,生產環境一定要開ValidateIssuerSigningKey = true,// 用來驗證簽名的密鑰IssuerSigningKey = secKey};});
?本質上就是中間件,別忘了使用.
?3.給登錄用戶發JWT
// 控制器:負責處理用戶登錄請求,并生成 JWT Token
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{private readonly IOptions<JwtSetting> _jwtSetting; // JWT 配置信息private readonly ILogger<AuthController > _logger;private readonly UserManager<User> _userManager;private readonly RoleManager<Role> _roleManager;public AuthController(ILogger<AuthController > logger, UserManager<User> userManager,RoleManager<Role> roleManager, IOptions<JwtSetting> jwtSetting){_logger = logger;_userManager = userManager;_roleManager = roleManager;_jwtSetting = jwtSetting;}// 登錄接口,接收用戶名密碼,驗證成功后生成 JWT[HttpPost]public async Task<IActionResult> Login(LoginRequest loginRequest){string userName = loginRequest.UserName;string password = loginRequest.Password;// 使用 Identity 框架查找用戶var user = await _userManager.FindByNameAsync(userName);if (user == null){return BadRequest("用戶不存在");}// 判斷用戶是否被鎖定(連續登錄失敗導致)var islocked = await _userManager.IsLockedOutAsync(user);if (islocked){// 用戶鎖定,返回 400,提示鎖定信息return BadRequest("用戶已鎖定!");}// 校驗密碼var success = await _userManager.CheckPasswordAsync(user, password);if (!success){// 密碼錯誤,記錄一次失敗嘗試(用于鎖定機制)var r = await _userManager.AccessFailedAsync(user);if (!r.Succeeded){// 記錄失敗信息失敗,返回錯誤return BadRequest("訪問失敗信息寫入錯誤!");}else{// 普通密碼錯誤返回 400return BadRequest("失敗!");} }//重置訪問失敗計數await _userManager.ResetAccessFailedCountAsync(user);// 構建 JWT Claims(載荷里的用戶信息)var claims = new List<Claim>{// 用戶唯一標識new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),// 用戶名new Claim(ClaimTypes.Name, user.UserName)};// 查詢用戶角色,并把每個角色加入到 Claimsvar roles = await _userManager.GetRolesAsync(user);foreach (var role in roles){claims.Add(new Claim(ClaimTypes.Role, role));}// 調用封裝好的 Token 構建方法,生成 JWT 字符串string jwtToken = BuildToken(claims, _jwtSetting.Value);// 把 Token 返回給前端return Ok(jwtToken);}/// <summary>/// 根據用戶 Claims 和 JWT 配置,生成 JWT Token 字符串/// </summary>private static string BuildToken(IEnumerable<Claim> claims, JwtSetting _jwtSetting){// 設置 Token 過期時間DateTime expires = DateTime.Now.AddSeconds(_jwtSetting.ExpireSeconds);// 根據配置的密鑰生成安全密鑰對象byte[] keyBytes = Encoding.UTF8.GetBytes(_jwtSetting.SigningKey);var secKey = new SymmetricSecurityKey(keyBytes);// 指定簽名算法,這里用 HMAC-SHA256var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature);// 創建 Token 對象,包括過期時間、簽名憑據、Claimsvar tokenDescriptor = new JwtSecurityToken(expires: expires,signingCredentials: credentials,claims: claims);// 序列化成最終 Token 字符串return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);}
}
?4.接口校驗JWT
[Route("[controller]/[action]")][ApiController][Authorize] // 表示:訪問此控制器下的所有 Action,都必須登錄并攜帶有效 JWTpublic class UserInfoController : ControllerBase{/// <summary>/// 測試用接口:返回當前登錄用戶的身份信息(從 JWT Claims 解析)/// </summary>[HttpGet]public IActionResult Hello(){// 從 Claims 中獲取用戶IDstring id = this.User.FindFirst(ClaimTypes.NameIdentifier)!.Value;// 獲取用戶名string userName = this.User.FindFirst(ClaimTypes.Name)!.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}");}}
添加的[Authorize]
表示這個控制器類下所有的操作方法都需要登錄后才能訪問。
ControllerBase中定義的ClaimsPrincipal
類型的User屬性代表當前登錄用戶的身份信息,我們可以通過ClaimsPrincipal的Claims屬性獲得當前登錄用戶的所有Claim信息,不過我們一般通過FindFirst
方法根據Claim的類型來查找需要的Claim,如果用戶身份信息中含有多個同類型的Claim,我們則可以通過FindAll
方法來找到所有Claim。
5.swagger調試
直接訪問401無權限 ,我們需要傳入jwt才能訪問該接口.
ASP.NET Core要求(這也是HTTP的規范)JWT放到名字為Authorization的HTTP請求報文頭中,報文頭的值為“Bearer JWT”。
Swagger中默認沒有提供設置自定義HTTP請求報文頭的方式,因此對于需要傳遞Authorization報文頭的接口,調試起來很麻煩。我們可以通過對OpenAPI進行配置,從而讓Swagger中可以發送Authorization報文頭。
// 注冊 Swagger 服務,同時配置 JWT 認證支持builder.Services.AddSwaggerGen(c =>{// 定義一個 OpenApiSecurityScheme:告訴 Swagger,這里有一個全局的 Header 參數叫 Authorizationvar scheme = new OpenApiSecurityScheme(){// Swagger UI 上顯示的描述信息,告訴開發者怎么填寫 TokenDescription = "在請求頭中加入 Authorization 字段,例如:'Bearer 12345abcdef'",// 給這個 SecurityScheme 起一個引用ID,后面配置用Reference = new OpenApiReference{Type = ReferenceType.SecurityScheme,Id = "Authorization"},// Scheme 字段,這里用 "oauth2" 字符串(其實可以寫任何字符串,Swagger 不校驗這個)Scheme = "oauth2",// 參數名,Swagger UI 會自動生成這個 Header 字段Name = "Authorization",// 參數的位置:在 HTTP Header 中In = ParameterLocation.Header,// 聲明類型是 API Key(Swagger 把 "Authorization" 這種 Header 參數用 ApiKey 類型)Type = SecuritySchemeType.ApiKey,};// 添加這個 Security 定義,名稱叫 "Authorization",Swagger UI 會顯示一個輸入框c.AddSecurityDefinition("Authorization", scheme);// 創建一個全局安全要求:告訴 Swagger,每個接口默認都要帶這個 SecuritySchemevar requirement = new OpenApiSecurityRequirement();// 給這個 requirement 加上剛才定義的 scheme,值是空列表(Swagger 需要這么寫)requirement[scheme] = new List<string>();// 把這個全局安全要求加到 Swagger 配置里c.AddSecurityRequirement(requirement);});
首先我們要先利用前面的登錄接口獲取一個JWT,然后通過這個按鈕將JWT傳入,此時你就可以訪問那些需要認證的接口了.
?