Token 认证模式
鉴于基于 Session
的会话管理⽅式存在上述多个缺点,基于
Token
的⽆状态会话管理⽅式诞⽣了,所谓⽆状态,就是服务端可以不再存储信息,甚⾄是不再存储 Session
,逻辑如下。
- 客户端使⽤⽤户名、密码进⾏认证
- 服务端验证⽤户名、密码正确后⽣成 Token 返回给客户端
- 客户端保存 Token,访问需要认证的接⼝时在 URL 参数或 HTTP Header 中加⼊ Token
- 服务端通过解码 Token 进⾏鉴权,返回给客户端需要的数据
基于 Token
的会话管理⽅式有效解决了基于
Session
的会话管理⽅式带来的问题。
- 服务端不需要存储和⽤户鉴权有关的信息,鉴权信息会被加密到 Token 中,服务端只需要读取 Token 中包含的鉴权信息即可
- 避免了共享 Session 导致的不易扩展问题
- 不需要依赖 Cookie,有效避免 Cookie 带来的 CSRF 攻击问题
- 使⽤ CORS 可以快速解决跨域问题
JWT介绍
JWT 是
JSON Web Token
的缩写,是为了在⽹络应⽤环境间传递声明⽽执⾏的⼀种基于
JSON
的开放标准(
(RFC 7519
)
。
JWT
本身没有定义任何技术实现,它只是定义了⼀种基于
Token
的会话管理的规则,涵盖 Token
需要包含的标准内容和
Token
的⽣成过程,特别适⽤于分布式站点的单点登录(
SSO)
场景。
⼀个 JWT Token 就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyODAxODcyNzQ4ODMyMzU4NSwiZ
XhwIjoxNTk0NTQwMjkxLCJpc3MiOiJibHVlYmVsbCJ9.lk_ZrAtYGCeZhK3iupHxP1kgjBJzQTVTtX
0iZYFx9wU
它是由
.
分隔的三部分组成,这三部分依次是:
- 头部(Header)
- 负载(Payload)
- 签名(Signature)
头部和负载以
JSON
形式存在,这就是
JWT
中的
JSON
,三部分的内容都分别单独经过了
Base64
编码,以 .
拼接成⼀个
JWT Token
。
Header
JWT
的
Header
中存储了所使⽤的加密算法和
Token
类型
{
"alg": "HS256",
"typ": "JWT"
}
Payload
Payload
表示负载,也是⼀个
JSON
对象,
JWT
规定了
7
个官⽅字段供选⽤,
iss (issuer):签发⼈
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):⽣效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官⽅字段,开发者也可以⾃⼰指定字段和内容,例如下⾯的内容。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
注意,
JWT
默认是不加密的,任何⼈都可以读到,所以不要把秘密信息放在这个部分。这个
JSON
对象 也要使⽤ Base64URL
算法转成字符串。
Signature
Signature
部分是对前两部分的签名,防⽌数据篡改。
⾸先,需要指定⼀个密钥(
secret
)。这个密钥只有服务器才知道,不能泄露给⽤户。然后,使⽤
Header
⾥⾯指定的签名算法(默认是
HMAC SHA256
),按照下⾯的公式产⽣签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)
JWT
优缺点
JWT 拥有基于
Token
的会话管理⽅式所拥有的⼀切优势,不依赖
Cookie
,使得其可以防⽌
CSRF
攻击,也能在禁⽤ Cookie
的浏览器环境中正常运⾏。
⽽ JWT
的最⼤优势是服务端不再需要存储
Session
,使得服务端认证鉴权业务可以⽅便扩展,避免存储 Session 所需要引⼊的
Redis
等组件,降低了系统架构复杂度。但这也是
JWT
最⼤的劣势,由于有效期 存储在 Token
中,
JWT Token
⼀旦签发,就会在有效期内⼀直可⽤,⽆法在服务端废⽌,当⽤户进⾏登出操作,只能依赖客户端删除掉本地存储的 JWT Token
,如果需要禁⽤⽤户,单纯使⽤
JWT
就⽆法做到了。
基于
jwt
实现认证实践
前⾯讲的 Token
,都是
Access Token
,也就是访问资源接⼝时所需要的
Token
,还有另外⼀种 Token,
Refresh Token
,通常情况下,
Refresh Token
的有效期会⽐较⻓,⽽
Access Token
的有效期⽐较短,当 Access Token
由于过期⽽失效时,使⽤
Refresh Token
就可以获取到新的
Access Token
,如果 Refresh Token
也失效了,⽤户就只能重新登录了。
在 JWT
的实践中,引⼊
Refresh Token
,将会话管理流程改进如下。
- 客户端使⽤⽤户名密码进⾏认证
- 服务端⽣成有效时间较短的 Access Token(例如 10 分钟),和有效时间较⻓的 Refresh
- Token(例如 7 天)
- 客户端访问需要认证的接⼝时,携带 Access Token
- 如果 Access Token 没有过期,服务端鉴权后返回给客户端需要的数据
- 如果携带 Access Token 访问需要认证的接⼝时鉴权失败(例如返回 401 错误),则客户端使⽤
- Refresh Token 向刷新接⼝申请新的 Access Token
- 如果 Refresh Token 没有过期,服务端向客户端下发新的 Access Token
- 客户端使⽤新的 Access Token 访问需要认证的接⼝
后端需要对外提供⼀个刷新
Token
的接⼝,前端需要实现⼀个当
Access Token
过期时⾃动请求刷新
Token
接⼝获取新
Access Token
的拦截器。
gin
框架使⽤
jwt
在gin框架中使用JWT | 李文周的博客 (liwenzhou.com)
鉴权中间件开发
const (
ContextUserIDKey = "userID"
)
var (
ErrorUserNotLogin = errors.New("当前⽤户未登录")
)
⽣成access token和refresh token
// JWTAuthMiddleware 基于JWT的认证中间件
func JWTAuthMiddleware() func(c *gin.Context) {
return func(c *gin.Context) {
// 客户端携带Token有三种⽅式 1.放在请求头 2.放在请求体 3.放在URI
// 这⾥假设Token放在Header的中
// 这⾥的具体实现⽅式要依据你的实际业务情况决定
authHeader := c.Request.Header.Get("Auth")
if authHeader == "" {
ResponseErrorWithMsg(c, CodeInvalidToken, "请求头缺少Auth Token")
c.Abort()
return
}
mc, err := utils.ParseToken(authHeader)
if err != nil {
ResponseError(c, CodeInvalidToken)
c.Abort()
return
}
// 将当前请求的username信息保存到请求的上下⽂c上
c.Set(ContextUserIDKey, mc.UserID)
c.Next() // 后续的处理函数可以⽤过c.Get("userID")来获取当前请求的⽤户信息
}
}
⽣成
access token
和
refresh token
// GenToken ⽣成access token 和 refresh token
func GenToken(userID int64) (aToken, rToken string, err error) {
// 创建⼀个我们⾃⼰的声明
c := MyClaims{
userID, // ⾃定义字段
jwt.StandardClaims{
ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间
Issuer: "bluebell", // 签发⼈
},
}
// 加密并获得完整的编码后的字符串token
aToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256,
c).SignedString(mySecret)
// refresh token 不需要存任何⾃定义数据
rToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Second * 30).Unix(), // 过期时间
Issuer: "bluebell", // 签发⼈
}).SignedString(mySecret)
// 使⽤指定的secret签名并获得完整的编码后的字符串token
return
}
解析
access token
// ParseToken 解析JWT
func ParseToken(tokenString string) (claims *MyClaims, err error) {
// 解析token
var token *jwt.Token
claims = new(MyClaims)
token, err = jwt.ParseWithClaims(tokenString, claims, keyFunc)
if err != nil {
return
}
if !token.Valid { // 校验token
err = errors.New("invalid token")
}
return
}
refresh token
// RefreshToken 刷新AccessToken
func RefreshToken(aToken, rToken string) (newAToken, newRToken string, err
error) {
// refresh token⽆效直接返回
if _, err = jwt.Parse(rToken, keyFunc); err != nil {
return
}
// 从旧access token中解析出claims数据
var claims MyClaims
_, err = jwt.ParseWithClaims(aToken, &claims, keyFunc)
v, _ := err.(*jwt.ValidationError)
// 当access token是过期错误 并且 refresh token没有过期时就创建⼀个新的access token
if v.Errors == jwt.ValidationErrorExpired {
return GenToken(claims.UserID)
}
return
}