JWT (Json Web Token)原理:
JWT 把登录信息(令牌),保存在客户端,这样可以有效的解决 Session 在分布式集群环境用户认证的问题。
使用服务端自定义的密钥对保存在客户端的令牌进行签名处理,每次服务端接收到客户端提交过来的令牌都需要检查下签名,验证用户身份
JWT 令牌 由三部分组成
头部 Header | 负载 Payload | 签名Signature |
---|---|---|
{“alg”:“hmac-sha256”【算法】, | {“Id”:“6”,“Name”:“admin”, | HMACSHA256(header+“.”+payload, |
“type”:“JWT”} | “exp”:“1688342858”} | secKey) |
(1)其中将用户信息存储到 Payload 中,一般不建议存太多数据,太多数据会加大流量的消耗
(2)在这三部分中,Header 和 Payload 都是明文,在不知道密钥的情况下也是可以将其解析出来的,所以不要将比较重要的信息放到 Payload 中,防止信息泄露。
JWT 令牌 头部、负载、签名 之间分别用 “.” 进行分隔
比如说:我这里的 JWT 令牌为:(其中我在负载中加了个数据 name = “百香果” )
eyJhbGciOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNobWFjLXNoYTI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoi55m-6aaZ5p6cIiwiZXhwIjoxNjk5NzU3NzQ3fQ.QJ6YXJAvhbaL34NotLYt7EkyZDU87Ib_ovaafhPWcN8
而通过下面的代码就可以将其头部和 负载中的信息解析出来
//解析代码
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);
}
//调用
Program program = new Program();
string[] jwtArr = jwt.Split('.');
Console.WriteLine(jwtArr[0]);//eyJhbGciOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNobWFjLXNoYTI1NiIsInR5cCI6IkpXVCJ9
Console.WriteLine(program.JwtDecode(jwtArr[0]));//{"alg":"http://www.w3.org/2001/04/xmldsig-more#hmac-sha256","typ":"JWT"}
Console.WriteLine(jwtArr[1]);//eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoi55m-6aaZ5p6cIiwiZXhwIjoxNjk5NzU3NzQ3fQ
Console.WriteLine(program.JwtDecode(jwtArr[1]));//{"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name":"百香果","exp":1699757747}
Console.WriteLine(jwtArr[2]);//QJ6YXJAvhbaL34NotLYt7EkyZDU87Ib_ovaafhPWcN8
//只有密钥解码不出来
Console.WriteLine(program.JwtDecode(jwtArr[2]));//@??\?/????h??-?I2d5<?????~?p?
(2)在这里就体现出 密钥 不能暴露的重要性,因为校验需要 头部信息 + 负载 + 密钥
三个方面去进行校验,任何一个不对都校验不成功。
如果传过来的令牌用户信息不对,校验信息肯定不通过,如果令牌在生成时使用的密钥与服务器中的不一致,即使用户信息一致,校验也不通过
JWT 原理示意图
一点要注意在服务端定义的密钥不要被客户端知道:然后在通过算法将 header payload 密钥,给结合在一起反馈回去,如果客户端知道了密钥,就可以造假了
--------------------- -------------------服务端--------------------------------------------------------------------------
| | -------传递用户信息过来--------> | 服务端生成JWT:JWT=header.payload(name:admin).签名; |
| | <-------JWT传递给客户端保存----- |签名=签名算法(header+payload+服务端才知道的密钥(secKey)) |
| | --------------------------------------------------------------------------------------------------
| 客户端 | ---客户端再次访问将 JWT 传过来--> |1.算签名=签名算法(用户提交的 jwt 的 header + 用户提交的 jwt 的 payload + 服务器端得出的密钥) |
| | |2.比较“算签名”和用户提交的 JWT 中的签名是否一致,如果不一致,报错。 |
| | |3.从 payload 中取出用户信息 |
--------------------- ----------------------------------------------------------------------------------------------------
生成 JWT 令牌
(1)Nuget: Install-Package System.IdentityModel.Tokens.Jwt
(2)调用下面的代码生成 JWT
//在这里写一下 JWT 生成令牌的代码---------------------,再具体的看文档
//1.生成一个 Claim 集合, 一般来讲,再该集合中不需要加这么多,会耗费流量
List<Claim> claims = new List<Claim>();
//2.在 Claim 集合中加入对应的信息
claims.Add(new Claim(ClaimTypes.Name, "百香果"));
//claims.Add(new Claim(ClaimTypes.Email, "bxg@bxg.com"));
//claims.Add(new Claim(ClaimTypes.NameIdentifier, "888888"));
//claims.Add(new Claim(ClaimTypes.HomePhone, "88888888888"));
可以为其指定多个角色信息
//claims.Add(new Claim(ClaimTypes.Role, "admin"));
//claims.Add(new Claim(ClaimTypes.Role, "manager"));
也可以自定义名字,但是只要解析方通过我们自定义的名字解析即可
//claims.Add(new Claim("QQ", "88888888888"));
//claims.Add(new Claim("WeiChart", "888888888"));
//设置在服务端的密钥:该密钥不要暴露
//这里只是演示,正常情况下,密钥应该保存在配置中,防止别人读取到
string key = "dq1oi314313j1esdf45455dw1sdq534*&8s4q>,__9214";
//设置过期时间,意思是发给你的 jwt 什么时候过期
DateTime expire = DateTime.Now.AddHours(1);//这里设置一小时后过期
//3.剩下的代码是生成 JWT 的
byte[] secBytes = Encoding.UTF8.GetBytes(key);
var secKey = new SymmetricSecurityKey(secBytes);
var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature);
var tokenDescriptor = new JwtSecurityToken(claims: claims,
expires: expire, signingCredentials: credentials);
string jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
校验 JWT 令牌
将 JWT 令牌保存客户端,当再次访问时带着 JWT 令牌一起访问,通过下面的代码进行校验
//这里的 Key 需要和生成 JWT 时的一样,同理需要保存好,最好保存在一个配置文件中,
//生成 JWT 令牌时取该配置文件,校验 JWT 令牌时取该配置文件
string key = "dq1oi314313j1esdf45455dw1sdq534*&8s4q>,__9214";
JwtSecurityTokenHandler tokenHandler = new();
TokenValidationParameters valParam = new();
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
valParam.IssuerSigningKey = securityKey;
valParam.ValidateIssuer = false;
valParam.ValidateAudience = false;
//这里解析如果密钥不对,就会抛异常,如果密钥对了,前面的payload 或者 header 信息被改了,也会抛异常
ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwt,
valParam, out SecurityToken secToken);
//没问题就可以解析出来
foreach (var claim in claimsPrincipal.Claims)
{
Console.WriteLine($"{claim.Type}={claim.Value}");
}
JWT 的优点
JWT 的优点:
1.状态保存在客户端,而非服务器端,天然适合分布式系统
2.签名保证了客户端无法数据造假
3.性能更高,不需要和中心状态服务器通讯(使用 Session 在分布式集群环境中可以通过将数据保存到中心服务器时,校验的时候取,但这样性能不高),纯内存的计算