当前位置: 首页 > news >正文

ASP.NET Core Web API 实现 JWT 身份验证

在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 的核心区别

对比点SessionJWT
状态管理服务端有状态,需要存储每个用户 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 的核心用途

  1. 身份认证(Authentication)
    用户登录成功后,服务器生成一个包含用户身份信息的 JWT,返回给客户端。客户端后续每次请求,都带上这个 Token,服务器通过验证 Token,确认用户身份,无需重复登录。

  2. 信息交换(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传入,此时你就可以访问那些需要认证的接口了.

 

http://www.lqws.cn/news/528607.html

相关文章:

  • 【人工智能与机器人研究】基于ROS的多传感器融合巡检机器人系统研究
  • Android 16系统源码_无障碍辅助(二)Android 的无障碍框架
  • 人工智能中的集成学习:从原理到实战
  • PDF Kit 使用示例(HarmonyOS)
  • 跟着AI学习C#之项目实战-电商平台 Day1
  • Web3解读:解锁去中心化网络的潜力
  • MessagesPlaceholder和多轮AI翻译助手实战
  • 【强化学习】《Reinforcement Learning: An Introduction》(第二版)概述
  • 杰理-可视化sdk-耳机灯效添加
  • Windows中使用createdump创建进程dump文件的基本用法
  • 开疆智能CCLinkIE转ModbusTCP网关连接PCA3200电能表配置案例
  • 人工智能编程三大核心流程详解--机器学习、神经网络、NLP自然语言处理
  • SQL Server 如何实现高可用和读写分离技术架构
  • SQL学习笔记3
  • AI矢量图与视频无痕修复:用Illustrator与After Effects解锁创作新维度
  • Android14音频子系统-Framework分析
  • Python 常用正则表达式大全
  • Spark 之 QueryStage
  • [Java实战]springboot3使用JDK21虚拟线程(四十)
  • 第十三章---软件工程过程管理
  • 【LLM】位置编码
  • vscode 回退代码版本
  • SQL变量声明与赋值 分支 循环
  • 信创国产化替代中的开发语言选择分析
  • 4.2_1朴素模式匹配算法
  • 6月份最新代发考试战报:思科华为HCIP HCSE 考试通过
  • Java四种拷贝方式总结!一文扫清所有拷贝问题
  • npm run dev报错
  • 软件安装——下载安装ollama
  • leetcode 65