Golang - 双Token认证

前言

学会如何在 Golang 开发中使用双Token进行验证。

什么是 JWT ?

jwt :是 json web token 的缩写,jwt 是服务端通过特定算法生成的的一个凭证信息,主要包含 Header(头部)、Payload(载荷)、Signature(签名)。

因为http 是无状态的,所以客户端触发的http请求我们不知道用户的状态信息,比如这个用户已经登陆过了。所以衍生出

令牌头部,记录了整个令牌的类型和签名算法,它是一个描述 jwt元数据的 json 对象

Payload

JWT 第二部分是 Payload,也是一个 Json 对象,除了包含需要传递的数据,还有七个默认的字段供选择。

一般来说 至少要使用 iss 和 exp 。

  • iss (issuer):签发人/发行人
  • sub (subject):主题
  • aud (audience):用户
  • exp (expiration time):过期时间
  • nbf (Not Before):生效时间,在此之前是无效的
  • iat (Issued At):签发时间
  • jti (JWT ID):用于标识该 JWT

SIgnature

JWT 第三部分是签名,主要的生成步骤有以下几点。

  1. 首先需要指定一个 secret,该 secret 仅仅保存在服务器中,保证不能让其他用户知道。这个部分需要 base64URL 加密后的 header 和 base64URL 加密后的 payload 使用
  2. 然后通过header 中声明的加密算法 进行加盐secret组合加密,然后就得出一个签名哈希,也就是Signature,且无法反向解密。

校验jwt

当服务端生成jwt传递给前端后,前端保存 jwt 信息到浏览器本地,后续请求附加到请求头的 Authorization 中,服务端通过 jwt 的 HeaderPayload 用同一套哈希算法和同一个secret 计算签名值,让后把计算结果和前端传递的 jwt 第三段进行比较如果相同那么就通过验证,

小结

jwt 出现场景

jwt 的出现是因为 http 是无状态的,并且因为使用算法进行计算校验相比服务端存储Session 节省更多资源 jwt 能够适用于更多的场景。

jwt 优缺点

优点

  • 持久性:服务端不用存储生成的 jwt 信息,服务器即便重启后也能对历史的 jwt 进行校验。
  • 节省资源:相对于通过 Session 存储用户的会话信息,服务端可以节省更多资源。

缺点

  • 安全性:因为可以存储在客户端,那么数据就可以被篡改,并且 Payload 部分没有加密,只是使用了Base64进行编码,所以 jwt 不能存储敏感信息。
  • 无法中途废弃:jwt 一旦进行签发,那么只能等到该凭据自动过期,无法强制过期,所以使用时应该尽量用合适的过期时间去签发。(双Token解决)
  • 续签问题:jwt 什么时候续签?如果续签频繁就违背了 jwt 的设计初衷,如果不续签,又影响用户体验。(双Token 解决)

Golang:双Token续签

实现思想

根据上面的jwt的缺点,我们可以分析道,jwt 续签才是问题所在,那么只要优雅的解决续签问题,其他的就好说了。

1.短Token

我们简称 Authorization 为Atoken,他是一个包含用户信息的主体Token用来,是用户访问权限校验的真实Toekn。所以短Token的生命周期很短,但是维护要很频繁,我们这里设置过期时间为2小时。

2. 长Token

我们简称 Reflash 为 Rtoken,他是一个用来帮助刷新Atoken的凭证,所以它不需要记录用户的信息,因此它需要一个较长的生命周期来保证维护短Token的更新。我们这里设置过期时间为 7 天

3.代码总览

核心思想就是:Atoken 肯定会先过期,过期后如果在Rtoken的时间范围内,那么重新生成Atoken。以此来进行维护更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package utils

import (
"errors"
"github.com/golang-jwt/jwt/v5"
"time"
)

const (
ATokenExpiredDuration = 2 * time.Hour
RTokenExpiredDuration = 30 * 24 * time.Hour
TokenIssuer = "admin"
)

var (
mySecret = []byte("my Secret Decode")
ErrorInvalidToken = errors.New("verify Token Failed")
)

// PayLoad 载荷,注意不要存放用户敏感数据。
type PayLoad struct {
UserID int64 `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}

func getJWTTime(t time.Duration) *jwt.NumericDate {
return jwt.NewNumericDate(time.Now().Add(t))
}

func keyFunc(token *jwt.Token) (any, error) {
return mySecret, nil
}

// GenToken 颁发token access token 和 refresh token
func GenToken(userID int64, userName string) (atoken, rtoken string, err error) {
// 构建 凭证 基础信息
rc := jwt.RegisteredClaims{
Issuer: TokenIssuer, // 颁发人
ExpiresAt: getJWTTime(ATokenExpiredDuration), // 到期时间
}
// 绑定载荷信息
at := PayLoad{userID, userName, rc}
// 使用SHA256对载荷非对称加密,进行签名和加盐
atoken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, at).SignedString(mySecret)

// refresh token 长token用来刷新,所以不需要载荷。
rt := rc
rt.ExpiresAt = getJWTTime(RTokenExpiredDuration)
rtoken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, rt).SignedString(mySecret)

return atoken, rtoken, err
}

// VerifyToken 验证Token
func VerifyToken(tokenId string) (pl *PayLoad, err error) {
token, err := jwt.ParseWithClaims(tokenId, pl, keyFunc)
if err != nil {
return pl, err
}
// 解析成功后为True
if !token.Valid {
err = ErrorInvalidToken
return nil, err
}
return pl, nil
}

// RefreshToken 通过refresh token 刷新 短token(atoken)
func RefreshToken(atoken, rtoken string) (newAtoken, newRtoken string, err error) {
// rtoken 无效退出
if _, err = jwt.Parse(rtoken, keyFunc); err != nil {
return
}
// 从旧的access token 中解析出 payload 数据信息
var claim PayLoad
// 校验不通过,并且该错误是因为Token过期引起的,那么进行续签。
_, err = jwt.ParseWithClaims(atoken, &claim, keyFunc)
if err == jwt.ErrTokenExpired {
return GenToken(claim.UserID, claim.Username)
}
return
}

Golang - 双Token认证
http://yoursite.com/2023/05/13/Golang-双Token认证/
作者
Meng-Xin
发布于
2023年5月13日
许可协议