.NET CORE 生成/验证 JWT Token
1. 什么是 JSON Web 令牌?
JSON Web 令牌 (JWT) 是一种开放标准 (RFC 7519),它定义了一种紧凑且自包含的方式,用于将信息作为 JSON 对象在各方之间安全地传输。此信息是经过数字签名的,因此可以验证和信任。可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对对 JWT 进行签名。
2. 何时应使用 JSON Web 令牌?
授权:这是使用 JWT 的最常见场景。用户登录后,每个后续请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销小,并且能够轻松地跨不同域使用。
信息交换:JSON Web 令牌是在各方之间安全地传输信息的好方法。由于 JWT 可以签名(例如,使用公钥/私钥对),因此您可以确保发件人是他们所声称的身份。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。
3. JSON Web 令牌结构?
令牌格式由三个部分组成:头部(Header)、载荷(Payload)、签名(Signature)。这三个部分通过Base64Url编码后,通过点(.)连接在一起。
1. 头部(Header)
头部通常包含两部分信息:
typ:表示令牌的类型,这里是JWT。
alg:表示签名的算法,例如HS256、RS256等。
例如,一个简单的头部看起来像这样:
{
"alg": "HS256",
"typ": "JWT"
}2. 载荷(Payload)
载荷包含了一些声明(claims),这些声明定义了关于实体(通常是用户)和其他数据的声明。这些声明分为三种类型:
已注册的声明(如iss(发行人)、exp(过期时间)、sub(主题)等)。
公共的声明:双方都可以理解的声明。
私有的声明:为私有使用,客户端和服务端都知道的声明。
例如,一个简单的载荷看起来像这样:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}3. 签名(Signature)
签名用于验证该令牌自被签发以来未被篡改过。它使用头部中指定的算法以及一个密钥(secret key)来生成。签名过程如下:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)将头部、载荷和签名组合在一起,用点(.)分隔,就形成了完整的JWT字符串。
4. 生成签名、验证签名
使用库 System.IdentityModel.Tokens.Jwt 来生成签名
<ItemGroup>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.5.0" />
</ItemGroup> public class CommonService : ICommonService
{
private readonly string _jwtSecurityKey;
private readonly int _LoginTicketExpireTimeMinute;
private readonly int _LoginAuthTokenExpireTimeDay;
private readonly ILogger<CommonService> _logger;
public CommonService(IConfiguration configuration, ILogger<CommonService> logger)
{
_jwtSecurityKey = configuration["Jwt:SecurityKey"]!.ToString();
_LoginTicketExpireTimeMinute = int.Parse(configuration["Jwt:LoginTicketExpireTimeMinute"]!.ToString());
_LoginAuthTokenExpireTimeDay = int.Parse(configuration["Jwt:LoginAuthTokenExpireTimeDay"]!.ToString());
_logger = logger;
}
/// <summary>
/// 获取登录临时凭证Ticket
/// </summary>
/// <param name="claimInfo"></param>
/// <param name="_tokenExpireTimeMinute"></param>
/// <returns></returns>
public string CreateLoginTicket(LoginTicketClaim claimInfo, out int _tokenExpireTimeMinute)
{
var key = _jwtSecurityKey;
var tokenExpireTimeMinute = _LoginTicketExpireTimeMinute;
_tokenExpireTimeMinute = tokenExpireTimeMinute;
var claims = new List<Claim>() {
// jti (JWT ID):编号
new Claim("jti",claimInfo.jti),
// sub (subject):主题
new Claim("sub",claimInfo.sub),
// 连锁店扩展信息
// 连锁店编号
new Claim("hospital_no",claimInfo.hospital_no),
new Claim("hospital_name",claimInfo.hospital_name),
// 雇员扩展信息
new Claim("employee_no",claimInfo.employee_no),
new Claim("employee_name",claimInfo.employee_name),
new Claim("employee_phone",claimInfo.employee_phone),
};
SecurityKey securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(key));
JwtSecurityToken jwtToken = new JwtSecurityToken(
// iss (issuer):签发人
issuer: "hospital-rpc",
// exp (expiration time):过期时间
expires: DateTime.Now.AddMinutes(_tokenExpireTimeMinute),
// aud (audience):受众
audience: "hospital-employee",
// nbf (Not Before):生效时间
notBefore: DateTime.Now,
// 部分定义私有字段
claims: claims,
// 签名
signingCredentials: new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256)
);
string token = new JwtSecurityTokenHandler().WriteToken(jwtToken);
return token;
}
/// <summary>
/// 验证登录临时凭证Ticket返回Ticket存储的Claim内容
/// </summary>
/// <param name="loginTicket"></param>
/// <param name="state">OK|TokenExpired|TokenInvalid|</param>
/// <returns></returns>
public LoginTicketClaim? CheckLoginTicket(string loginTicket, out string state)
{
SecurityKey securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_jwtSecurityKey));
var validateParameter = new TokenValidationParameters()
{
// 验证:过期时间
ValidateLifetime = true,
// 验证:aud (audience):受众
ValidateAudience = true,
// 验证:iss (issuer):签发人
ValidateIssuer = true,
// 验证:签名密钥
ValidateIssuerSigningKey = true,
//校验过期时间必须加此属性
ClockSkew = TimeSpan.Zero,
ValidIssuer = "hospital-rpc",
ValidAudience = "hospital-employee",
IssuerSigningKey = securityKey,
};
try
{
// 校验并解析token
// validatedToken是解密后的对象
var principal = new JwtSecurityTokenHandler().ValidateToken(loginTicket, validateParameter, out SecurityToken validatedToken);
//获取payload中的数据
var jwtPayload = ((JwtSecurityToken)validatedToken).Payload.SerializeToJson();
state = "OK";
return JsonConvert.DeserializeObject<LoginTicketClaim>(jwtPayload!)!;
}
catch (SecurityTokenExpiredException ex)
{
// 表示过期
state = "TokenExpired";
}
catch (SecurityTokenException ex)
{
// 表示token错误
state = "TokenInvalid";
}
catch (Exception ex)
{
state = "TokenInvalid";
}
return null;
}
/// <summary>
/// 获取登录授权凭证Token
/// </summary>
/// <param name="claimInfo"></param>
/// <param name="_tokenExpireTimeDays"></param>
/// <returns></returns>
public string CreateLoginAuthToken(LoginTokenClaim claimInfo, out int _tokenExpireTimeDays)
{
var key = _jwtSecurityKey;
var tokenExpireTimeDays = _LoginAuthTokenExpireTimeDay;
_tokenExpireTimeDays = tokenExpireTimeDays;
var claims = new List<Claim>() {
// jti (JWT ID):编号
new Claim("jti",claimInfo.jti),
// sub (subject):主题
new Claim("sub",claimInfo.sub),
// 连锁店扩展信息
// 连锁店编号
new Claim("hospital_no",claimInfo.hospital_no),
new Claim("hospital_name",claimInfo.hospital_name),
// 雇员扩展信息
new Claim("employee_no",claimInfo.employee_no),
new Claim("employee_name",claimInfo.employee_name),
new Claim("employee_phone",claimInfo.employee_phone),
};
SecurityKey securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(key));
JwtSecurityToken jwtToken = new JwtSecurityToken(
// iss (issuer):签发人
issuer: "hospital-rpc",
// exp (expiration time):过期时间
expires: DateTime.Now.AddDays(tokenExpireTimeDays),
// aud (audience):受众
audience: "hospital-employee",
// nbf (Not Before):生效时间
notBefore: DateTime.Now,
// 部分定义私有字段
claims: claims,
// 签名
signingCredentials: new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256)
);
string token = new JwtSecurityTokenHandler().WriteToken(jwtToken);
return token;
}
}
/// <summary>
/// 获取登录临时凭证Ticket-扩展JWTClaim
/// </summary>
public class LoginTicketClaim
{
/// <summary>
/// jti (JWT ID):编号
/// </summary>
public string jti { get; set; }
/// <summary>
/// sub (subject):主题
/// </summary>
public string sub { get; set; }
/// <summary>
/// 连锁店扩展信息:连锁店编号
/// </summary>
public string hospital_no { get; set; }
/// <summary>
/// 连锁店扩展信息:连锁店名称
/// </summary>
public string hospital_name { get; set; }
/// <summary>
/// 雇员扩展信息:雇员编号
/// </summary>
public string employee_no { get; set; }
/// <summary>
/// 雇员扩展信息:雇员名称
/// </summary>
public string employee_name { get; set; }
/// <summary>
/// 雇员扩展信息:雇员手机号
/// </summary>
public string employee_phone { get; set; }
}
/// <summary>
/// 获取登录授权凭证Token-扩展JWTClaim
/// </summary>
public class LoginTokenClaim
{
/// <summary>
/// jti (JWT ID):编号
/// </summary>
public string jti { get; set; }
/// <summary>
/// sub (subject):主题
/// </summary>
public string sub { get; set; }
/// <summary>
/// 连锁店扩展信息:连锁店编号
/// </summary>
public string hospital_no { get; set; }
/// <summary>
/// 连锁店扩展信息:连锁店名称
/// </summary>
public string hospital_name { get; set; }
/// <summary>
/// 雇员扩展信息:雇员编号
/// </summary>
public string employee_no { get; set; }
/// <summary>
/// 雇员扩展信息:雇员名称
/// </summary>
public string employee_name { get; set; }
/// <summary>
/// 雇员扩展信息:雇员手机号
/// </summary>
public string employee_phone { get; set; }
}