本文内容:
-
JWT Token
-
JWT Token颁发
-
JWT 签名验证
-
JWT 安全检验
Token 原理
打开 https://jwt.io/ ,进入后会看到默认页面有一段自动生成的 token,右边是关于这个 token 的信息。
可以看到默认有一段 token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.EJzdDeNLOQhy-2WmXuK1B49xF17Tk0pja1tCPp81YjY
左边是 token 示例,右侧是解析 token 之后的结构,因为 token 是明文存储信息,所以不需要密钥也可以解析出来,所以后面会提到为什么需要签名检验。
右侧可以看到 token 解析之后分为三个部分:HEADER
、PAYLOAD
、VERIFY SIGNATURE
。
HEADER 部分记载签名方式以及颁发 token 的类型:
{
"alg": "HS256",
"typ": "JWT"
}
只有两个字段,存储了两个信息 ALGORITHM (加密方式)、 TOKEN TYPE。
PAYLOAD 部分记录用户信息,这里是明文的:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
payload 是数据区,存储一些自定义信息,比如 token 有效期、当前用户姓名、用户 id、token 是哪个程序颁发等,这里完全是自定义的。
token 并不规定数据区必须有哪些字段,但是 token 定义了一系列的常用字段使用缩写表示,如 sub(subjectd 订阅者)、name(token 拥有者)、iat(过期时间),其他缩写字段在 C# 的 Claim 类型中有定义。
VERIFY SIGNATURE 部分:
token 的签名,首先生成前面两部分后,使用密钥对内容进行 PAYLOAD 签名,生成第三部分。
注意,签名不是加密。
以常见的 HMACSHA256 算法为例。
token 的加密方法如下所示:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
这里使用密钥 abcd$a1aacd1213ccccccccc13113aaaaa
和 HMACSHA256 模拟这个过程。
首先将 header 和 payload 各种使用 base64 编码,然后连接起来:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImFhYSIsIm5iZiI6MTczNTYzMTAyOSwiZXhwIjoxNzQ0MjcxMDI5LCJpYXQiOjE3MzU2MzEwMjksImlzcyI6Imh0dHA6Ly8xOTIuMTY4LjYuNjo2NjYiLCJhdWQiOiJhYWEifQ
经过签名之后生成:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImFhYSIsIm5iZiI6MTczNTYzMTAyOSwiZXhwIjoxNzQ0MjcxMDI5LCJpYXQiOjE3MzU2MzEwMjksImlzcyI6Imh0dHA6Ly8xOTIuMTY4LjYuNjo2NjYiLCJhdWQiOiJhYWEifQ.a_HxBRItLSrXCKAIWzjKHVLSHTz-92C0qVFElls-M1w
把这个 token 复制到页面解析,会发现解析出内容,但是签名经验不通过。
如果在这个位置填写签名的密钥,则会显示经验通过。
JWT 颁发 和签名检查
大多数情况下,大家使用的 C# 生成 JWT Token 代码是这样的:
// 定义用户信息
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, userName)
};
SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenOption.SecurityKey));
JwtSecurityToken token = new JwtSecurityToken(
issuer: userName,
audience: "http://192.168.6.6:666",
claims: claims,
notBefore: DateTime.Now,
expires: DateTime.Now.AddDays(1),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
);
但是这样生成的 Token 属于不安全的 Token,没有被签名。
为了让 Token 更加安全,可以这样:
// 定义用户信息
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, userName)
};
SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenOption.SecurityKey));
SecurityToken securityToken = new JwtSecurityTokenHandler().CreateToken(new SecurityTokenDescriptor
{
Claims = claims.ToDictionary(x => x.Type, x => (object)x.Value),
Issuer = "http://192.168.6.6:666",
Audience = userName,
NotBefore = DateTime.Now,
Expires = DateTime.Now.AddDays(100),
SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
});
var indf = securityToken.ToString();
var jwtToken = new JwtSecurityTokenHandler().WriteToken(securityToken);
return jwtToken;
另外检查 Token 的代码可以这样写:
if (string.IsNullOrWhiteSpace(token)) return false;
if (!token.StartsWith("Bearer ")) return false;
var newToken = token[7..];
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
if (!jwtSecurityTokenHandler.CanReadToken(newToken)) return false;
var checkResult = await jwtSecurityTokenHandler.ValidateTokenAsync(newToken, new TokenValidationParameters()
{
RequireExpirationTime = true,
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenOption.SecurityKey)),
});
if (!checkResult.IsValid) return false;
var jwt = jwtSecurityTokenHandler.ReadJwtToken(newToken);
IEnumerable<Claim> claims = jwt.Claims;
Jwt token 安全检查
前面的都是常规写法,在分布式系统可能不适合。
对 Jwt token 的签名加密一般有两种方式,一种是对称加密,一种是非对称加密。
其中对称加密又称为共享密钥。
在前面例子中生成的 token 方式就是对称加密方式:
SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenOption.SecurityKey))
SymmetricSecurityKey 是 C# 中对称加密的基类。
这个用于对 token 后面做签名和检查。
但是在分布式下有个问题,用户获得登录中心颁发的 token 后,请求内部的其中一个系统 A。
请问 A 系统怎么判断这个 token 不是有效的?怎么验证签名?
如果使用共享密钥(对称加密)的方式,每个子系统都需要知道密钥是什么,这样很容易导致密钥泄露。
或者换个方式,每个请求,子系统都要向登录中心判断 token 是否有效,但是这样对网络不友好,以及会带来其它问题。
所以这里又出现了非对称加密方式,以 RSA 为例,登录中心保存唯一的私钥,每个子系统只有公钥。
这样一来,子系统不能颁发 token,但是可以检查 token 是否有效。
首先生成 RSA 非对称加密的私钥和密钥:
如果对 RSA 感兴趣,可以参考笔者的另一篇文章,从里面复制一个 RsaHelper 帮助类:
void Main()
{
using (RSA rsa = RSA.Create())
{
rsa.ExportPkcs8PrivateKeyPem().Dump();
string publicKeyPem = "-----BEGIN PUBLIC KEY-----\n" +
Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo(), Base64FormattingOptions.InsertLineBreaks) +
"\n-----END PUBLIC KEY-----";
publicKeyPem.Dump();
}
}
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDS+kP8X+q9qJdu
Rh3sft2WlxZZee4IIA+JXQKic8HSNEOTPB8ZFu8mjnxy16gebQDyOuMF3M00+wip
LLNSfYqQU3yLe/EcKgD9DqBps5CvSmv3PgyKrYgJNlXVcbUq9zGhfZ2dEJTWALhs
tHfMH7+GZEKtcah/sEm6m+Us4W49sIjjeAWrRQl9Fq8+288uoIg4tpYNBGRVi90R
uPnf8a3manbEPHwlPX9rIeskkXRpwoftfHegeK/1EReyuE86co17SJePzwHxiHEv
Ro4UoOkij49IIhiVbmksBlzN4XEgl3YXj0Gtshc5X8Ce6SccNkFoi3KRax/KWmuM
wG8dpT9NAgMBAAECggEBAJfsofJlu6sxcUJ2eWvo+3ZKfEyYceEl/SokcRY8l1Dg
U9z9iUNO8Y3pQxKL20N1qR3Fa9+33YmOT/FLACKhxpshk2j6KmjmkmmoE7mqFcE0
rUSQSQW/6lr/5pVaWWSENxgcVdhZrWPhhuy4lB/IqOmE30L4uqagcqdPRZupBfKj
EjMQzNhV9OwhjhPPPDq9Fek4tdjdPm8wPuQuiDmOmIsB29bRSpwrgzp31kND4jw0
4DCb8TaoJe90F/f1JX5zAHeeToBRHsJFuO2ICLMtNmJWTD1uZIup7vQAivW0xf9F
/oQE+cXsjc5GZmQaGUr03LtWKJBbD8Hc+MsOZynMk50CgYEA1WdmnZ9/7jn1sW3J
h80b10fQxM0tOdP0NySXdP+LShhqCvELzQZgg3aGww5KVR9Zax+LVmsM4GM90f6c
WrAspoNjOQXZxZeOmg71sP6tOKKaSaf0oqUxT3kXeFL5/ubo/hQ6cVyP7qgvojPM
Vc63YIdmLWwFl1aXNxrHbPUzmxcCgYEA/RbiWV3PyTc2yT7cvQzQAd4yLDiaijK0
gvwLzWx9UDrOBGs5DULF4QMOgsCK0rerwvi4FLOuPIdqWqW7ysX+lhn5X9OZGYHv
lNp0GaIcid/KJNBs5TNi6VWMEY0a1v8ic68lMsRvf1xQXTMBA8RQUkR1YrT90LlB
95vhrobgJzsCgYEAzSpm9n1kwgTJGHbjfQMNlDCAHuTfaSxEK0urrRkNsgO8154c
6VULLvih4R95CVNlZV7jWAb9TzE6OwzdBzc/BitlFmpwjs4BlE1zmmGO6dcyHEQ0
JrZIrQ5PKSglHxKix7ts4JXL7veVLA0+kvR1SoGCE4M58OCX6qt9NVyb66sCgYEA
ozrLKZATn1b5ArqEa3mD/nBsM5EeOtuRCJm+kvLRr5j9nmP5G9BhB0qNZU8BOf4z
zT/UmaV5TpiXw3b4s0MXe3+tElzKdWUUPBDYqF+hwFqRaUTztq95r7v45qj3Eori
kXH4r9F5h87mFfX7RY6rryNwAgVxXdjd7vCekY1zrFkCgYAYFqf1ywcnAN8+Sfmj
wRavUjG0T4SSaDjwDpP1dv1B6+WdX32S+2sQHyHUE41GCnhJxFghJqY+2HMrxUz+
tzgmicLGpTrLmXFomz0XhjrJyFdvh0Y0gQChK+14hr9ZluO/a3cgARHnHY20njkl
6BA30QaaarVkSR/NMa1gDEU86Q==
-----END PRIVATE KEY-----
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0vpD/F/qvaiXbkYd7H7dlpcWWXnuCCAP
iV0ConPB0jRDkzwfGRbvJo58cteoHm0A8jrjBdzNNPsIqSyzUn2KkFN8i3vxHCoA/Q6gabOQr0pr
9z4Miq2ICTZV1XG1KvcxoX2dnRCU1gC4bLR3zB+/hmRCrXGof7BJupvlLOFuPbCI43gFq0UJfRav
PtvPLqCIOLaWDQRkVYvdEbj53/Gt5mp2xDx8JT1/ayHrJJF0acKH7Xx3oHiv9REXsrhPOnKNe0iX
j88B8YhxL0aOFKDpIo+PSCIYlW5pLAZczeFxIJd2F49BrbIXOV/AnuknHDZBaItykWsfylprjMBv
HaU/TQIDAQAB
-----END PUBLIC KEY-----
将公钥密钥放到目录下面:
导入私钥和公钥:
var privateKey = RSA.Create();
privateKey.ImportFromPem(File.ReadAllText("./private.key"));
var publicKey = RSA.Create();
publicKey.ImportPublichKeyPck8(File.ReadAllText("./public.key"));
生成 RSA 非对称的 token:
var token = GetToken(privateKey, "aaa");
static string GetToken(RSA rsa,string userName)
{
var rsaKey = new RsaSecurityKey(rsa);
// 定义用户信息
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, userName)
};
SecurityToken securityToken = new JwtSecurityTokenHandler().CreateToken(new SecurityTokenDescriptor
{
Claims = claims.ToDictionary(x => x.Type, x => (object)x.Value),
Issuer = "http://192.168.6.6:666",
Audience = userName,
NotBefore = DateTime.Now,
Expires = DateTime.Now.AddDays(100),
SigningCredentials = new SigningCredentials(rsaKey, SecurityAlgorithms.RsaSha256)
});
var indf = securityToken.ToString();
var jwtToken = new JwtSecurityTokenHandler().WriteToken(securityToken);
return jwtToken;
}
使用公钥检查 token:
var userinfos = await CheckToken(publicKey, "Bearer " + token);
static async Task<Claim[]> CheckToken(RSA rsa, string token)
{
ArgumentNullException.ThrowIfNullOrEmpty(token);
if (!token.StartsWith("Bearer "))
{
throw new FormatException("不是有效的 token 格式");
}
var newToken = token[7..];
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
if (!jwtSecurityTokenHandler.CanReadToken(newToken))
{
throw new FormatException("不是有效的 token 格式");
}
var rsaKey = new RsaSecurityKey(rsa);
var checkResult = await jwtSecurityTokenHandler.ValidateTokenAsync(newToken, new TokenValidationParameters()
{
RequireExpirationTime = true,
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = rsaKey,
});
if (!checkResult.IsValid)
{
throw new FormatException("token 检查不通过");
}
var jwt = jwtSecurityTokenHandler.ReadJwtToken(newToken);
IEnumerable<Claim> claims = jwt.Claims;
return claims.ToArray();
}
完整代码如下:
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
var privateKey = RSA.Create();
privateKey.ImportFromPem(File.ReadAllText("./private.key"));
var publicKey = RSA.Create();
publicKey.ImportPublichKeyPck8(File.ReadAllText("./public.key"));
var token = GetToken(privateKey, "aaa");
var userinfos = await CheckToken(publicKey, "Bearer " + token);
Console.ReadLine();
static string GetToken(RSA rsa, string userName)
{
var rsaKey = new RsaSecurityKey(rsa);
// 定义用户信息
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, userName)
};
SecurityToken securityToken = new JwtSecurityTokenHandler().CreateToken(new SecurityTokenDescriptor
{
Claims = claims.ToDictionary(x => x.Type, x => (object)x.Value),
Issuer = "http://192.168.6.6:666",
Audience = userName,
NotBefore = DateTime.Now,
Expires = DateTime.Now.AddDays(100),
SigningCredentials = new SigningCredentials(rsaKey, SecurityAlgorithms.RsaSha256)
});
var indf = securityToken.ToString();
var jwtToken = new JwtSecurityTokenHandler().WriteToken(securityToken);
return jwtToken;
}
static async Task<Claim[]> CheckToken(RSA rsa, string token)
{
ArgumentNullException.ThrowIfNullOrEmpty(token);
if (!token.StartsWith("Bearer "))
{
throw new FormatException("不是有效的 token 格式");
}
var newToken = token[7..];
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
if (!jwtSecurityTokenHandler.CanReadToken(newToken))
{
throw new FormatException("不是有效的 token 格式");
}
var rsaKey = new RsaSecurityKey(rsa);
var checkResult = await jwtSecurityTokenHandler.ValidateTokenAsync(newToken, new TokenValidationParameters()
{
RequireExpirationTime = true,
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = rsaKey,
});
if (!checkResult.IsValid)
{
throw new FormatException("token 检查不通过");
}
var jwt = jwtSecurityTokenHandler.ReadJwtToken(newToken);
IEnumerable<Claim> claims = jwt.Claims;
return claims.ToArray();
}
/// <summary>
/// Rsa 加解密.
/// </summary>
public static class RsaHelper
{
/// <summary>
/// 导出 pck 8 公钥.
/// </summary>
/// <param name="rsa"></param>
/// <returns></returns>
public static string ExportPublichKeyPck8(this RSA rsa)
{
StringBuilder builder = new StringBuilder();
builder.AppendLine("-----BEGIN PUBLIC KEY-----");
builder.AppendLine(Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo(), Base64FormattingOptions.InsertLineBreaks));
builder.AppendLine("-----END PUBLIC KEY-----");
return builder.ToString();
}
public static void ImportPublichKeyPck8(this RSA rsa, string publicKey)
{
publicKey = publicKey
.Replace("-----BEGIN RSA PUBLIC KEY-----", "")
.Replace("-----END RSA PUBLIC KEY-----", "")
.Replace("-----BEGIN PUBLIC KEY-----", "")
.Replace("-----END PUBLIC KEY-----", "")
.Replace("\r", "")
.Replace("\n", "");
rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicKey), out var bytesRead);
}
/// <summary>
/// 加密信息.
/// </summary>
/// <param name="rsa"></param>
/// <param name="message"></param>
/// <param name="padding">如 <see cref="RSAEncryptionPadding.OaepSHA256"/></param>
/// <returns></returns>
public static string Encrypt(this RSA rsa, string message, RSAEncryptionPadding padding)
{
var encryptData = rsa.Encrypt(Encoding.UTF8.GetBytes(message), padding);
return Convert.ToBase64String(encryptData);
}
/// <summary>
/// 解密
/// </summary>
/// <param name="rsa"></param>
/// <param name="message"></param>
/// <param name="padding"></param>
/// <returns></returns>
public static string Decrypt(this RSA rsa, string message, RSAEncryptionPadding padding)
{
var cipherByteData = Convert.FromBase64String(message);
var encryptData = rsa.Decrypt(cipherByteData, padding);
return Encoding.UTF8.GetString(encryptData);
}
}
文章评论