Ajoutez la fonctionnalité jwt à votre projet Gin

Qu'est-ce que JWT

Le nom complet de JWT est JSON Web Token, qui est une solution d'authentification inter-domaine. Il appartient à une norme ouverte. Il spécifie une méthode de mise en œuvre de jeton. Actuellement, il est principalement utilisé dans les projets de séparation front-end et back-end et Scénarios commerciaux OAuth2.0.

Pourquoi utiliser JWT dans votre Gin

Le mode Cookie-Sesson traditionnel occupe la mémoire du serveur et a une faible évolutivité. Si vous rencontrez un cluster ou un scénario de vérification interservices, vous devez prendre en charge la réplication ou la persistance de sesson

Fondamentaux de JWT

Après l'authentification du serveur, obtenir les informations utilisateur JSON

{
     "user_id": "xxxxxx",
    "role": "xxxxxx",
    "refresh_token": "xxxxx"
}

Comment est composé JWT TOKEN ?

JWT est une très longue chaîne

eyJhbGciOiJI123afasrwrqqewrcCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

image.png

Il se compose de trois parties, chaque partie est .séparée par un point ( ), les trois parties sont les suivantes

  • Entête
  • Charge utile
  • Signature

Entête

L'en-tête est un objet JSON chiffré par l'algorithme BASE64URL, déchiffré comme suit

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

Parmi eux, l'  algattribut indique l'algorithme utilisé pour la signature, et la valeur par défaut est HS256 ;

typIl indique le type du jeton actuel et le type de JWT estjwt

Base64URL

Semblable à BASE64, étant donné que ces symboles +, /, =, ont des significations particulières dans les URL, l'algorithme BASE64RUL remplace ces symboles

+ -> -

= -> ignoré

/ -> _

Charge utile

La partie Payload est également un objet JSON converti en chaîne par l'algorithme BASE64URL. La partie Payload contient 7 champs de base

  • iss (émetteur): émetteur
  • exp (délai d'expiration) : délai d'expiration
  • sub (sujet): sujet
  • aud (audience): audience
  • nbf (Pas avant) : temps effectif
  • iat (émis à): Émis à
  • jti (identifiant JWT): nombre

Vous pouvez également y insérer des champs métier personnalisés, comme suit

  • identifiant d'utilisateur
  • rôle

Signature

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

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

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

解密过程

当系统接收到TOKEN时, 拿出Header和Payload的字符串用.拼接在一起之后, 用Header里面指定的哈希方法通过公式

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

进行加密得出密文

然后用刚刚得出的密文与TOKEN传过来的密文对比, 如果相等则表明密文没有更改.

一些特点(优点和缺点)

  1. JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
  2. JWT 不加密的情况下,不能将秘密数据写入 JWT。
  3. JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
  4. JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  5. JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  6. 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

GIN整合JWT

go get -u github.com/dgrijalva/jwt-go
go get github.com/gin-gonic/gin

编写jwtutil

var Secret = []byte("whatasecret")

// jwt过期时间, 按照实际环境设置
const expiration = 2 * time.Minute

type Claims struct {
	// 自定义字段, 可以存在用户名, 用户ID, 用户角色等等
	Username string
	// jwt.StandardClaims包含了官方定义的字段
	jwt.StandardClaims
}

func GenToken(username string) (string, error) {
	// 创建声明
	a := &Claims{
		Username: username,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: time.Now().Add(expiration).Unix(), // 过期时间
			IssuedAt:  time.Now().Unix(),                 // 签发时间
			Issuer:    "gin-jwt-demo",                    // 签发者
			Id:        "",                                // 按需求选这个, 有些实现中, 会控制这个ID是不是在黑/白名单来判断是否还有效
			NotBefore: 0,                                 // 生效起始时间
			Subject:   "",                                // 主题
		},
	}
	
	// 用指定的哈希方法创建签名对象
	tt := jwt.NewWithClaims(jwt.SigningMethodHS256, a)
	// 用上面的声明和签名对象签名字符串token
	// 1. 先对Header和PayLoad进行Base64URL转换
	// 2. Header和PayLoadBase64URL转换后的字符串用.拼接在一起
	// 3. 用secret对拼接在一起之后的字符串进行HASH加密
	// 4. 连在一起返回
	return tt.SignedString(Secret)
}

func ParseToken(tokenStr string) (*Claims, error) {
	// 第三个参数: 提供一个回调函数用于提供要选择的秘钥, 回调函数里面的token参数,是已经解析但未验证的,可以根据token里面的值做一些逻辑, 如`kid`的判断
	token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		return Secret, nil
	})
	if err != nil {
		return nil, err
	}
	// 校验token
	if claims, ok := token.Claims.(*Claims); ok && token.Valid {
		return claims, nil
	}
	return nil, errors.New("invalid token")
}

Secret是秘钥, 用于加密签名

expiration是TOKEN过期时间

Claims是签名声明对象, 包含自定义的字段和JWT规定的字段

type Claims struct {
	// 自定义字段, 可以存在用户名, 用户ID, 用户角色等等
	Username string
	// jwt.StandardClaims包含了官方定义的字段
	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"`
}

GenToken方法

GenToken方法为某个username生成一个token, 每次生成都不一样

jwt.NewWithClaims(jwt.SigningMethodHS256, a)声明了一个签名对象, 并且指定了HS256的哈希算法

token.SignedString(Secret)表明用刚刚的声明对象和SECRET利用指定的哈希算法去加密,包含下面流程

  1. 先对Header和PayLoad进行Base64URL转换
  2. Header和PayLoadBase64URL转换后的字符串用.拼接在一起
  3. 用secret对拼接在一起之后的字符串进行HASH加密
  4. BASE64URL(Header).BASE64URL(Payload).signature连在一起的字符串返回
func GenToken(username string) (string, error) {
	// 创建声明
	a := &Claims{
		Username: username,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: time.Now().Add(expiration).Unix(), // 过期时间
			IssuedAt:  time.Now().Unix(),                 // 签发时间
			Issuer:    "gin-jwt-demo",                    // 签发者
			Id:        "",                                // 按需求选这个, 有些实现中, 会控制这个ID是不是在黑/白名单来判断是否还有效
			NotBefore: 0,                                 // 生效起始时间
			Subject:   "",                                // 主题
		},
	}
	
	// 用指定的哈希方法创建签名对象
	tt := jwt.NewWithClaims(jwt.SigningMethodHS256, a)
	// 用上面的声明和签名对象签名字符串token
	// 1. 先对Header和PayLoad进行Base64URL转换
	// 2. Header和PayLoadBase64URL转换后的字符串用.拼接在一起
	// 3. 用secret对拼接在一起之后的字符串进行HASH加密
	// 4. 连在一起返回
	return tt.SignedString(Secret)
}

ParseToken方法

ParseToken方法解析一个Token, 并验证Token是否生效

jwt.ParseWithClaims方法, 用于解析Token, 其第三个参数:

提供一个回调函数用于提供要选择的秘钥, 回调函数里面的token参数,是已经解析但未验证的,可以根据token里面的值做一些逻辑, 如判断kid来选用不同的secret

KID(可选): 代表秘钥序号。开发人员可以用它标识认证token的某一秘钥

func ParseToken(tokenStr string) (*Claims, error) {
	// 第三个参数: 提供一个回调函数用于提供要选择的秘钥, 回调函数里面的token参数,是已经解析但未验证的,可以根据token里面的值做一些逻辑, 如`kid`的判断
	token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		return Secret, nil
	})
	if err != nil {
		return nil, err
	}
	// 校验token
	if claims, ok := token.Claims.(*Claims); ok && token.Valid {
		return claims, nil
	}
	return nil, errors.New("invalid token")
}

编写中间件

从Header中取出Authorization并拿去解析jwt.ParseToken,

验证token是否被串改, 是否过期

从token取出有效信息并设置到上下文

func JWTAuthMiddleware() func(ctx *gin.Context) {
	return func(ctx *gin.Context) {
		// 根据实际情况取TOKEN, 这里从request header取
		tokenStr := ctx.Request.Header.Get("Authorization")
		if tokenStr == "" {
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
				"code": code.ERR_AUTH_NULL,
				"msg":  code.GetMsg(code.ERR_AUTH_NULL),
			})
			return
		}
		
		claims, err := jwt.ParseToken(tokenStr)
		if err != nil {
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
				"code": code.ERR_AUTH_INVALID,
				"msg":  code.GetMsg(code.ERR_AUTH_INVALID),
			})
			return
		} else if time.Now().Unix() > claims.ExpiresAt {
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
				"code": code.ERR_AUTH_EXPIRED,
				"msg":  code.GetMsg(code.ERR_AUTH_EXPIRED),
			})
			return
		}
		
		// 此处已经通过了, 可以把Claims中的有效信息拿出来放入上下文使用
		ctx.Set("username", claims.Username)
		ctx.Next()
	}
}

使用中间件

/login不用中间件

中间件指定在authorizedrouter, 因此authorized下的所有路由都会使用此中间件

func main() {
	r := gin.Default()
	
	r.POST("/login", router.Login)
	
	authorized := r.Group("/auth")
	authorized.Use(jwt.JWTAuthMiddleware())
	{
		authorized.GET("/getUserInfo", router.GetUserInfo)
	}
	
	r.Run(":8082")
}

测试

login请求获取token

POST http://localhost:8082/login

image-20210528152311032

把token放入getUserInfo请求

GET http://localhost:8082/auth/getUserInfo

image-20210528152537635

其他

完整的JWT登录还应该包括

  1. 使TOKEN失效(过期或者黑名单等功能)
  2. refresh token

通过上述流程你就能快速的在你的Go Gin项目中应用JWT能力

本文正在参加技术专题18期-聊聊Go语言框架

Je suppose que tu aimes

Origine juejin.im/post/7120796419836870669
conseillé
Classement