中间件
这里主要介绍鉴权。关于jwt的组成这里就不介绍了。可以看下面阮一峰的教程。
学习博客:在gin框架中使用JWT
生成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
,同时service
的token
包放到外面。我也不太清楚token到底需不需要存储?可能看场景吧!我觉得例如删除这样的api
是不用传参的直接利用token
的负载id
去解决。这是我的一点点看法。
鉴权不仅仅是这么一点点东西,后面可以分角色鉴权,对接进去casbin
。