JWT 简介与 C# 示例

发布时间 2023-12-26 20:00:53作者: 橙子家

〇、什么是 JWT ?

JWT,即 JSON Web Token,是一种基于 JSON 的开放标准(RFC 7519),主要用于在网络应用环境间安全地传递声明。这种声明被进行了数字签名,可以验证和信任,因此,它适用于各种需要信息安全性和无状态的应用。

在具体加密过程中,客户端会使用 RSA 算法生成 JWT 串,这里用到了私钥“加密”,而公钥是公开的,任何人都能解密,但内容无法变更。也就是说,在 JWT 中并没有纯粹的加密过程,而是通过加密保障了信息的完整性和真实性。

适用场景:

  • 用户认证:当用户成功登录后,服务器会生成一个 JWT 令牌并返回给客户端,此后客户端只需携带这个令牌即可访问服务器提供的资源。
  • 一次性验证:比如,用户注册后发一封邮件让其激活账户,通常邮件中需要有一个链接,这个链接需要具备能够标识用户、具有时效性、不能被篡改以及一次性的特性。这种场景就适合使用 JWT。
  • 防止传输数据篡改:即使数据在传输过程中被截获,由于 JWT 可以使用加密算法对传输内容进行签名,因此很难同时篡改签名和传输内容。

优点:

  • 更少的数据库连接:因其基于算法来实现身份认证,在使用 JWT 时查询数据的次数更少,可以获得更快的系统响应时间。
  • 构建更简单:如果你的应用程序本身是无状态的,那么选择 JWT 可以加快系统构建过程。
  • 跨服务调用:可以通过构建一个认证中心,来处理用户身份认证和发放签名的工作,其他应用服务在后续的用户请求中,可使用自有的公钥对用户签名进行验证。
  • 无状态:你不需要向传统的 Web 应用那样将用户状态保存于 Session 中。

局限性:

  • 安全性:由于 JWT 的 Payload 中负载信息,是使用 base64Url 编码的,并没有加密,因此 JWT 中不能存储敏感数据
  • 一次性:无状态是 JWT 的特点,但也导致了 JWT 是一次性的。想修改里面的内容,就必须签发一个新的 JWT
  • 严重依赖于秘钥:JWT 的生成与解析过程都需要依赖于秘钥(Secret),且都以硬编码的方式存在于系统中或配置里。如果秘钥泄露,系统的安全性将受到严重威胁。
  • 服务端无法管理客户端的信息:如果用户身份发生异常(信息泄露或者被攻击),服务端很难将异常用户进行隔离。
  • 服务端无法主动推送消息:服务端由于是无状态的,就无法推送消息到客户端。例如过期时间将至,服务端无法主动为用户续约,需要客户端向服务端发起续约请求。
  • 冗余的数据开销:一个 JWT 签名的大小要远比一个 Session ID 长很多,如果你对有效载荷(payload)中的数据不做有效控制,其长度会成几何倍数增长,且在每一次请求时都需要负担额外的网络开销。

一、JWT 的组成

下边是一个示例密文 token:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6IuW8oOS4iSIsImVtYWlsIjoiemhhbmdzYW5AZXhhbXBsZS5jb20iLCJ0ZW1wa2V5IjoidGVtcHZhbHVl5YC8IiwibmJmIjoxNzAyODg2MjgxLCJleHAiOjE3MDI4ODYyOTEsImlhdCI6MTcwMjg4NjI4MX0.2nuyYrAxVq3aAReN257eMHKGG44j5QyPMabxMnSzVBU

密文起始就是,看起来是非常复杂,实际上有章可循的,如下图,密文以其中的两个句点为分隔,可分为三个部分:Header、Payload、Signature

1.1 Header 头信息

Header 的主要作用是用来标识。通常是两部分组成:

  • typ:type 的简写,令牌类型,也就是 JWT。
  • alg:Algorithm 的简写,加密签名算法。

alg 参数 JWT 官网提供了 12 种算法,如下图,但一般都采用 HS256

  

明文示例:

{
  "alg": "HS256",
  "typ": "JWT"
}

经过 Base64Url 编码后,就是密文中第一部分的内容:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

1.2 Payload 有效负载

Payload 是 JWT 密文中的重要组成部分,也可称为 JWT claims,它包含了需要传递的数据信息,解密后的数据格式也是 Json。

claims 可以分为三种类型:registered(预定义声明)、public(公共声明)、private(私有声明)。

registered(预定义声明):不是强制性的,但推荐使用,以提供一组有用的、可互操作的声明。

  其中主要包括四个:iss(issuer,发送数据人,标识发送 JWT 主体)、exp(expiration time,数据消息的过期时间,一般采用时间戳格式)、sub(subject,数据消息主题)、aud(audience,数据消息的接收者)。

public(公共声明):公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。一般不建议添加敏感信息,因为该部分在任何客户端均可解密。

private(私有声明):私有声明是提供者和消费者所共同定义的声明

注:claims 声明名称一般只有三个字符长,因为 JWT 的目的是精简。

示例:

{
	"unique_name": "张三",
	"email": "zhangsan@example.com",
	"tempkey": "tempvalue值"
}

经过 Base64Url 加密后,得到密文的第二部分内容:

eyJ1bmlxdWVfbmFtZSI6IuW8oOS4iSIsImVtYWlsIjoiemhhbmdzYW5AZXhhbXBsZS5jb20iLCJ0ZW1wa2V5IjoidGVtcHZhbHVl5YC8IiwibmJmIjoxNzAyODg2MjgxLCJleHAiOjE3MDI4ODYyOTEsImlhdCI6MTcwMjg4NjI4MX0

1.3 signature 签名信息

Signature 部分是对 Header 和 Payload 两部分的签名,作用是防止 JWT 被篡改

要创建签名部分,前提是必须获取编码后的 Header、编码后的有效负载 Payload、secret 密钥、标头中 alg 指定的算法,一般为 HS256,然后才能对其进行签名。

HMACSHA256(
    base64UrlEncode(header) + "." +  base64UrlEncode(payload),
    secret
)

加密后得到的密文就是 Token 中最后一部分内容。

密钥 secret 是保存在服务端的,服务端会根据这个密钥进行生成 token 和验证,所以需要严格保密

二、JWT 身份验证的流程简介

JWT 经常用于身份验证流程,以下是一个简单的步骤:

  1. 用户首次登录,输入账号密码,请求登录接口 /users/login。
  2. 服务端验证登陆信息,并通过密钥创建 JWT 凭证。
  3. 服务端返回 JWT 凭证到浏览器,浏览器进行缓存。
  4. 用户操作触发请求,浏览器会自动将 JWT 凭证加入到请求的 Header 中。
  5. 服务端接收到请求,先判断 JWT 凭证的有效性。
  6. 若 JWT 凭证有效,则正常返回的请求结果数据;若校验不通过,则提示用户重新进行身份验证。

另外,在日常业务中,有可能出现并发异常问题。

当服务端在检查到请求的令牌过期之后,会提示用户再次做登录操作。

这在流程上没什么问题,但在页面加载后,如果同一个页面中有多个异步请求同时触发,每一个请求都携带原始令牌,在这样的设计下,就有可能出现在第一个请求到达后刷新了 Token,并更改了缓存中数据的时间戳,以至于剩余请求校验时发现时间戳不一致导致验证失败。

同一时间触发的请求越多,抛出的异常也就越多。虽然第一个请求已经刷新了 Token,但是其余的请求是失败的,页面中的数据并不完整,显然这是不正常的。那如何避免呐?

redis 锁机制:在触发更新 Token 时,将同一用户信息加锁,使得此用户的其他请求均失败,待登录验证通过后再重新加载。

Token 定时刷新:当用户在线时,间隔一段时间刷新一次 Token。要刷新令牌,API 需要一个新的端点,它接收一个有效的,没有过期的 JWT,并返回与新的到期字段相同的签名的 JWT。若用户长时间没登录,则直接跳转到登录页。

三、C# 简单实现

直接看代码吧。

// 测试一下
class Program
{
    static void Main(string[] args)
    {
        JwtTest jwtTest= new JwtTest();
        string key = "keayvkkakeyvaluyeaeayvalalujeehayvalguaealrue";
        var claims = new[]
        {
            new Claim(ClaimTypes.Name, "张三"),
            new Claim(ClaimTypes.Email, "zhangsan@example.com"),
            new Claim("tempkey", "tempvalue值"),
        };
        string token = jwtTest.JwtEncode(key, claims);
        Console.WriteLine(token);
        // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6IuW8oOS4iSIsImVtYWlsIjoiemhhbmdzYW5AZXhhbXBsZS5jb20iLCJ0ZW1wa2V5IjoidGVtcHZhbHVl5YC8IiwibmJmIjoxNzAzNTkxMTk4LCJleHAiOjE3MDM1OTEyMDgsImlhdCI6MTcwMzU5MTE5OH0.65Mx_ldbQijHevkalutHMaejQ06vhe5fW6e6-t-aziw
        string json = jwtTest.JwtDecode(token, key);
        // {"unique_name":"张三","email":"zhangsan@example.com","tempkey":"tempvalue值","nbf":1703591258,"exp":1703591268,"iat":1703591258}
        Console.WriteLine(json);
    }
}

验证一下:https://jwt.io/

// JWT 加密解密类
public class JwtTest
{
    public string JwtEncode(string keyvalue, Claim[] claims)
    {
        var key = Encoding.UTF8.GetBytes(keyvalue);
        var tokenHandler = new JwtSecurityTokenHandler();
        var tokenDescriptor = new SecurityTokenDescriptor
        {
                
            Subject = new ClaimsIdentity(claims),
            Expires = DateTime.UtcNow.AddSeconds(10),
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };

        var token = tokenHandler.CreateToken(tokenDescriptor);
        var tokenString = tokenHandler.WriteToken(token);
        Console.WriteLine($"加密后的JWT: {tokenString}");
        return tokenString;
    }
    public string JwtDecode(string jwttoken, string publickey)
    {
        try
        {
            IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
            IJsonSerializer serializer = new JsonNetSerializer();
            IDateTimeProvider provider = new UtcDateTimeProvider();
            IJwtValidator validator = new JwtValidator(serializer, provider);
            IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
            IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm);
            var json = decoder.Decode(jwttoken, publickey, verify: true);
            return json;
        }
        catch(Exception ex)
        {
            return "";
        }
    }
}

参考:https://jwt.io/introduction  https://juejin.cn/post/7232550589964140602  https://cloud.tencent.com/developer/article/2148676