由于本来搭建的是基于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())
}