JWT 简介与 C# 示例
〇、什么是 JWT ?
JWT,即 JSON Web Token,是一种基于 JSON 的开放标准(RFC 7519),主要用于在网络应用环境间安全地传递声明。这种声明被进行了数字签名,可以验证和信任,因此,它适用于各种需要信息安全性和无状态的应用。
在具体加密过程中,客户端会使用 RSA 算法生成 JWT 串,这里用到了私钥“加密”,而公钥是公开的,任何人都能解密,但内容无法变更。也就是说,在 JWT 中并没有纯粹的加密过程,而是通过加密保障了信息的完整性和真实性。
适用场景:
- 用户认证:当用户成功登录后,服务器会生成一个 JWT 令牌并返回给客户端,此后客户端只需携带这个令牌即可访问服务器提供的资源。
- 一次性验证:比如,用户注册后发一封邮件让其激活账户,通常邮件中需要有一个链接,这个链接需要具备能够标识用户、具有时效性、不能被篡改以及一次性的特性。这种场景就适合使用 JWT。
- 防止传输数据篡改:即使数据在传输过程中被截获,由于 JWT 可以使用加密算法对传输内容进行签名,因此很难同时篡改签名和传输内容。
优点:
- 更少的数据库连接:因其基于算法来实现身份认证,在使用 JWT 时查询数据的次数更少,可以获得更快的系统响应时间。
- 构建更简单:如果你的应用程序本身是无状态的,那么选择 JWT 可以加快系统构建过程。
- 跨服务调用:可以通过构建一个认证中心,来处理用户身份认证和发放签名的工作,其他应用服务在后续的用户请求中,可使用自有的公钥对用户签名进行验证。
- 无状态:你不需要向传统的 Web 应用那样将用户状态保存于 Session 中。
局限性:
- 安全性:由于 JWT 的 Payload 中负载信息,是使用 base64Url 编码的,并没有加密,因此 JWT 中不能存储敏感数据。
- 一次性:无状态是 JWT 的特点,但也导致了 JWT 是一次性的。想修改里面的内容,就必须签发一个新的 JWT。
- 严重依赖于秘钥:JWT 的生成与解析过程都需要依赖于秘钥(Secret),且都以硬编码的方式存在于系统中或配置里。如果秘钥泄露,系统的安全性将受到严重威胁。
- 服务端无法管理客户端的信息:如果用户身份发生异常(信息泄露或者被攻击),服务端很难将异常用户进行隔离。
- 服务端无法主动推送消息:服务端由于是无状态的,就无法推送消息到客户端。例如过期时间将至,服务端无法主动为用户续约,需要客户端向服务端发起续约请求。
- 冗余的数据开销:一个 JWT 签名的大小要远比一个 Session ID 长很多,如果你对有效载荷(payload)中的数据不做有效控制,其长度会成几何倍数增长,且在每一次请求时都需要负担额外的网络开销。
一、JWT 的组成
下边是一个示例密文 token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6IuW8oOS4iSIsImVtYWlsIjoiemhhbmdzYW5AZXhhbXBsZS5jb20iLCJ0ZW1wa2V5IjoidGVtcHZhbHVl5YC8IiwibmJmIjoxNzAyODg2MjgxLCJleHAiOjE3MDI4ODYyOTEsImlhdCI6MTcwMjg4NjI4MX0.2nuyYrAxVq3aAReN257eMHKGG44j5QyPMabxMnSzVBU
密文起始就是,看起来是非常复杂,实际上有章可循的,如下图,密文以其中的两个句点为分隔,可分为三个部分:Header、Payload、Signature。
1.1 Header 头信息
Header 的主要作用是用来标识。通常是两部分组成:
- typ:type 的简写,令牌类型,也就是 JWT。
- alg:Algorithm 的简写,加密签名算法。
alg 参数 JWT 官网提供了 12 种算法,如下图,但一般都采用 HS256。
明文示例:
{
"alg": "HS256",
"typ": "JWT"
}
经过 Base64Url 编码后,就是密文中第一部分的内容:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
1.2 Payload 有效负载
Payload 是 JWT 密文中的重要组成部分,也可称为 JWT claims,它包含了需要传递的数据信息,解密后的数据格式也是 Json。
claims 可以分为三种类型:registered(预定义声明)、public(公共声明)、private(私有声明)。
registered(预定义声明):不是强制性的,但推荐使用,以提供一组有用的、可互操作的声明。
其中主要包括四个:iss(issuer,发送数据人,标识发送 JWT 主体)、exp(expiration time,数据消息的过期时间,一般采用时间戳格式)、sub(subject,数据消息主题)、aud(audience,数据消息的接收者)。
public(公共声明):公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。一般不建议添加敏感信息,因为该部分在任何客户端均可解密。
private(私有声明):私有声明是提供者和消费者所共同定义的声明。
注:claims 声明名称一般只有三个字符长,因为 JWT 的目的是精简。
示例:
{
"unique_name": "张三",
"email": "zhangsan@example.com",
"tempkey": "tempvalue值"
}
经过 Base64Url 加密后,得到密文的第二部分内容:
eyJ1bmlxdWVfbmFtZSI6IuW8oOS4iSIsImVtYWlsIjoiemhhbmdzYW5AZXhhbXBsZS5jb20iLCJ0ZW1wa2V5IjoidGVtcHZhbHVl5YC8IiwibmJmIjoxNzAyODg2MjgxLCJleHAiOjE3MDI4ODYyOTEsImlhdCI6MTcwMjg4NjI4MX0
1.3 signature 签名信息
Signature 部分是对 Header 和 Payload 两部分的签名,作用是防止 JWT 被篡改。
要创建签名部分,前提是必须获取编码后的 Header、编码后的有效负载 Payload、secret 密钥、标头中 alg 指定的算法,一般为 HS256,然后才能对其进行签名。
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
加密后得到的密文就是 Token 中最后一部分内容。
密钥 secret 是保存在服务端的,服务端会根据这个密钥进行生成 token 和验证,所以需要严格保密。
二、JWT 身份验证的流程简介
JWT 经常用于身份验证流程,以下是一个简单的步骤:
- 用户首次登录,输入账号密码,请求登录接口 /users/login。
- 服务端验证登陆信息,并通过密钥创建 JWT 凭证。
- 服务端返回 JWT 凭证到浏览器,浏览器进行缓存。
- 用户操作触发请求,浏览器会自动将 JWT 凭证加入到请求的 Header 中。
- 服务端接收到请求,先判断 JWT 凭证的有效性。
- 若 JWT 凭证有效,则正常返回的请求结果数据;若校验不通过,则提示用户重新进行身份验证。
另外,在日常业务中,有可能出现并发异常问题。
当服务端在检查到请求的令牌过期之后,会提示用户再次做登录操作。
这在流程上没什么问题,但在页面加载后,如果同一个页面中有多个异步请求同时触发,每一个请求都携带原始令牌,在这样的设计下,就有可能出现在第一个请求到达后刷新了 Token,并更改了缓存中数据的时间戳,以至于剩余请求校验时发现时间戳不一致导致验证失败。
同一时间触发的请求越多,抛出的异常也就越多。虽然第一个请求已经刷新了 Token,但是其余的请求是失败的,页面中的数据并不完整,显然这是不正常的。那如何避免呐?
redis 锁机制:在触发更新 Token 时,将同一用户信息加锁,使得此用户的其他请求均失败,待登录验证通过后再重新加载。
Token 定时刷新:当用户在线时,间隔一段时间刷新一次 Token。要刷新令牌,API 需要一个新的端点,它接收一个有效的,没有过期的 JWT,并返回与新的到期字段相同的签名的 JWT。若用户长时间没登录,则直接跳转到登录页。
三、C# 简单实现
直接看代码吧。
// 测试一下
class Program
{
static void Main(string[] args)
{
JwtTest jwtTest= new JwtTest();
string key = "keayvkkakeyvaluyeaeayvalalujeehayvalguaealrue";
var claims = new[]
{
new Claim(ClaimTypes.Name, "张三"),
new Claim(ClaimTypes.Email, "zhangsan@example.com"),
new Claim("tempkey", "tempvalue值"),
};
string token = jwtTest.JwtEncode(key, claims);
Console.WriteLine(token);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6IuW8oOS4iSIsImVtYWlsIjoiemhhbmdzYW5AZXhhbXBsZS5jb20iLCJ0ZW1wa2V5IjoidGVtcHZhbHVl5YC8IiwibmJmIjoxNzAzNTkxMTk4LCJleHAiOjE3MDM1OTEyMDgsImlhdCI6MTcwMzU5MTE5OH0.65Mx_ldbQijHevkalutHMaejQ06vhe5fW6e6-t-aziw
string json = jwtTest.JwtDecode(token, key);
// {"unique_name":"张三","email":"zhangsan@example.com","tempkey":"tempvalue值","nbf":1703591258,"exp":1703591268,"iat":1703591258}
Console.WriteLine(json);
}
}
验证一下:https://jwt.io/。
// JWT 加密解密类
public class JwtTest
{
public string JwtEncode(string keyvalue, Claim[] claims)
{
var key = Encoding.UTF8.GetBytes(keyvalue);
var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddSeconds(10),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
Console.WriteLine($"加密后的JWT: {tokenString}");
return tokenString;
}
public string JwtDecode(string jwttoken, string publickey)
{
try
{
IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
IJsonSerializer serializer = new JsonNetSerializer();
IDateTimeProvider provider = new UtcDateTimeProvider();
IJwtValidator validator = new JwtValidator(serializer, provider);
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm);
var json = decoder.Decode(jwttoken, publickey, verify: true);
return json;
}
catch(Exception ex)
{
return "";
}
}
}
参考:https://jwt.io/introduction https://juejin.cn/post/7232550589964140602 https://cloud.tencent.com/developer/article/2148676
本文来自博客园,作者:橙子家,欢迎微信扫码关注博主【橙子家czzj】,有任何疑问欢迎沟通,共同成长!