JWT Token | 青训营笔记

354 阅读5分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第二篇笔记。

1. 遇到的问题

在项目期间,注册用户在下次登录的时候,经常需要重新输入用户名和密码比较麻烦,于是引入jwt Token。

2. 什么原因导致这个问题

因为http协议本身是一种无状态的协议,而这就导致用户每次请求时,都需要携带用户名和密码来进行用户认证,因为根据http协议,我们不清楚是哪个用户发的请求。

3. 解决方法

我们可以在服务器当中,存一份用户登录的信息,这份登录信息会在响应的时候发送给应用,这样应用就能知道识别是哪个用户请求的。这是传统的session认证

Cookie-Session原理

image.png

session缺点

通常session保存在内存当中,这就意味着每次用户认证,都要请求这台服务器,服务器的开销比较大,这在分布式应用上,限制了负载均衡的能力。在分布式当中,你在一台服务器做了认证,并且该服务器有你的session,但是当你的访问被打到另一个服务器时,你还是要重新登录。

为了解决上述问题,项目当中采用的是JWT Token

JWT Token原理

image.png

JWT优点

性能方面: Session最大的缺点就是存储在服务器中,所以占用大量服务器内存,token把压力给到客户端,可以明显减轻服务器内存压力。

分布式:例如在一个集群(3台服务器),每次用户登录,访问服务器。如果是传统的Session那么,该Session只会保存在一台服务器上,而Token就无所谓。

安全性: token需要签名,所以安全性相对高。

是否跨域:token可以跨域访问。

JWT缺点

  • jwt不适合存放大量信息,因为信息越长,token越长。
  • jwt token设置的过期时间是死的,不能修改。

4. 项目中JWT Token的使用

JWT数据结构

eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIi0iIxMjM0NTY30DkwIiwi bmFtZSI6IkpvaG4gRG91IiwiaXNTb2NpYWwi0nRydWV9.
4pcPyMD09olPSyXnrXCj TwXyr4BsezdI1AVTmud2fU4

token是一段很长的字符串,中间用.分割成三部分。

Header    (头部)
Payload   (负载)
Signature (签名)
​
//token是一行
Header.Payload.Signature

image.png

Header

{
  "alg": "HS256",
  "typ": "JWT"
}

alg:签名算法,默认是HS256

typ:表示这个令牌(token)的类型,jwt令牌,类型就是jwt

Payload

官方字段:

iss (issuer):         签发人
exp (expiration time):过期时间
sub (subject):        主题
aud (audience):       受众
nbf (Not Before):     生效时间
iat (Issued At):      签发时间
jti (JWT ID):         编号

自定义字段:除了官方字段,还可以加上自定义字段,用于实际需求

{
  "sub": "1111111",
  "name": "小呆鸟",
  "admin": true
}

Signature

Signature主要是对前俩部分的签名,防止数据篡改

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是HS256)产生签名。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

Base64URL

Header 和 Payload 串型化的算法是 Base64URL

5. JWT的使用方式

客户端携带Token有三种方式 :

  • 放在请求头 (Token放在Header的Authorization中,并使用Bearer开头)

  • 放在请求体

  • 放在URI

    注意:客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面

6.代码

定义密钥

var (
    Secret = []byte("TikTok")
)

生成token

type JWTClaims struct {                 
    UserId   int64  `json:"user_id"`         // 创建一个我们自己的声明
    Username string `json:"user_name"`        // 创建一个我们自己的声明
    jwt.RegisteredClaims
}
​
//生成token
func GenToken(userid int64, userName string) (string, error) {
    claims := JWTClaims{
        UserId:   userid,                   // 自定义字段
        Username: userName,                 // 自定义字段
        RegisteredClaims: jwt.RegisteredClaims{
            Issuer: "server",
            //ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(),可用于设定token过期时间
        },
    }
    
    //// 使用指定的签名方法创建签名对象
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    
    //// 使用指定的secret签名并获得完整的编码后的字符串token
    signedToken, err := token.SignedString([]byte("TikTok"))
    if err != nil {
        return "", err
    }
    return signedToken, nil
}

解析token

//解析token
func ParsenToken(tokenString string) (*JWTClaims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (i interface{}, err error) {
        return Secret, nil
    })
    if err != nil {
        return nil, err
    }
    // 对token对象中的Claim进行类型校验
    if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
        return claims, nil
    }
    return nil, errors.New("invalid token")
}
​

验证token

func VerifyToken(tokenString string) (int64, error) {
​
    log.Debugf("tokenString:%v", tokenString)
​
    if tokenString == "" {
        return int64(0), nil
    }
    claims, err := ParsenToken(tokenString)
    if err != nil {
        return int64(0), err
    }
    return claims.UserId, nil
}

token鉴权放当做中间件

这样不用频繁的访问服务器,在中间件如果核实不对,就直接返回错误

func AuthMiddleware() gin.HandlerFunc {
​
    return func(c *gin.Context) {
        tokenString := c.PostForm("token")
        if tokenString == "" {
            tokenString = c.Query("token")
        }
​
        //tokenString := c.Query("token")
        userId, err := VerifyToken(tokenString)
        if err != nil || userId == int64(0) {
            response.Fail(c, "auth error", nil)
            c.Abort()
        }
    // 将当前请求的username信息保存到请求的上下文c上
        c.Set("UserId", userId)
    // 后续的处理函数可以用过c.Get("userId")来获取当前请求的用户信息
        c.Next()
    }
}

参考文章链接:jwt.io/introductio…