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()
|
登陆
登陆成功后,复制刚刚的token去访问
重新发起一个登陆请求
仍是第一次登陆的token去访问