本文内容:
-
JWT Token
-
JWT Token Issuance
-
JWT Signature Verification
-
JWT Security Check
Token Principles
Visit https://jwt.io/ to see a default page with an automatically generated token and information about this token on the right side.
The default token is as follows:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.EJzdDeNLOQhy-2WmXuK1B49xF17Tk0pja1tCPp81YjY
On the left, there is a token example, and the right side shows the structure after parsing the token. Since the token stores information in plaintext, it can be parsed without a key, which leads to the need for signature verification mentioned later.
On the right side, the parsed token is divided into three parts: HEADER
, PAYLOAD
, and VERIFY SIGNATURE
.
The HEADER part records the signing method and the type of token issued:
{
"alg": "HS256",
"typ": "JWT"
}
There are only two fields, storing two pieces of information: ALGORITHM (encryption method) and TOKEN TYPE.
The PAYLOAD part records user information, which is in plaintext:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
The payload is the data area that stores some custom information such as token validity, the current user's name, user ID, and which program issued the token; all of these are custom fields.
The token does not specify which fields must be present in the data area, but it defines a series of commonly used fields indicated by abbreviations, such as sub (subject), name (token owner), iat (issued at). Other abbreviated fields are defined in the Claim type in C#.
The VERIFY SIGNATURE part:
The signature of the token is generated after creating the previous two parts by signing the PAYLOAD content with the key to create the third part.
Note that the signature is not encryption.
For example, using the common HMACSHA256 algorithm.
The encryption method for the token is as follows:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
Here, we use the key abcd$a1aacd1213ccccccccc13113aaaaa
and HMACSHA256 to simulate this process.
First, encode the header and payload using base64, and then concatenate them:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImFhYSIsIm5iZiI6MTczNTYzMTAyOSwiZXhwIjoxNzQ0MjcxMDI5LCJpYXQiOjE3MzU2MzEwMjksImlzcyI6Imh0dHA6Ly8xOTIuMTY4LjYuNjo2NjYiLCJhdWQiOiJhYWEifQ
After signing, it generates:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImFhYSIsIm5iZiI6MTczNTYzMTAyOSwiZXhwIjoxNzQ0MjcxMDI5LCJpYXQiOjE3MzU2MzEwMjksImlzcyI6Imh0dHA6Ly8xOTIuMTY4LjYuNjo2NjYiLCJhdWQiOiJhYWEifQ.a_HxBRItLSrXCKAIWzjKHVLSHTz-92C0qVFElls-M1w
Copy this token to the page for parsing, and you will find the content parsed, but the signature verification will fail.
If you input the signature key in this place, the verification will succeed.
JWT Issuance and Signature Checking
In most cases, the code for generating JWT Tokens used in C# is as follows:
// Define user information
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)
);
However, the token generated this way is an insecure token and has not been signed.
To make the token more secure, you can do this:
// Define user information
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;
Additionally, the code to check the token can be written like this:
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 Security Check
The above examples are conventional methods, which may not be suitable in a distributed system.
The signing encryption of JWT tokens generally has two methods: symmetric encryption and asymmetric encryption.
Among them, symmetric encryption is also known as shared key encryption.
The method of token generation shown in the previous examples uses symmetric encryption:
SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenOption.SecurityKey))
SymmetricSecurityKey is the base class for symmetric encryption in C#.
This is used for signing and checking the token later.
However, in distributed systems, there's a problem: after a user obtains a token issued by the login center, they make requests to one of the internal systems A.
How can system A determine that this token is not valid? How does it verify the signature?
If the shared key (symmetric encryption) method is used, each subsystem needs to know what the key is, which can easily lead to key leakage.
Alternatively, if each request requires the subsystem to check with the login center whether the token is valid, this is not network-friendly and could bring other issues.
Thus emerges the asymmetric encryption method. For example, using RSA, the login center keeps a unique private key, and each subsystem only has the public key.
In this way, subsystems cannot issue tokens but can check whether tokens are valid.
First, generate the private and public keys for RSA asymmetric encryption:
If you are interested in RSA, you can refer to another article by the author to copy an RsaHelper helper class:
https://www.whuanle.cn/archives/21160
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-----
Place the public key file in the directory below:
Import the private and public keys:
var privateKey = RSA.Create();
privateKey.ImportFromPem(File.ReadAllText("./private.key"));
var publicKey = RSA.Create();
publicKey.ImportPublichKeyPck8(File.ReadAllText("./public.key"));
Generate the RSA asymmetric token:
var token = GetToken(privateKey, "aaa");
static string GetToken(RSA rsa,string userName)
{
var rsaKey = new RsaSecurityKey(rsa);
// Define user information
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;
}
Use the public key to verify the 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("Not a valid token format");
}
var newToken = token[7..];
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
if (!jwtSecurityTokenHandler.CanReadToken(newToken))
{
throw new FormatException("Not a valid token format");
}
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 validation failed");
}
var jwt = jwtSecurityTokenHandler.ReadJwtToken(newToken);
IEnumerable<Claim> claims = jwt.Claims;
return claims.ToArray();
}
The complete code is as follows:
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);
// Define user information
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("Not a valid token format");
}
var newToken = token[7..];
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
if (!jwtSecurityTokenHandler.CanReadToken(newToken))
{
throw new FormatException("Not a valid token format");
}
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 validation failed");
}
var jwt = jwtSecurityTokenHandler.ReadJwtToken(newToken);
IEnumerable<Claim> claims = jwt.Claims;
return claims.ToArray();
}
/// <summary>
/// RSA encryption and decryption.
/// </summary>
public static class RsaHelper
{
/// <summary>
/// Export PCK8 public key.
/// </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>
/// Encrypt message.
/// </summary>
/// <param name="rsa"></param>
/// <param name="message"></param>
/// <param name="padding">e.g., <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>
/// Decrypt
/// </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 decryptData = rsa.Decrypt(Convert.FromBase64String(message), padding);
return Encoding.UTF8.GetString(decryptData);
}
}
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);
}
文章评论