三段代码搞懂go-jwt做token登录校验的基本使用

由于本来搭建的是基于gin+go-micro+etcd的微服务架构,生成token放在了用户服务,校验则放在了api网关,因此两边代码可能重复。

用户服务端

package handler

import (
    "context"
    "errors"
    "fmt"
    "github.com/dgrijalva/jwt-go"
    "math/rand"
    "micro-file-store/common"
    "micro-file-store/conf"
    "micro-file-store/databases/redisdb"
    "micro-file-store/model/user"
    userProto "micro-file-store/service/account/proto"
    "micro-file-store/util"
    "regexp"
    "time"
)
// 创建token之前需要检验用户有效性,就不放上来了

// 定义claim包含的内容
type jwtClaims struct {
    jwt.StandardClaims
    UserID   uint   `json:"user_id"`
    UserName string `json:"user_name"`
    Password string `json:"Password"`
    RedisKey string `json:"redis_key"`
    Status   uint32 `json:"Status"`
    UserType uint32 `json:"user_type"`
}

const (
    // 盐
    jwtSalt = "瞅你咋地?"
)

/**
 *@Method 获取token
 *@Params user usermodel.UserModel
 *@Return token string, err error
 */
func createToken(user usermodel.UserModel) (token string, err error) {
    var key string
    // 尝试获取上一次的redis key
    lastKey, err := redisdb.GetJWTPool().Get(user.UserName).Result()
    if err != nil {
    // 如果为空,说明未登入过或者已过期删除
        if err == redis.Nil{
            // 如果上次一的token过期了 使用username + 随机五位大写字母,作为redis查询token的key
            r := rand.New(rand.NewSource(time.Now().UnixNano()))
            adStr := make([]byte, 5)
            for i := 0; i < 5; i++ {
                b := r.Intn(26) + 65
                adStr[i] = byte(b)
            }
            key = user.UserName + string(adStr)
        }else{
            return "", err
        }
    }else{
    //如果上次一的token还未过期,继续用上一次的key
        key = lastKey
    }
    // 添加claims信息
    claims := jwtClaims{
        UserID:   user.ID,
        UserName: user.UserName,
        Password: user.Password,
        RedisKey: key,
        Status:   user.Status,
        UserType: user.UserType,
        StandardClaims: jwt.StandardClaims{
            // 签发时间
            IssuedAt: time.Now().Unix(),
            // 不早于。。。生效
            NotBefore: time.Now().Unix() - 1000,
            // 有效时间
            ExpiresAt: time.Now().Unix() + 60*60*24*7,
            Issuer:    "ironHuang",
        },
    }
    jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    // 加盐转换为字符串
    token, err = jwtToken.SignedString([]byte(jwtSalt))
    if err != nil {
        return "", err
    }
    // 将token保存至redis
    err = saveToken(key, token, user.UserName)
    if err != nil {
        return "", err
    }
    return token, nil
}

/**
 *@Method 保存token
 *@Params redisKey, token, userName string
 *@Return error
 */
func saveToken(redisKey, token, userName string) error {
  err = redisdb.GetJWTPool().Set(redisKey, token, 60*60*24*7*1000*1000*1000).Err()
    if err != nil {
        fmt.Println(err.Error())
        return err
    }
  // 关联rediskey和username
    err := redisdb.GetJWTPool().Set(userName, redisKey, 60*60*24*7*1000*1000*1000).Err()
    if err != nil {
        fmt.Println(err.Error())
        return err
    }
    return nil
}

api网关端(关于ParseToken错误处理可以参考最后一段代码理解)

package middleware

import (
    "errors"
    "github.com/dgrijalva/jwt-go"
    "github.com/gin-gonic/gin"
    "micro-file-store/common"
    "micro-file-store/databases/redisdb"
    "net/http"
    "time"
)

const (
    // 盐
    jwtSalt = "瞅你咋地?"
)

// 定义claim
type JwtClaims struct {
    jwt.StandardClaims
    UserID   int64  `json:"user_id"`
    UserName string `json:"user_name"`
    Password string `json:"Password"`
    RedisKey string `json:"redis_key"`
    Status   uint32 `json:"Status"`
    UserType uint32 `json:"user_type"`
}

// 预设错误信息
var (
    TokenExpired     = errors.New("Token is expired")
    TokenNotValidYet = errors.New("Token not active yet")
    TokenMalformed   = errors.New("That's not even a token")
    TokenInvalid     = errors.New("Couldn't handle this token:")
)

/**
 *@Method jwt认证主函数
 *@Params
 *@Return gin.HandlerFunc
 */
func JWTAuth() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        // 获取前端传回的token(传递方式不同,获取的位置也不同,根据实际情况选择)
        authToken := ctx.Request.FormValue("auth_token")
        // 无token直接返回错误
        if authToken == "" {
            ctx.JSON(http.StatusOK, gin.H{
                // 返回代码
                "code": common.StatusTokenInvalid,
                // 返回信息
                "msg": "未登录或非法访问",
            })
            // 校验失败终止后续操作
            ctx.Abort()
            return
        }
        // 解析token
        claims, err := ParseToken(authToken)
        // 错误处理
        if err != nil {
            // token过期
            if err == TokenExpired {
                ctx.JSON(http.StatusOK, gin.H{
                    "code": common.StatusTokenInvalid,
                    "msg":  "授权过期,请重新登录",
                })
                ctx.Abort()
                return
            }
            ctx.JSON(http.StatusOK, gin.H{
                "code": common.StatusTokenInvalid,
                "msg":  err.Error(),
            })
            ctx.Abort()
            return
        }
        if storeToken := redisdb.GetJWTPool().Get(claims.RedisKey).Val(); authToken != storeToken {
            ctx.JSON(http.StatusOK, gin.H{
                "code": common.StatusTokenInvalid,
                "msg":  "授权过期,请重新登录",
            })
            ctx.Abort()
            return
        }
        // 将claim加入上下文,便于后续使用
        ctx.Set("user_claims", claims)

        /*
            claimObj, _ := ctx.Get("user_claims")
            转成JwtClaims
            claimsObj := claimObj.(*JwtClaims)
            userId := claimsObj.UserID
            fmt.Println(userId)
        */

        ctx.Next()
    }
}

/**
 *@Method 解析token
 *@Params token String
 *@Return *JwtClaims, error
 */
func ParseToken(tokenString string) (*JwtClaims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &JwtClaims{}, func(token *jwt.Token) (interface{}, error) {
        return []byte(jwtSalt), nil
    })
    if err != nil {
        // 如果强转*jwt.ValidationError成功,对错误进行判断
        if validationError, ok := err.(*jwt.ValidationError); ok {
            /*
                当validationError中的错误信息由错误的token结构引起时,
                **************************************************
                源码vErr.Errors |= ValidationErrorExpired,
                指将管道符后面的参数值传递给前面参数,返回前面参数的原始值+后面参数值之和
                由于vErr.Errors的初始值为0,所以等价于将ValidationErrorMalformed赋值给validationError的Errors,
                *****************************************************
                如果没有赋值,Errors的初始值为0,那么validationError.Errors&jwt.ValidationErrorMalformed = 0,
                赋值后造成validationError.Errors不为0,那么validationError.Errors&jwt.ValidationErrorMalformed != 0
            */
            if validationError.Errors&jwt.ValidationErrorMalformed != 0 {
                return nil, TokenMalformed
                // 以下与上方原理相同
            } else if validationError.Errors&jwt.ValidationErrorExpired != 0 {
                return nil, TokenExpired
            } else if validationError.Errors&jwt.ValidationErrorNotValidYet != 0 {
                return nil, TokenNotValidYet
            } else {
                return nil, TokenInvalid
            }
        }
    }
    if token != nil {
        // 强转成jwtClaims
        if claims, ok := token.Claims.(*JwtClaims); ok && token.Valid {
            // 如果合法返回claims
            return claims, nil
        }
        return nil, TokenInvalid
    } else {
        return nil, TokenInvalid
    }
}

/**
 *@Method 刷新token
 *@Params token String
 *@Return string,error
 */
func RefreshToken(tokenString string) (string, error) {
    // 重写TimeFunc初始化过期时间
    jwt.TimeFunc = func() time.Time {
        return time.Unix(0, 0)
    }
    // 解析*jwt.Token
    token, err := jwt.ParseWithClaims(tokenString, &JwtClaims{}, func(token *jwt.Token) (interface{}, error) {
        return []byte(jwtSalt), nil
    })
    if err != nil {
        return "", err
    }
    // 强转成定义的jwtClaims,成功继续操作,失败返回错误
    if claims, ok := token.Claims.(JwtClaims); ok && token.Valid {
        // 设置为当前时间过期
        jwt.TimeFunc = time.Now
        // 过期时间加1小时
        claims.StandardClaims.ExpiresAt = time.Now().Local().Add(1 * time.Hour).Unix()
        // 加密
        jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
        // 加盐转字符串
        token, err := jwtToken.SignedString([]byte(jwtSalt))
        // 更新redis,使用ExpireAt(claims.RedisKey, time.Now().Local().Add(1*time.Hour)可能会有误差
        // 可以用第二种更新过期时间
        //redisdb.GetJWTPool().ExpireAt(claims.RedisKey, time.Now().Local().Add(1*time.Hour))
        redisdb.GetJWTPool().Expire(claims.RedisKey, 60*60*1000*1000*1000)
        return token, err
    }
    return "", TokenInvalid
}

关于ParseToken错误处理原理

package main

import (
    "fmt"
    "github.com/pkg/errors"
)

// 定义一个错误码
const myValidErr = 1

// 定义一个结构体
type user struct {
    userName string
}

// 自定义错误
type myError struct {
    // 用于存放外部返回的错误
    Inner error
    // 可以理解为错误代码
    Errors uint32
    // 如果没有另外返回的错误,可以使用text中设置的错误信息
    text string
}

func (u *user) valid() error {
    // 实例化myError
    vErr := new(myError)
    // 假设如果用户名不等于lalala
    if u.userName != "lalala" {
        // 添加错误信息
        vErr.Inner = errors.New("这是一个错误信息")
        // 以下代码等价于vErr.Errors = vErr.Errors + myValidErr,用管道符传递更高效,
        //vErr.Errors初始值为0,这里相当于将myValidErr赋值给vErr.Errors。
        vErr.Errors |= myValidErr
    }
    return vErr
}


/* 源码builtin.go中,error是接口类型意味着可以自定义
type error interface {
    Error() string
}
*/
func (e myError) Error() string {
    if e.Inner != nil {
        return e.Inner.Error()
    } else if e.text != "" {
        return e.text
    } else {
        return "上面都没有,只能靠我拯救世界了"
    }
}

func main() {
    testUser := user{
        userName: "hahaha",
    }
    err := testUser.valid()
    // 强转成myError
    errtest := err.(*myError)

    fmt.Println(errtest.Errors)
    // 如果Inner没有信息将打印text的错误信息,如果text也没用,只能放大招了
    fmt.Println(errtest.Error())
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342