golang实现jwt | 青训营笔记

115 阅读4分钟

这是我参与「第五届⻘训营 」笔记创作活动的第19天。

这篇文章记录了项目中jwt(JSON Web Token)验证的实现。

什么是JWT

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案。

传统的用户认证流程,即服务器在session中保存登录用户信息,返回给用户id,用户在每次访问服务器时每次将id放入cookie中发送给服务端,服务端根据id验证用户登录状态。

这种解决方案的一个弊端是,如果系统部署在集群中,用户访问的机器不定,所有的机器都需要保存登录的用户信息,造成了服务端实现上的不便。

一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。

另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。

jwt原理

jwt的原理是,服务器完成验证后,生成一个json对象,返回给用户,之后每次通信时候,都需要发送这个json对象。为防止用户篡改数据,服务器生成这个对象时候会加上签名。

jwt结构

实际的jwt大概是这样

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlhpYW9NaW4iLCJleHAiOjE2NzY0NDM0OTl9.zhsKFmV3gdmcqQ81B6Woq10JmX1l56GQUeyOP44L6lo

分为三个部分,由‘.’隔开,分别为Header、Payload、Signature。

Header

Header部分是一个Json对象,描述JWT元数据,通常是这样

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

alg表示签名算法,typ属性表示令牌类型,jwt令牌写作JWT。实际的jwt中此部分使用base64URL算法转成字符,此步骤不加密。

Payload

此部分也是一个JSON对象,存放实际需要传递数据。 官方规定字段。

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号 除了官方字段自己也可以存放一些私有字段,当然由于jwt不加密,不要放密码之类的敏感信息。 同样此部分也是用base64URL转为字符串。

Signature

Signature 部分是对前两部分的签名,防止数据篡改。

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

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

算出签名后,将三部分使用点拼接成一个字符串返回给用户。

jwt客户端使用方法

客户端收到服务器返回的JWT之后,每次与服务器通信都需要带上JWT,如果放到cookie中不可以跨域,建议放到HTTP信息头的Authorization字段里面。

golang中jwt

使用jwt-go来方便的进行jwt令牌发放和验证。

安装

go get -u github.com/golang-jwt/jwt/v4

导入

import "github.com/golang-jwt/jwt/v4"

生成令牌

mySigningKey := []byte("AllYourBase")

type MyCustomClaims struct {
	Foo string `json:"foo"`
	jwt.RegisteredClaims
}

// Create the claims
claims := MyCustomClaims{
	"bar",
	jwt.RegisteredClaims{
		// A usual scenario is to set the expiration time relative to the current time
		ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
		IssuedAt:  jwt.NewNumericDate(time.Now()),
		NotBefore: jwt.NewNumericDate(time.Now()),
		Issuer:    "test",
		Subject:   "somebody",
		ID:        "1",
		Audience:  []string{"somebody_else"},
	},
}

// Create claims while leaving out some of the optional fields
claims = MyCustomClaims{
	"bar",
	jwt.RegisteredClaims{
		// Also fixed dates can be used for the NumericDate
		ExpiresAt: jwt.NewNumericDate(time.Unix(1516239022, 0)),
		Issuer:    "test",
	},
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString(mySigningKey)
fmt.Printf("%v %v", ss, err)

output:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.xVuY2FZ_MRXMIEgVQ7J-TFtaucVFRXUzHm9LmV41goM <nil>

此处在登录或注册时候将生成的令牌返回给客户端,以便客户端之后的使用。

解析

// Token from another example.  This token is expired
var tokenString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c"

token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
	return []byte("AllYourBase"), nil
})

if token.Valid {
	fmt.Println("You look nice today")
} else if errors.Is(err, jwt.ErrTokenMalformed) {
	fmt.Println("That's not even a token")
} else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
	// Token is either expired or not active yet
	fmt.Println("Timing is everything")
} else {
	fmt.Println("Couldn't handle this token:", err)
}

output:

Timing is everything

gin验证中间件实现

// JWTAuthMiddleware 基于JWT的认证中间件--验证用户是否登录
func JWTAuthMiddleware() func(c *gin.Context) {
   return func(c *gin.Context) {
      authHeader := c.Request.Header.Get("authorization")
      if authHeader == "" {
         c.JSON(http.StatusUnauthorized, gin.H{
            "code": 2003,
            "msg":  "请求头中auth为空",
         })
         c.Abort()
         return
      }
      // 按空格分割
      parts := strings.Split(authHeader, ".")
      if len(parts) != 3 {
         c.JSON(http.StatusUnauthorized, gin.H{
            "code": 2004,
            "msg":  "请求头中auth格式有误",
         })
         c.Abort()
         return
      }
      mc, ok := ParseToken(authHeader, Secret)
      if ok == false {
         c.JSON(http.StatusUnauthorized, gin.H{
            "code": 2005,
            "msg":  "无效的Token",
         })
         c.Abort()
         return
      }
      m := mc.(jwt.MapClaims)
      // 将当前请求的username信息保存到请求的上下文c上
      c.Set("username", m["username"])
      c.Next() // 后续的处理函数可以用过c.Get("username")来获取当前请求的用户信息
   }
}