三段代码搞懂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())
}

你可能感兴趣的:(三段代码搞懂go-jwt做token登录校验的基本使用)