使用 JWT 实现会话认证
基于 JWT 来实现 Token 认证很简单,相对复杂的签名认证算法都已经封装到工具包里,使用起来很容易。
而真正造成 JWT 的应用困难其实是在适应业务上,并不是所有业务都适合原生的 JWT 特性。我们很多时候选型使用 JWT 看重的是其无状态和校验方便的优点,但在实际的业务场景中经常是要 Revoke 功能的。
如果要实现 Token 的 Revoke 那 Token 会变成有状态,这让人使用起来非常纠结。 我都有状态了为啥还要用 JWT?直接生成一个 ID 存 Redis 不更简单?直接用 Redis 缓存有效性来控制 Token 的有效性,失效删除就好了。
最终选择使用 JWT 作为 Token 主要是基于以下考虑:
- JWT 实现完善,有丰富的开发工具包;
- 不需要自己再设计一套 Token 结构和校验算法;
- 可以利用 JWT 进行前置验证(JWT 正确性、是否超时失效),避免每次都查询缓存;
- JWT 可以基于 Payload 存储数据,使用很灵活,可以提供保存一些数据在 Token 中避免查询;
- JWT 签名方法很丰富,能满足以后演进的需要,能实现一处签发,多处验证。
功能点
使用 JWT 作为认证 Token 应该需要满足我们以下几个需求(部分需求为我当前业务需要的功能,在其它系统中并不常见)。
Token 包含用户基本信息
包含用户的基本信息保证了登录认证时,系统可以通过 Token 携带的信息确定用户的身份。JWT 的 Payload 中可以携带信息,我们可以把用户的 uid 等信息存储在 payload 中。
防止篡改
非法用户不能通过伪造或修改 Payload 的方式,篡改 Token 代表的实例。JWT 基于签名实现的校验,如果内容被篡改,签名校验无法通过,所以可以有效防止篡改。
支持不同有效期
我正在开发的系统需要两种类型的 Token,一种是短期 Token,用户使用 API 可以生成;一种是长期 Token,由管理员通过管理系统生成。
JWT 在生成时支持指定签名有效期,可以生成需要的 Token。
长期 Token 需要支持 Revoke
JWT 通常都是无状态的,因为 JWT 一般都只生成短期 Token,有效期也只是几个小时。就算 Token 泄漏也只是影响短暂的时间,过期后就自动失效,所以也不会有 Revoke 的需求。
这是 JWT 的一个优点,无需专门的维护和存储 Token。但在里需要生成长期 Token,生成后如果无状态将无法保证安全,被盗用后的不能失效会产生持续的安全风险。
为了实现 Revoke,很显然需要对 Token 进行持久化操作。可以将 Token 按照有效期不同进行区分:
- 短期 Token:无状态管理,仅能通过 API 生成,不支持 Revoke;
- 长期 Token:有状态管理,由管理员在管理后台生成,生成时写表保存,通过 Redis 缓存 Token 数据,支持 Revoke。
当对长期 Token 进行 Revoke 操作时,同步修改表数据和 Token 缓存中的 Token 状态,实时失效。
Token 在校验时,分为以下步骤:
- 先校验 JWT 签名
- 根据 Token 失效时间,如果是长期 Token,查询缓存是否实效。
实现
JWT 相关实现依赖于第三方的 Golang 包 github.com/golang-jwt/jwt/v4
,通过以下方式获取:
1 | go get github.com/golang-jwt/jwt/v4 |
首先实现 JWT 的 Payload 结构,用来存储我们的 Payload 数据。Payload 在这个包中叫作 Claims,是一个接口。
1 | type Token struct { |
在 Payload 中除了用户的信息,还包括了 Token Id,用于方便标识 Token。可以快速定位到 Token 的缓存信息,方便查询有效性。
jwt.RegisteredClaims
是 JWT 包中的结构本,该结构体实现了 Claims
接口,通过引入这个结构体可以让 jwt 在解析的时候自动把 Payload 的数据写到我们的字段中。
校验
方法实现如下:
1 | func (j *JwtChecker) Verify(token string) (*ptoken.Token, error) { |
- 方法首先通过
Keyfunc
校验签名方法是否 HMAC - 如果
ParseWithClaims
没错误,那么 Token 正确解析 - 判断 Token 是否有效并进行类型转换
长期 Token 判断
从解析好的 Token 对象中可以直接获取到 Token 的失效时间。
1 | func (t *Token) IsLongToken() bool { |
如果 Token 是长期 Token,我们可以去缓存中查询 token 是否有效。
生成 Token
生成 Token 很简单,只需要依次调用 NewWithClaims
和 SignedString
就能生成签名后的 Token,这里使用 HmacSHA256 的签名算法。
1 | func DoGenerateToken(token *Token, secret string, expire time.Duration) (*TokenHolder, error) { |
Token 的失效时间需要在签名前手动设置到 ExpiresAt
字段,并不是以某个参数的方式提供的。
总结
本文介绍的和一般 JWT 用法并无多少区别,JWT 大家也都是这样用的。而对于需要 Revoke 这个功能,我这里是使用有效时间,将其区分成两种情况。
对于短期 Token,使用的还是 JWT 常规方法;对于长期 Token 则进行持久化,并对 Token 的信息进行多级缓存。
在我们的使用场景中,长期 Token 是非常少的,而长期 Token 的 Revoke 就更少了,所以查询持久化的 Token 信息并不会很多。
如果未来有演进需要,大量使用长期 Token,也可以将 Token 信息缓存调整成为 Revoke 黑名单列表的方式。只对 Revoke 的 Token 进行记录和查询,毕竟 Revoke 的数据终究还是少数。