JWT配合中间件实现接口访问控制 | 青训营笔记

370 阅读3分钟

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

1 JWT介绍

JSON Web令牌(JWT)是一个开放标准(RFC7519),它定义了一种紧凑且自包含的方式,用于在各方之间作为 JSON对象安全地传输信息。 由于此信息是经过数字签名的,因此可以被验证和信任。 可以使用使用RSA或ECDSA的公用/专用密钥对对JWT进行签名,其格式如下:

image.png

JSON Web令牌(JWT)由三部分组成,每部分用“.”相隔,三个部分分别表示:

  • Header:头部,包含令牌类型和签名算法名称(HMAC SHA256、RSA 等)
  • Payload:有效载荷,用于存储信息,应包含claims
  • Signature:签名,通过Header中申请使用的签名算法,加密Header和Payload数据

我们定义好的header、payload字段需经过base64UrlEncode算法的转换,才能成为token的一部分。

Base64UrlEncode是Base64算法的变种,为什么要变呢,原因是在业界中我们经常可以看到JWT令牌会被放入Header或Query Param中(也就是URL)。

而在URL中,一些个别字符是有特殊意义的,例如:“+”、“/”、“=” 等等,因此在Base64UrlEncode算法中,会对其进行替换,例如:“+” 替换为 “-”、“/” 替换成 “_”、“=” 会被进行忽略处理,以此来保证JWT令牌的在URL中的可用性和准确性。

2 准备

添加JWT依赖,在终端执行代码:

go get github.com/dgrijalva/jwt-go

3 编写功能代码

首先,定义加密密钥和claims。

加密密钥应只有服务提供方自己知道(一般写在配置文件中,此处为方便定义成了变量)!

// jwtSecret is the secret used to encrypt header and payload
var jwtSecret = []byte("this_is_a_secret")

Claims中包含token的配置信息,jwt-go库中预定义的 jwt.StandardClaims 结构体如下:

type StandardClaims struct {
    Audience  string `json:"aud,omitempty"`
    ExpiresAt int64  `json:"exp,omitempty"`
    Id        string `json:"jti,omitempty"`
    IssuedAt  int64  `json:"iat,omitempty"`
    Issuer    string `json:"iss,omitempty"`
    NotBefore int64  `json:"nbf,omitempty"`
    Subject   string `json:"sub,omitempty"`
}

其中包含 ExpiresAt 过期时间、Issuer 发布者等信息。

我们可以直接使用 jwt.StandardClaims,也可以结合应用场景,设计自己的鉴权场景。例如在服务中会频繁应用到当前用户ID,那么我们可以将用户ID加入到claims中:

// Claims of the token
type Claims struct {
   UserId uint
   jwt.StandardClaims
}

注意:敏感信息不要放在claims中!

其次,定义token发放函数和解析函数。

发放函数

// ReleaseToken release a token for specified user
func ReleaseToken(userId uint) (string, error) {
   // token的有效期为7天
   expirationTime := time.Now().Add(7 * 24 * time.Hour)

   claims := &Claims{
      // 自定义字段
      UserId: userId,
      // 标准字段
      StandardClaims: jwt.StandardClaims{
         // 过期时间
         ExpiresAt: expirationTime.Unix(),
         // 发放的时间
         IssuedAt: time.Now().Unix(),
         // 发放者
         Issuer: "Comintern",
         // 主题
         Subject: "user token",
      },
   }

   // 指定加密算法,利用密钥加密header和payload
   token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
   tokenString, err := token.SignedString(jwtSecret)

   if err != nil {
      return "", err
   }

   // 返回token
   return tokenString, nil
}

解析函数

// ParseToken parses a token
func ParseToken(tokenString string) (*jwt.Token, *Claims, error) {
   claims := &Claims{}

   token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (i interface{}, err error) {
      return jwtSecret, nil
   })
   return token, claims, err
}

Claims中解析出的信息方便我们后续使用。

完整脚本

import (
   "github.com/dgrijalva/jwt-go"
   "time"
)

// jwtSecret is the secret used to encrypt header and payload
var jwtSecret = []byte("this_is_a_secret")

// Claims of the token
type Claims struct {
   UserId uint
   jwt.StandardClaims
}

// ReleaseToken release a token for specified user
func ReleaseToken(userId uint) (string, error) {
   // token的有效期为7天
   expirationTime := time.Now().Add(7 * 24 * time.Hour)

   claims := &Claims{
      // 自定义字段
      UserId: userId,
      // 标准字段
      StandardClaims: jwt.StandardClaims{
         // 过期时间
         ExpiresAt: expirationTime.Unix(),
         // 发放的时间
         IssuedAt: time.Now().Unix(),
         // 发放者
         Issuer: "Comintern",
         // 主题
         Subject: "user token",
      },
   }

   // 指定加密算法,利用密钥加密header和payload
   token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
   tokenString, err := token.SignedString(jwtSecret)

   if err != nil {
      return "", err
   }

   // 返回token
   return tokenString, nil
}

// ParseToken parses a token
func ParseToken(tokenString string) (*jwt.Token, *Claims, error) {
   claims := &Claims{}

   token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (i interface{}, err error) {
      return jwtSecret, nil
   })
   return token, claims, err
}

func main() {
   token, err := ReleaseToken(23)
   if err != nil {
      log.Fatal(err)
   }
   fmt.Println(token)
   _, claims, err := ParseToken(token)
   fmt.Println(claims.UserId)
}

运行结果

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjIzLCJleHAiOjE2NTU0NTg1MDgsIml
hdCI6MTY1NDg1MzcwOCwiaXNzIjoiQ29taW50ZXJuIiwic3ViIjoidXNlciB0b2tlbiJ9.y84lEZEs64
v8S-qCg_DK8xsYncrkN3MTgWha9u3scY4
23

为方便使用,我们可将token验证过程写成中间件。