【阅读 GinSkeleton】中间件

中间件

这里主要介绍鉴权。关于jwt的组成这里就不介绍了。可以看下面阮一峰的教程。

学习博客:在gin框架中使用JWT

阮一峰的JWT入门教程

使用jwt-go验证API

生成jwt(创建工具)

常量

  JwtTokenSignKey:  "goskeleton"   #设置token生成时加密的签名
  JwtTokenOnlineUsers:  10         #一个账号密码允许最大获取几个有效的token,当超过这个值,第一次获取的token的账号、密码就会失效
  JwtTokenCreatedExpireAt: 28800   #创建时token默认有效秒数(token生成时间加上该时间秒数,算做有效期),3600*8=28800 等于8小时
  JwtTokenRefreshAllowSec: 86400   #对于过期的token,允许在多少小时之内刷新,超过此时间则不允许刷新换取新token,86400=3600*24,即token过期24小时之内允许换新token
  JwtTokenRefreshExpireAt: 36000  #对于过期的token,支持从相关接口刷新获取新的token,它有效期为10个小时,3600*10=36000 等于10小时
  BindContextKeyName: "userToken"  #用户在 header 头部提交的token绑定到上下文时的键名,方便直接从上下文(gin.context)直接获取每个用户的id等信息
复制代码

定义JWT验签 结构体

type JwtSign struct {
	SigningKey []byte
}
复制代码

工厂创建对象

//可以自定义或者选择默认,
func CreateMyJWT(signKey string) *JwtSign {
	if len(signKey) <= 0 {
		signKey = "goskeleton"
	}
	return &JwtSign{
		[]byte(signKey),
	}
}
复制代码

生成jwt

//\http\middleware\my_jwt
//jwt的负载
type CustomClaims struct {
	UserId int64  `json:"user_id"`
	Name   string `json:"user_name"`
	Phone  string `json:"phone"`
	jwt.StandardClaims //官方8个字段
}

//http\middleware\my_jwt
func (j *JwtSign) CreateToken(claims CustomClaims) (string, error) {
	// 生成jwt格式的header、claims 部分
    //负载可以自己添加字段
	tokenPartA := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	// 继续添加秘钥值,生成最后一部分
    //使用指定的secret签名并获得完整的编码后的字符串token
	return tokenPartA.SignedString(j.SigningKey)
}
复制代码

Payload(负载)

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

同时你也可以自己添加字段

解析jwt

func (j *JwtSign) ParseToken(tokenString string) (*CustomClaims, error) {
	// 解析token
	// token,输入用户自定义的Claims结构体对象以及自定义函数来解析token字符串为jwt的Token结构体指针
	//	 Keyfunc是匿名函数类型: type Keyfunc func(*Token) (interface{}, error)
	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		return j.SigningKey, nil
	})
	//token无效
	if token == nil {
		return nil, errors.New(my_errors.ErrorsTokenInvalid)
	}
	if err != nil {
		//如果校验失败,分多种情况
		if ve, ok := err.(*jwt.ValidationError); ok {
			//令牌格式错误
			if ve.Errors&jwt.ValidationErrorMalformed != 0 {
				return nil, errors.New(my_errors.ErrorsTokenMalFormed)
				// ValidationErrorNotValidYet表示无效token
			} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
				return nil, errors.New(my_errors.ErrorsTokenNotActiveYet)
				//token过期
			} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
				// 如果 TokenExpired ,只是过期(格式都正确),我们认为他是有效的,接下可以允许刷新操作
				token.Valid = true
				goto labelHere
			} else {
				return nil, errors.New(my_errors.ErrorsTokenInvalid)
			}
		}
	}

labelHere:
	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { // 获取负载
		return claims, nil
	} else {
		return nil, errors.New(my_errors.ErrorsTokenInvalid)
	}
}
复制代码

更新jwt

func (j *JwtSign) RefreshToken(tokenString string, extraAddSeconds int64) (string, error) {
	//检验token
	if CustomClaims, err := j.ParseToken(tokenString); err == nil {
		//设置过期时间
		CustomClaims.ExpiresAt = time.Now().Unix() + extraAddSeconds
		//返回token
		return j.CreateToken(*CustomClaims)
	} else {
		return "", err
	}
}
复制代码

使用jwt(封装工具)

地址:service\users\token

user-token对象

type userToken struct {
	userJwt *my_jwt.JwtSign
}
复制代码

创建工厂生产token

func CreateUserFactory() *userToken {
    //创建jwt对象
	return &userToken{
		userJwt: my_jwt.CreateMyJWT(variable.ConfigYml.GetString("Token.JwtTokenSignKey")),
	}
}
复制代码

生产token

func (u *userToken) GenerateToken(userid int64, username string, phone string, expireAt int64) (tokens string, err error) {

   // 根据实际业务自定义token需要包含的参数,生成token,注意:用户密码请勿包含在token
   customClaims := my_jwt.CustomClaims{
      UserId: userid,
      Name:   username,
      Phone:  phone,
      // 特别注意,针对前文的匿名结构体,初始化的时候必须指定键名,并且不带 jwt. 否则报错:Mixture of field: value and value initializers
      StandardClaims: jwt.StandardClaims{
         NotBefore: time.Now().Unix() - 10,       // 生效开始时间
         ExpiresAt: time.Now().Unix() + expireAt, // 失效截止时间
      },
   }
   return u.userJwt.CreateToken(customClaims)
}
复制代码

解析token

// ParseToken 将 token 解析为绑定时传递的参数
func (u *userToken) ParseToken(tokenStr string) (CustomClaims my_jwt.CustomClaims, err error) {
	if customClaims, err := u.userJwt.ParseToken(tokenStr); err == nil {
		return *customClaims, nil
	} else {
		return my_jwt.CustomClaims{}, errors.New(my_errors.ErrorsParseTokenFail)
	}
}
复制代码

其余功能 (建议自行选择或者自己写)

// RecordLoginToken 用户login成功,记录用户token
func (u *userToken) RecordLoginToken(userToken, clientIp string) bool {
	if customClaims, err := u.userJwt.ParseToken(userToken); err == nil {
		userId := customClaims.UserId
		expiresAt := customClaims.ExpiresAt
		return model.CreateUserFactory("").OauthLoginToken(userId, userToken, expiresAt, clientIp)
	} else {
		return false
	}
}

//TokenIsMeetRefreshCondition 检查token是否满足刷新条件
func (u *userToken) TokenIsMeetRefreshCondition(token string) bool {
	// token基本信息是否有效:1.过期时间在允许的过期范围内;2.基本格式正确
	customClaims, code := u.isNotExpired(token, variable.ConfigYml.GetInt64("Token.JwtTokenRefreshAllowSec"))
	switch code {
	case consts.JwtTokenOK, consts.JwtTokenExpired:
		//在数据库的存储信息是否也符合过期刷新刷新条件
		if model.CreateUserFactory("").OauthRefreshConditionCheck(customClaims.UserId, token) {
			return true
		}
	}
	return false
}

// RefreshToken 刷新token的有效期(默认+3600秒,参见常量配置项)
func (u *userToken) RefreshToken(oldToken, clientIp string) (newToken string, res bool) {
	var err error
	//如果token是有效的、后者在在过期时间内,那么执行更新,换取新token
	if newToken, err = u.userJwt.RefreshToken(oldToken, variable.ConfigYml.GetInt64("Token.JwtTokenRefreshExpireAt")); err == nil {
		if customClaims, err := u.userJwt.ParseToken(newToken); err == nil {
			userId := customClaims.UserId
			expiresAt := customClaims.ExpiresAt
			if model.CreateUserFactory("").OauthRefreshToken(userId, expiresAt, oldToken, newToken, clientIp) {
				return newToken, true
			}
		}
	}

	return "", false
}

// 判断token本身是否未过期
// 参数解释:
// token: 待处理的token值
// expireAtSec: 过期时间延长的秒数,主要用于用户刷新token时,判断是否在延长的时间范围内,非刷新逻辑默认为0
func (u *userToken) isNotExpired(token string, expireAtSec int64) (*my_jwt.CustomClaims, int) {
	if customClaims, err := u.userJwt.ParseToken(token); err == nil {

		if time.Now().Unix()-(customClaims.ExpiresAt+expireAtSec) < 0 {
			// token有效
			return customClaims, consts.JwtTokenOK
		} else {
			// 过期的token
			return customClaims, consts.JwtTokenExpired
		}
	} else {
		// 无效的token
		return nil, consts.JwtTokenInvalid
	}
}

// IsEffective 判断token是否有效(未过期+数据库用户信息正常)
func (u *userToken) IsEffective(token string) bool {
	customClaims, code := u.isNotExpired(token, 0)
	if consts.JwtTokenOK == code {
		//if user_item := Model.CreateUserFactory("").ShowOneItem(customClaims.UserId); user_item != nil {
		if model.CreateUserFactory("").OauthCheckTokenIsOk(customClaims.UserId, token) {
			return true
		}
	}
	return false
}
复制代码

拦截jwt(中间件鉴权)

检查token–中间件

func CheckTokenAuth() gin.HandlerFunc {
	return func(context *gin.Context) {

		headerParams := HeaderParams{}

		//  推荐使用 ShouldBindHeader 方式获取头参数
		if err := context.ShouldBindHeader(&headerParams); err != nil {
			context.Abort()
			response.ErrorParam(context, consts.JwtTokenMustValid+err.Error())
			return
		}
		//按空格分割token
		token := strings.Split(headerParams.Authorization, " ")
		//判断token格式
		if len(token) == 2 && len(token[1]) >= 20 {
			//检验token是否有效
			tokenIsEffective := userstoken.CreateUserFactory().IsEffective(token[1])
			if tokenIsEffective {
				if customeToken, err := userstoken.CreateUserFactory().ParseToken(token[1]); err == nil {
					key := variable.ConfigYml.GetString("Token.BindContextKeyName")
					// token验证通过,同时绑定在请求上下文
					context.Set(key, customeToken)
				}
				context.Next()
			} else {
				response.ErrorTokenAuthFail(context)
			}
		} else {
			response.ErrorTokenBaseInfo(context)
		}
	}
}
复制代码

刷新token–中间件

// RefreshTokenConditionCheck 刷新token条件检查中间件,针对已经过期的token,要求是token格式以及携带的信息满足配置参数即可
func RefreshTokenConditionCheck() gin.HandlerFunc {
	return func(context *gin.Context) {
		//获取头部参数
		headerParams := HeaderParams{}
		if err := context.ShouldBindHeader(&headerParams); err != nil {
			context.Abort()
			response.ErrorParam(context, consts.JwtTokenMustValid+err.Error())
			return
		}
		//按空格分割token
		token := strings.Split(headerParams.Authorization, " ")
		if len(token) == 2 && len(token[1]) >= 20 {
			// 判断token是否满足刷新条件
			if userstoken.CreateUserFactory().TokenIsMeetRefreshCondition(token[1]) {
				context.Next()
			} else {
				response.ErrorTokenRefreshFail(context)
			}
		} else {
			response.ErrorTokenBaseInfo(context)
		}
	}
}
复制代码

我的一点看法

我觉得生成jwt的那部分包可以放到utils,同时servicetoken包放到外面。我也不太清楚token到底需不需要存储?可能看场景吧!我觉得例如删除这样的api是不用传参的直接利用token的负载id去解决。这是我的一点点看法。

鉴权不仅仅是这么一点点东西,后面可以分角色鉴权,对接进去casbin

Guess you like

Origin juejin.im/post/7034096731139047432