JWT

jwt,即json-web-token,是一种轻量级的认证的令牌,通常用于身份认证和授权。由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。

  • 头部(Header):JWT 的头部包含了两部分信息:令牌类型(typ)和签名算法(alg),用 Base64 编码后的字符串表示。
  • 载荷(Payload):JWT 的载荷包含了一些声明(Claims),用来表示一些实体(主题、发行者、过期时间等)和一些元数据。需要注意的是,载荷中的信息是可以被解码的,因此不要在 JWT 中存储敏感信息。
  • 签名(Signature):JWT 的签名用于验证消息的完整性和真实性。签名的过程需要使用头部和载荷中的信息,以及一个秘钥,然后通过指定的算法进行签名。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOjEzMzA4MDgxNTYyMDg1Mzc2MCwidXNlcm5hbWUiOiJ3d3ciLCJleHAiOjE2ODcwOTgxNzksImlzcyI6IndlYi1hcHAifQ.29KktS5R7sZt_K23CfAElgusmrHw8rbsp8ftKoVkWsc

这是一个token,分为3段,每一段都被.隔开,前两段分别由头部和载荷Base64URL转化为字符串,由.连接起来,最后使用特定算法对前2段和一个特定的secret拼凑起来加密。

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

如下图是上面token的一个解密。

使用jwt完成登陆认证

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//token的生成与解析
package auth

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

//结构体,也就是jwt中对应的payload,可以自定义
type MyClaim struct {
	Userid   int64  `json:"userid"`
	Username string `json:"username"`
	jwt.StandardClaims
}

//secret
var CustomSecret = []byte("happy")

var InvalidToken = errors.New("invalid token")

//生成token
func GetToken(username string, userid int64) (string, error) {
  //根据配置文件获取token的过期时间
	TokenExpireDuration := viper.GetInt64("jwt.tokenExpire")
  
	mc := &MyClaim{
		Userid:   userid,
		Username: username,
		StandardClaims: jwt.StandardClaims{
			Issuer:    "web-app",
      //设置过期时间
			ExpiresAt: time.Now().Add(time.Duration(TokenExpireDuration)).Unix(),
		},
	}
  //使用HS256加密算法
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, mc)
	return token.SignedString(CustomSecret)
}

// 解析token
func ParseToken(tokenString string) (*MyClaim, error) {
	mc := new(MyClaim)
	token, err := jwt.ParseWithClaims(tokenString, mc, func(token *jwt.Token) (interface{}, error) {
		return CustomSecret, nil
	})
	if err != nil {
		return nil, err
	}
	if token.Valid {
		return mc, nil
	}
	return nil, InvalidToken
}

完成jwt的生成解析后,就可以写中间件了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package middlewares

import (
	"github.com/gin-gonic/gin"
	"strings"
	"web_app/controller"
	"web_app/dao/redis"
	"web_app/utils/auth"
)


var CtxUserIDKey = "userId"

func JWTAuthMiddleware(c *gin.Context) {
  //从请求头拿出token,token不一定在这里,需要和前端协同
	authHeader := c.Request.Header.Get("Authorization")
	if authHeader == "" {
    //自定义的返回方法
		controller.ResponseError(c, controller.CodeNeedLogin)
		c.Abort()
		return
	}
	part := strings.SplitN(authHeader, " ", 2)
	if len(part) != 2 || part[0] != "Bearer" {
		controller.ResponseError(c, controller.CodeInvalidToken)
		c.Abort()
		return
	}
	mc, err := auth.ParseToken(part[1])
	if err != nil {
		controller.ResponseError(c, controller.CodeInvalidToken)
		c.Abort()
		return
	}
  
	//每次请求都可以解析出user信息,可以辨别是哪个用户
	c.Set(CtxUserIDKey, mc.Userid)
	c.Next()
}

限制一个设备登陆

这里用的方案是使用redis+jwt,每次登陆完,将用户的username和token存到redis中,如果有新的登陆,将会覆盖原来的token。当用户在A端第一次登陆后,生成的token会存到redis中返回给用户,用户拿着这个token来访问特定的资源时,会和redis中的token进行比较,如果一致,才允许放行。而如果在用户在B端又进行了登陆,那么新的token将会覆盖旧的token,当用户在A端拿着旧的token来访问时,与redis中的token明显不一致,此时会拒绝访问。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package redis

import (
	"context"
	"errors"
	"fmt"
	"github.com/go-redis/redis/v8"
	"github.com/spf13/viper"
	"go.uber.org/zap"
	"time"
)

var NotExistToken = errors.New("不存在的key")

//存redis
func StorgeUserIdToken(token, username string) (err error) {
	ctx := context.Background()
  //同样获取token存活时间,将redis的token时间设为一致
	duration := time.Duration(viper.GetInt("jwt.tokenExpire"))
	//存进redis
	if err = rdb.Set(ctx, username, token, duration).Err(); err != nil {
		zap.L().Error("insert username into redis error", zap.Error(err))
		fmt.Printf("insert username into redis error:%v\n", err)
	}
	return
}

//从redis取token
func GetJwtToken(username string) (token string, err error) {
	ctx := context.Background()
	token, err = rdb.Get(ctx, username).Result()
  
	if err == redis.Nil {
		return "", NotExistToken
	}
	if err != nil {
		zap.L().Error("search redis error", zap.Error(err))
		return
	}
	return
}

然后在刚刚的中间件加上以下代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
	//获取token
	token, err := redis.GetJwtToken(mc.Username)
	//如果token不存在,判定需要登陆
	if err == redis.NotExistToken {
		controller.ResponseError(c, controller.CodeNeedLogin)
		c.Abort()
		return
	}
	
	//比较token,不一致则判定已在另一端登陆
	if part[1] != token {
		controller.ResponseError(c, controller.CodelimitLogin)
		c.Abort()
		return
	}

	c.Set(CtxUserIDKey, mc.Userid)
	c.Next()

效果

登陆

image-20230619145709515

登陆成功后,复制刚刚的token去访问

image-20230619145630316

重新发起一个登陆请求

image-20230619145824484

仍是第一次登陆的token去访问

image-20230619145839641