.NET CORE 生成/验证 JWT Token

192

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 来生成签名 https://www.nuget.org/packages/System.IdentityModel.Tokens.Jwt/8.6.1?_src=template

  <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; }
    }