更轻易地实现Jwt Token

Intro

最近在多个项目中都有用到 Jwt Token 认证,就想着把之前项目里 Jwt Token 的使用封装一下,以便于之后集成起来更加地方便,不用再拷贝代码了.

JWT

JWT 是 JSON Web Token 的缩写,是目前最流行的基于 Token 的认证解决方案,JWT 是一种无状态的认证方式,我们不需要依赖 Session,更为简单,对于随时准备扩容缩容的云原生应用来说更加的友好。

JWT token 的格式分成三个部分,他们之间以 “.” 作为分隔

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

更轻易地实现Jwt Token

我们可以在 https://jwt.io 网站上查看 token 的内容,也可以自己写个小工具来查看,token 的内容是 base64 URL 编码的

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+/=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-/替换成_ 。这就是 Base64URL 算法。

JWT Token 内容解析之后如下所示:

更轻易地实现Jwt Token

Header 是 token 所用到的签名算法信息,默认是 HMAC SHA 256(HS256) 以及 RSA SHA 256(RS256)

Payload 就是我们 token 中保存的信息,这些信息是可以解析成明文的,所以内容上不能保存敏感信息,只能保存一些敏感度不高的信息

Signature 是基于 header 和 payload 生成出来的,例如使用 HMAC SHA 256 签名

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

Sample

介绍了一些基本知识,我们再来看示例吧

首先需要集成 WeihanLi.Web.Extensions 这个 NuGet 包

我们只需要注册服务即可:

services.AddJwtTokenService(options =>
{
    options.SecretKey = Guid.NewGuid().ToString();
    options.Issuer = "https://id.weihanli.xyz";
    options.Audience = "SparkTodo";
});

SecretKey 是用来生成 token 和 token 签名校验的,默认签名方式 是 HMAC SHA256

Issuer 是签发人,是指定谁颁发的 token

Audience 是受众,是指 token 是给谁用的

注册好服务之后,我们就可以从依赖注入服务中获取 ITokenService 来进行 token 的操作了

[HttpGet("getToken")]
public async Task<IActionResult> GetToken([Required] string userName, [FromServices] ITokenService tokenService)
{
    var token = await tokenService
        .GenerateToken(new Claim("name", userName));
    return token.WrapResult().GetRestResult();
}

[HttpGet("validateToken")]
public async Task<IActionResult> ValidateToken(string token, [FromServices] ITokenService tokenService)
{
    return await tokenService
        .ValidateToken(token)
        .ContinueWith(r =>
            r.Result.WrapResult().GetRestResult()
        );
}

GetToken:

更轻易地实现Jwt Token

VaidateToken:

更轻易地实现Jwt Token

Implement

我们来看一些实现细节

public class JwtTokenService : ITokenService
{
    private readonly JwtSecurityTokenHandler _tokenHandler = new();
    private readonly JwtTokenOptions _tokenOptions;

    private readonly Lazy<TokenValidationParameters>
        _lazyTokenValidationParameters;

    public JwtTokenService(IOptions<JwtTokenOptions> tokenOptions)
    {
        _tokenOptions = tokenOptions.Value;
        _lazyTokenValidationParameters = new(() =>
            _tokenOptions.GetTokenValidationParameters());
    }

    public virtual Task<TokenEntity> GenerateToken(params Claim[] claims)
        => GenerateTokenInternal(claims);

    public virtual Task<TokenValidationResult> ValidateToken(string token)
    {
        return _tokenHandler.ValidateTokenAsync(token, _lazyTokenValidationParameters.Value);
    }

    private async Task<TokenEntity> GenerateTokenInternal(bool refreshToken, Claim[] claims)
    {
        var now = DateTimeOffset.UtcNow;
        var claimList = new List<Claim>()
        {
            new (JwtRegisteredClaimNames.Iat, now.ToUnixTimeMilliseconds().ToString(), ClaimValueTypes.Integer64)
        };
        if (claims != null)
        {
            claimList.AddRange(claims);
        }

        var jti = claimList.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Jti)?.Value;
        if (jti is null)
        {
            jti = _tokenOptions.JtiGenerator?.Invoke() ?? GuidIdGenerator.Instance.NewId();
            claimList.Add(new(JwtRegisteredClaimNames.Jti, jti));
        }
        var jwt = new JwtSecurityToken(
            issuer: _tokenOptions.Issuer,
            audience: _tokenOptions.Audience,
            claims: claimList,
            notBefore: now.UtcDateTime,
            expires: now.Add(_tokenOptions.ValidFor).UtcDateTime,
            signingCredentials: _tokenOptions.SigningCredentials);
        var encodedJwt = _tokenHandler.WriteToken(jwt);

        var response = new TokenEntity()
        {
            AccessToken = encodedJwt,
            ExpiresIn = (int)_tokenOptions.ValidFor.TotalSeconds
        };
        return response;
    }
}

More

默认是基于 HS256 的签名方式,你也可以很轻松地切换成使用基于 RSA 的 RS256 方式,可以自己探索一下

更多实现细节可以参考源码:https://github.com/WeihanLi/WeihanLi.Web.Extensions/tree/dev/src/WeihanLi.Web.Extensions/Authorization/Jwt