@iEricLee Creation Time : 2024-09-11 11:48:40 Last Modification Time : 2024-09-11 12:53:11
在前面我们已经实现 在 .NET Core 中生成和解析JWT令牌(2):使用 HS256(HMAC-SHA256)算法签名,本文中将通过示例演示使用 RS256(RSA-SHA256)算法签名创建JWT令牌。
思路和流程是相同的,不同的地方在于,密钥的生成和解析不一样。
为了方便代码管理,将这个示例的核心代码封装在一个单独的类 Jwt_RSASHA256Helper
中。
假设我们已经生成私钥和公钥。通过私钥生成令牌:(密钥部分核心代码)
//私钥
private static string PRIVATE_KEY = "PRIVATE_KEY";
//公钥
private static string PUBLIC_KEY = "PUBLIC_KEY";
var rsa = RSA.Create()
.ImportPkcs8PrivateKey(Convert.FromBase64String(PRIVATE_KEY), out _);
SecurityKey securityKey = new RsaSecurityKey(rsa);
var token = new JwtSecurityToken(
issuer: "fan",
audience: "fan",
claims: claims,
notBefore: DateTime.Now,
expires: DateTime.Now.AddSeconds(10),
signingCredentials: new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256)
);
签名凭据 SigningCredentials
构造函数接收两个参数:密钥和算法,这里使用对称密钥 RsaSecurityKey
和 SecurityAlgorithms.RsaSha256
算法。
RSA签名算法的校验需要公钥和私钥,验证参数和 HS256 略有不同。
登陆 https://jwt.io ,将生成的访问令牌复制到输入框:
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IlVYR1pYUF9HSFBFNlUwLVhXUVFZQlMxUUJCWlAzT1cxQlZCVTZTUVEiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo3MDE4LyIsImV4cCI6MTcyNDUwODE5MCwiaWF0IjoxNzI0NTA0NTkwLCJzY29wZSI6Im9mZmxpbmVfYWNjZXNzIiwianRpIjoiYjI2NTNkZTktZDNhZi00M2Y0LTgwOWMtZWIyOTU2ODQ4NjBkIiwic3ViIjoiYWRtaW4iLCJzb21lIGNsYWltIjoic29tZSB2YWx1ZSIsIm9pX3Byc3QiOiJjbGllbnRfYXBwIiwib2lfYXVfaWQiOiJiMmEzOTVjNi1hMGY1LTQ5OGQtYTcwMy1mMmQ2MWM4Nzk1ZmUiLCJjbGllbnRfaWQiOiJjbGllbnRfYXBwIiwib2lfdGtuX2lkIjoiMTEzYzY1NjAtNjc4OC00MWI5LTk2OWUtZmNlODcxYjgzYTk3In0.OImRQf6qBaVn_qe9ZtuQKu3TSEKpmWLmqkv0uwrPqe0vkGAa26zvl3J4hfbcKM-XYB1EsamIfISRudbc8PoQ5ZBoXEbWU1A6eNMqg53MZ1UZedNZ41Vy_1Z_rmZqMGwdacMt4zxgLR0PN551V-FEif_CPCDYWaMHRB9ghjKVrn2CplMl73ssZy0D61Qblb1utb-NXHTsNzGWvfgDZ39-BINBjqg06Nu4bccfJUbtIfxp18CYxGfPMI60JQOVyVKC7APtrqbLIdCkgYG_gS3OOjOm9cDSmDgblrBAkvzF5xWhMZmkt1dSMqyt8rnKlSb_YO5RywnFxvvnmjnrPk-bc7ldF6rjHqN6drxqJ7PzKLr-CqmWFgYC3h6N26BU7hItuzHUu4HyViuVZZbi78EKiQmXUmzF4AZMfhEH-vCy23XDi-6klnrx6lN2tv1ath1wDhWGHL7NavvTNyn9yXN-97Dzv4ujJwo1CpFzqgImX84m5sHe2o12JS1Kk-5gHP5VPvuFgRKirP9_ArnjXxOL8NFuT1ji_CffDfrEY9bicdd593NcA6Ljjujv-iK_Stjcku1RpDJtnW6Ap-LRYsjdgkxx9_wqv9H3BJiyyNYXLQmpx9f3XlnmJKFWhgDGHogLcZHR__EgLWprmJcm2BTL9VORHzG-E70WRBIKlAD4B5g"
只需要粘贴 access_token 内容即可。
首先获取公钥,访问 https://localhost:7018/.well-known/jwks
返回如下内容:
关于 JWKS ,可以参看 JWS和JWKS详解 。
{
"keys": [
{
"kid": "UXGZXP_GHPE6U0-XWQQYBS1QBBZP3OW1BVBU6SQQ",
"use": "sig",
"kty": "RSA",
"alg": "RS256",
"e": "AQAB",
"n": "uXGzXP_GHpe6U0-xWQqyBs1qBBzP3OW1bVBU6sQqKU1Ef5IjK8Qoej4d7mjYQk5keq9OOu2d4WnVp5SiqqJQwCOY74dBFa5JhAGvlW1gk3hewtwtGFtq0LfLtlTzs1QFeqX-IWZoKksLfK6yZn6w1Ol1tv2UYeHuTLwM0zcaEqyHwUl1-B2gc86zu2evv9TxQWZgIEemrHJu7QQL59Z4BRE15v7kHUc_mLq9uYFT-a2Ftq7mcQWgfIphTGs4SZzKV0vRLwWsERSwyfrs4YyDL0NvqswWq3NNJF0ascc3_TUor-rjHi98L-Shi-Ga1ItMCKXbjnVcAXSwX47c8gL5qPZkl0r9Csbp3vK0Ukc91ZvkTYZ7IDCbqHU7db7QsdtVyz92vJu45cJbGuwDGqwqP0ExYKbf5zAVXv25Gd3aBLRt_s7sQOOWWNn1XbIdtpOdiD124ZiY1tA7J-BqHV4EWlu0azu6lS7q1GSfMPiK_i6ltB1H7xVaW9Mw9PxHr_ZtO_mZ1B4XDeoMSCWv1wgSzk-U03AB_qF8Z4OeIZhhPZ4fqCfhQKEfT7sZInCcFzhFr3gMlWN5-THcFBlcLi-fntnsMEYYXBw8UKB3OZV0u8ZJ0xp6_yj-45lRgBU6gVCW81elhgdE65LBUlsEy-UI0Eeb2Hq73mdGpgzQ_Z1w0Wc"
}
]
}
可能会返回多个公钥,这里我们选择:
{
"kid": "UXGZXP_GHPE6U0-XWQQYBS1QBBZP3OW1BVBU6SQQ",
"use": "sig",
"kty": "RSA",
"alg": "RS256",
"e": "AQAB",
"n": "uXGzXP_GHpe6U0-xWQqyBs1qBBzP3OW1bVBU6sQqKU1Ef5IjK8Qoej4d7mjYQk5keq9OOu2d4WnVp5SiqqJQwCOY74dBFa5JhAGvlW1gk3hewtwtGFtq0LfLtlTzs1QFeqX-IWZoKksLfK6yZn6w1Ol1tv2UYeHuTLwM0zcaEqyHwUl1-B2gc86zu2evv9TxQWZgIEemrHJu7QQL59Z4BRE15v7kHUc_mLq9uYFT-a2Ftq7mcQWgfIphTGs4SZzKV0vRLwWsERSwyfrs4YyDL0NvqswWq3NNJF0ascc3_TUor-rjHi98L-Shi-Ga1ItMCKXbjnVcAXSwX47c8gL5qPZkl0r9Csbp3vK0Ukc91ZvkTYZ7IDCbqHU7db7QsdtVyz92vJu45cJbGuwDGqwqP0ExYKbf5zAVXv25Gd3aBLRt_s7sQOOWWNn1XbIdtpOdiD124ZiY1tA7J-BqHV4EWlu0azu6lS7q1GSfMPiK_i6ltB1H7xVaW9Mw9PxHr_ZtO_mZ1B4XDeoMSCWv1wgSzk-U03AB_qF8Z4OeIZhhPZ4fqCfhQKEfT7sZInCcFzhFr3gMlWN5-THcFBlcLi-fntnsMEYYXBw8UKB3OZV0u8ZJ0xp6_yj-45lRgBU6gVCW81elhgdE65LBUlsEy-UI0Eeb2Hq73mdGpgzQ_Z1w0Wc"
}
复制到公钥输入框中。将私钥复制到私钥输入框中:(代码校验不需要知道私钥)
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
验证成功,如下图所示:
通过公钥创建 RsaSecurityKey
,构建令牌验证参数。注意需要对密钥进行格式转换。
//验证
public static bool VerifyJwtToken(string token, out ClaimsPrincipal principal)
{
principal = null;
//校验token
var validateParameter = new TokenValidationParameters()
{
ValidateLifetime = true,
ValidateAudience = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Issuer,
ValidAudience = Audience,
IssuerSigningKey = new RsaSecurityKey(CreateRsaProviderFromPublicKey(PUBLIC_KEY)),
ClockSkew = TimeSpan.Zero//校验过期时间必须加此属性
};
bool success = false;
try
{
//校验并解析token,validatedToken是解密后的对象
principal = new JwtSecurityTokenHandler().ValidateToken(token, validateParameter, out SecurityToken validatedToken);
//获取payload中的数据
//var jwtPayload = ((JwtSecurityToken)validatedToken).Payload.SerializeToJson();
success = true;
}
catch (SecurityTokenExpiredException ex)
{
//表示过期
success = false;
}
catch (SecurityTokenException ex)
{
//表示token错误
success = false;
}
catch (Exception ex)
{
success = false;
}
return success;
}
#region 私有方法
/// <summary>
/// 通过公钥创建RSA
/// </summary>
/// <param name="publicKeyString"></param>
/// <returns></returns>
private static RSA CreateRsaProviderFromPublicKey(string publicKeyString)
{
// encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
byte[] seqOid = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
byte[] seq = new byte[15];
var x509Key = Convert.FromBase64String(publicKeyString);
// --------- Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob ------
using (MemoryStream mem = new MemoryStream(x509Key))
{
using (BinaryReader binr = new BinaryReader(mem)) //wrap Memory Stream with BinaryReader for easy reading
{
byte bt = 0;
ushort twobytes = 0;
twobytes = binr.ReadUInt16();
if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
binr.ReadByte(); //advance 1 byte
else if (twobytes == 0x8230)
binr.ReadInt16(); //advance 2 bytes
else
return null;
seq = binr.ReadBytes(15); //read the Sequence OID
if (!CompareBytearrays(seq, seqOid)) //make sure Sequence for OID is correct
return null;
twobytes = binr.ReadUInt16();
if (twobytes == 0x8103) //data read as little endian order (actual data order for Bit String is 03 81)
binr.ReadByte(); //advance 1 byte
else if (twobytes == 0x8203)
binr.ReadInt16(); //advance 2 bytes
else
return null;
bt = binr.ReadByte();
if (bt != 0x00) //expect null byte next
return null;
twobytes = binr.ReadUInt16();
if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
binr.ReadByte(); //advance 1 byte
else if (twobytes == 0x8230)
binr.ReadInt16(); //advance 2 bytes
else
return null;
twobytes = binr.ReadUInt16();
byte lowbyte = 0x00;
byte highbyte = 0x00;
if (twobytes == 0x8102) //data read as little endian order (actual data order for Integer is 02 81)
lowbyte = binr.ReadByte(); // read next bytes which is bytes in modulus
else if (twobytes == 0x8202)
{
highbyte = binr.ReadByte(); //advance 2 bytes
lowbyte = binr.ReadByte();
}
else
return null;
byte[] modint = { lowbyte, highbyte, 0x00, 0x00 }; //reverse byte order since asn.1 key uses big endian order
int modsize = BitConverter.ToInt32(modint, 0);
int firstbyte = binr.PeekChar();
if (firstbyte == 0x00)
{ //if first byte (highest order) of modulus is zero, don't include it
binr.ReadByte(); //skip this null byte
modsize -= 1; //reduce modulus buffer size by 1
}
byte[] modulus = binr.ReadBytes(modsize); //read the modulus bytes
if (binr.ReadByte() != 0x02) //expect an Integer for the exponent data
return null;
int expbytes = (int)binr.ReadByte(); // should only need one byte for actual exponent data (for all useful values)
byte[] exponent = binr.ReadBytes(expbytes);
// ------- create RSACryptoServiceProvider instance and initialize with public key -----
var rsa = RSA.Create();
RSAParameters rsaKeyInfo = new RSAParameters
{
Modulus = modulus,
Exponent = exponent
};
rsa.ImportParameters(rsaKeyInfo);
return rsa;
}
}
}
private static bool CompareBytearrays(byte[] a, byte[] b)
{
if (a.Length != b.Length)
return false;
int i = 0;
foreach (byte c in a)
{
if (c != b[i])
return false;
i++;
}
return true;
}
#endregion
在 Program.cs 中调用:
string rsa_token = Jwt_RSAHelper.GetJwtToken();
bool success = Jwt_RSAHelper.VerifyJwtToken(rsa_token, out ClaimsPrincipal cp);
if(success)
{
Console.WriteLine("RSA token Success");
}