简介
jwt,即json-web-token
,是一种轻量级的认证的令牌,通常用于身份认证和授权。由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。
- 头部(Header):JWT 的头部包含了两部分信息:令牌类型(typ)和签名算法(alg),用 Base64 编码后的字符串表示。
- 载荷(Payload):JWT 的载荷包含了一些声明(Claims),用来表示一些实体(主题、发行者、过期时间等)和一些元数据。需要注意的是,载荷中的信息是可以被解码的,因此不要在 JWT 中存储敏感信息。
- 签名(Signature):JWT 的签名用于验证消息的完整性和真实性。签名的过程需要使用头部和载荷中的信息,以及一个秘钥,然后通过指定的算法进行签名。
package jwt
import (
"errors"
"github.com/dgrijalva/jwt-go"
"time"
)
const TokenExpireDuration = time.Hour * 2
var mySecret = []byte("夏天夏天悄悄过去")
type MyClaims struct {
UserID int64 `json:"user_id"`
Username string `json:"username"`
jwt.StandardClaims
}
// GenToken 生成JWT,一般在登录成功时,返回给客户端,客户端拿到token之后保存在请求头中,从route中-》
func GenToken(userID int64, username string) (string, error) {
//创建一个我们自己的声明的数据
c := MyClaims{
UserID: userID,
Username: "username",
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(),
Issuer: "bluebell",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
return token.SignedString(mySecret)
}
//解析token
func ParseToken(tokenString string) (*MyClaims, error) {
mc := new(MyClaims)
token, err := jwt.ParseWithClaims(tokenString, mc, func(token *jwt.Token) (interface{}, error) {
return mySecret, nil
})
if err != nil {
return nil, err
}
if token.Valid {
return mc, nil
}
return nil, errors.New("invalid token")
}
中间件:
package midllewares
import (
"github.com/gin-gonic/gin"
"net/http"
"strings"
"web_app/pkg/jwt"
)
const ContextUserIDKey = "userID"
const ContextName = "Username"
// jwtauthmiddleware 基于jwt认证的中间件
func JWTAuthMiddleware() func(c *gin.Context) {
//客户端携带token有三种方式,1.放在请求头2.放在请求体3.放在URL
//这里假设token放在Header的authorization中,并使用Berer开头
//这里具体实现方式,要依据实际业务情况决定
return func(c *gin.Context) {
authHeader := c.Request.Header.Get("Authorization")
if authHeader == "" {
c.JSON(http.StatusOK, gin.H{
"msg": "No Authorization Header",
})
c.Abort()
return
}
//按空格分割
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
c.JSON(http.StatusOK, gin.H{
"msg": "Invalid Authorization Header",
})
c.Abort()
return
}
mc, err := jwt.ParseToken(parts[1])
if err != nil {
c.JSON(http.StatusOK, gin.H{
"msg": "无效的token",
})
c.Abort()
return
}
c.Set(ContextName, mc.Username)
c.Set(ContextUserIDKey, mc.UserID)
c.Next()
}
}
限制一个设备登陆
这里用的方案是使用redis+jwt,每次登陆完,将用户的username和token存到redis中,如果有新的登陆,将会覆盖原来的token。当用户在A端第一次登陆后,生成的token会存到redis中返回给用户,用户拿着这个token来访问特定的资源时,会和redis中的token进行比较,如果一致,才允许放行。而如果在用户在B端又进行了登陆,那么新的token将会覆盖旧的token,当用户在A端拿着旧的token来访问时,与redis中的token明显不一致,此时会拒绝访问。
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
}
然后在刚刚的中间件加上以下代码
//获取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()