更轻易地实现一个 Jwt Server
Intro
最近在多个项目中都有用到 Jwt Token 认证,就想着把之前项目里 Jwt Token 的使用封装一下,以便于之后集成起来更加地方便,不用再拷贝代码了
JWT
JWT 是 JSON Web Token 的缩写,是目前最流行的基于 Token 的认证解决方案,JWT 是一种无状态的认证方式,我们不需要依赖 Session,更为简单,对于随时准备扩容缩容的云原生应用来说更加的友好。
JWT token 的格式分成三个部分,他们之间以 “.” 作为分隔
Header(头部)
Payload(负载)
Signature(签名)
我们可以在 https://jwt.io 网站上查看 token 的内容,也可以自己写个小工具来查看,token 的内容是 base64 URL 编码的
JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符
+
、/
和=
,在 URL 里面有特殊含义,所以要被替换掉:=
被省略、+
替换成-
,/
替换成_
。这就是 Base64URL 算法。
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:
VaidateToken:
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
下一篇文章我们介绍如何使用 RefreshToken
References
https://github.com/WeihanLi/SparkTodo/blob/master/SparkTodo.API/Program.cs#L40
https://github.com/WeihanLi/WeihanLi.Web.Extensions/blob/dev/samples/WeihanLi.Web.Extensions.Samples/Program.cs#L40
https://github.com/WeihanLi/WeihanLi.Web.Extensions