我们之前使用session和cookies来进行身份验证,但是cookies是浏览器的产物,它有局限性,无法适用多个不同平台的客户端,JWT可以很好的解决这种问题。
1. JWT是什么
JWT即Json Web Token
,用于服务端身份验证;
整个JWT分为三分部,使用
.
号连接组成
- Header
- Payload
- Signature
Hedaer和Payload需要进行base64编码,使用.
连接Header和Payload进行哈希加密得到Signature
Header: {
"alg": "HS256", // 摘要算法,保证header和payload不被篡改
"typ": "jwt" // token类型
}
// base64编码:ewogICJhbGciOiAiSFMyNTYiLCAKICAidHlwIjogImp3dCIKfQ==
Payload: {
"userid": 1,
"username": "zhangsan",
"iss": "cyj19", // 颁发者
"iat": "1654395001", // 颁发时间
"exp": "1654310111" // 过期时间
}
// base64编码:ewogICJ1c2VyaWQiOiAxLAogICJ1c2VybmFtZSI6ICJ6aGFuZ3NhbiIsCiAgImlzcyI6ICJjeWoxOSIsIAogICJpYXQiOiAiMTY1NDM5NTAwMSIsIAogICJleHAiOiAiMTY1NDMxMDExMSIgCn0=
/*
ewogICJhbGciOiAiSFMyNTYiLCAKICAidHlwIjogImp3dCIKfQ==.ewogICJ1c2VyaWQiOiAxLAogICJ1c2VybmFtZSI6ICJ6aGFuZ3NhbiIsCiAgImlzcyI6ICJjeWoxOSIsIAogICJpYXQiOiAiMTY1NDM5NTAwMSIsIAogICJleHAiOiAiMTY1NDMxMDExMSIgCn0=
进行HS256加密后得到:53ba6671038c3777db4f3abad2eb412bda391c51231073a4abcdaf721a0c1f8a
*/
Signature: 53ba6671038c3777db4f3abad2eb412bda391c51231073a4abcdaf721a0c1f8a
经过上面的编码加密,我们得到token: ewogICJhbGciOiAiSFMyNTYiLCAKICAidHlwIjogImp3dCIKfQ==.ewogICJ1c2VyaWQiOiAxLAogICJ1c2VybmFtZSI6ICJ6aGFuZ3NhbiIsCiAgImlzcyI6ICJjeWoxOSIsIAogICJpYXQiOiAiMTY1NDM5NTAwMSIsIAogICJleHAiOiAiMTY1NDMxMDExMSIgCn0=.53ba6671038c3777db4f3abad2eb412bda391c51231073a4abcdaf721a0c1f8a
那么服务端是如何进行身份验证的呢?其实服务端主要是验证token信息的有效性,通过取出token的Header和Payload部分,使用同样的哈希算法生成摘要信息Signature,然后与token中的摘要信息进行比对即可。
2. 实践
我们使用github.com/golang-jwt/jwt/v4
库进行开发
创建生成token和检验token的方法
type CustomClaims struct {
AppKey map[string]interface{} `json:"app_key"`
jwt.RegisteredClaims
}
func GetSecret(secret string) []byte {
return []byte(secret)
}
func GenerateToken(appKey map[string]interface{}, secret, issuer string, expire time.Duration) (string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(expire)
claims := CustomClaims{
AppKey: appKey,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expireTime),
Issure: issuer,
IssuedAt: jwt.NewNumericDate(nowTime),
},
}
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 使用hmac算法,参数必须是[]byte
return tokenClaims.SignedString([]byte(secret))
}
func ParseToken(token, secret string) (*CustomClaims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return GetSecret(secret), nil
})
if err != nil {
return nil, err
}
if tokenClaims != nil {
claims, ok := tokenClaims.Claims.(*CustomClaims)
// 检验token
if ok && tokenClaims.Valid {
return claims, nil
}
}
return nil, err
}
创建JWT中间件
import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
"strings"
)
func JWT() gin.HandleFunc {
return func(c *gin.Context) {
// 从请求头的Authorization中获取token,格式为"Bearer token"
authorization := c.Request.Header.Get("Authorization")
if authorization == "" {
c.Abort()
c.Writer.Write([]byte("token 为空"))
return
}
// 去除前后两段空格
authorization = strings.TrimSpace(authorization)
// 根据空格分割
parts := strings.SplitN(authorizaton, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
c.Abort()
c.Writer.Write([]byte("无法获取token"))
return
}
// 检验token
claim, err := ParseToken(token, "go-secret")
if err != nil {
str := "token鉴权失败"
if err.(*jwt.ValidationError).Errors == jwt.ValidationErrorExpired {
str = "token已过期"
}
c.Abort()
c.Writer.Write([]byte(str))
return
}
c.Set("sys_user", claim.AppKey)
c.Next()
}
}