当前位置: 首页 > 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.dtcms.com/a/260148.html

相关文章:

  • (LeetCode 面试经典 150 题 ) 55. 跳跃游戏 (贪心)
  • 人工智能中的集成学习:从原理到实战
  • 用 PyTorch 构建液态神经网络(LNN):下一代动态深度学习模型
  • 从Yocto中获取源码用传统的方式单独编译
  • 网络分层模型与协议体系技术研究报告
  • 【力扣 中等 C】467. 环绕字符串中唯一的子字符串
  • OBCP第三章 OceanBase SQL 引擎高级技术学习笔记
  • 【C++11】右值引用和移动语义
  • 云效代码仓库导入自建gitlab中
  • 多相机人脸扫描设备如何助力高效打造数字教育孪生体?
  • UE Universal Camera 相机插件-限制镜头在区域内移动
  • 【Unity智能模型系列】MediaPipeUnityPlugin 实现人脸数据获取
  • [按键手机安卓/IOS脚本插件开发] 按键插件调试与判断循环结构辅助工具
  • 【CMake基础入门教程】第七课:查找并使用第三方库(以 find_package() 为核心)
  • 数字孪生技术驱动UI前端变革:从静态展示到动态交互的飞跃
  • 面试150 判断子序列
  • Jenkins执行Jenkinsfile报错
  • 频宽是什么: 0.35/Tr、0.5/Tr?
  • Spring AI Alibaba
  • windows下 tomcat的安装部署
  • 多租户多会话隔离存储架构的完整实现方案
  • GNSS位移监测站在大坝安全中的用处
  • 物联网与低代码:Node-RED如何赋能工业智能化与纵横智控的创新实践
  • Java常用设计模式详解
  • TCP 重传机制详解:原理、变体与故障排查应用
  • Prompt工程解析:从指令模型到推理模型的提示词设计
  • k8s基础概念和组件介绍
  • 【UniApp 日期选择器实现与样式优化实践】
  • 构建数据“高速路”绿算技术亮相数据要素联盟可信数据空间生态交流会,解锁可信数据空间新动能
  • 开启 DMARC 的作用对发件域名来说