gin和gorm进阶功能(3) | 青训营笔记

306 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 7 天

使用JWT

JWT是项目中比较常用的一种用来解决权限认证和校验的一种方法。

知识点

特点

JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。从 JWT 的全称可以看出,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。 Token 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。 可以看出,JWT 更符合设计 RESTful API 时的「Stateless(无状态)」原则 。 并且, 使用 Token 认证可以有效避免 CSRF 攻击,因为 Token 一般是存在在 localStorage 中,使用 JWT 进行身份验证的过程中是不会涉及到 Cookie 的。

组成

JWT 由哪些部分组成? JWT 本质上就是一组字串,通过(.)切分成三个为 Base64 编码的部分:

  • 头部 Header : 生成签名的算法以及 Token 的类型。

  • 负载 Payload : 用来存放实际需要传递的数据

  • 签名 Signature(签名) : 服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。

JWT 通常是这样的:xxxxx.yyyyy.zzzzz

优缺点

优点 不依赖Cookie,使得其可以防止CSRF攻击 也能在禁用Cookie的浏览器环境中正常运行

(最大优势)服务端不再需要存储Session,使得服务端认证鉴权业务可以方便扩展,避免存储Session所需要引入的Redis等组件,降低了系统架构复杂度

缺点 JWT最大的劣势,由于有效期存储在Token中,JWT Token-旦签发,就会在有效期内-直可用,无法在服务端废止,当用户进行登出操作,只能依赖客户端删除掉本地存储的JWT Token,如果需要禁用用户,单纯使用JWT就无法做到了。

配置

go get github.com/dgrijalva/jwt-go

项目结构

├─.idea
├─config
├─controller
├─dao
├─database
├─global
│  ├─consts
│  ├─my_errors
│  └─variable
├─main
├─model
├─router
├─service
│  └─user
│      ├─curd
│      └─token
├─test
└─utils
    ├─my_jwt
    └─response

Bug:sql: Scan error on column index 5, name "create_time": unsupported Scan, storing driver.Value type []uint8 into type *time.Time

解决方法 使用 gorm 这样的第三方库时,它提供了自动转换时间字段的功能。 首先在初始化数据库连接时设置相应的自动转换选项,如下:

db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")

这将告诉 gorm 使用 MySQL 驱动程序并解析时间字段。 然后在结构体字段上使用 gorm:"column:create_time" 这样的标签来指定数据库字段的名称。

type User struct {
    CreateTime    time.Time     `gorm:"column:create_time" json:"create_time"`
}

这样,gorm 就会自动地将数据库中的时间戳字段转换为 Go 中的 time.Time 类型。

代码

(关键代码) utils/my_jwt/my_jwt.go

package my_jwt  
  
import "github.com/dgrijalva/jwt-go"  
  
// 自定义jwt的声明字段信息+标准字段,参考地址:https://blog.csdn.net/codeSquare/article/details/99288718  
type CustomClaims struct {  
   UserID   uint64 `json:"user_id"`  // user_id  
   Username string `json:"username"` // username  
   jwt.StandardClaims  
}

utils/my_jwt/custom_claims.go

package my_jwt  
  
import (  
   "errors"  
   "github.com/dgrijalva/jwt-go"   "go_douyin/global/my_errors"   "time")  
  
// 使用工厂创建一个 JWT 结构体  
func CreateMyJWT(signKey string) *JwtSign {  
   if len(signKey) <= 0 {  
      signKey = "douyin217@qqTEAM"  
   }  
   return &JwtSign{  
      []byte(signKey),  
   }  
}  
  
// 定义一个 JWT验签 结构体  
type JwtSign struct {  
   SigningKey []byte  
}  
  
// CreateToken 生成一个token  
func (j *JwtSign) CreateToken(claims CustomClaims) (string, error) {  
   // 生成jwt格式的header、claims 部分  
   tokenPartA := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)  
   // 继续添加秘钥值,生成最后一部分  
   return tokenPartA.SignedString(j.SigningKey)  
}  
  
// 解析Token  
func (j *JwtSign) ParseToken(tokenString string) (*CustomClaims, error) {  
   token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {  
      return j.SigningKey, nil  
   })  
   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)  
         } else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {  
            return nil, errors.New(my_errors.ErrorsTokenNotActiveYet)  
         } 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)  
   }  
}  
  
// 更新token  
func (j *JwtSign) RefreshToken(tokenString string, extraAddSeconds int64) (string, error) {  
  
   if CustomClaims, err := j.ParseToken(tokenString); err == nil {  
      CustomClaims.ExpiresAt = time.Now().Unix() + extraAddSeconds  
      return j.CreateToken(*CustomClaims)  
   } else {  
      return "", err  
   }  
}

service/user/token/token.go

package token  
  
import (  
   "errors"  
   "github.com/dgrijalva/jwt-go"   "go_douyin/global/my_errors"   "go_douyin/utils/my_jwt"   "time")  
  
// CreateUserFactory 创建 userToken 工厂  
func CreateUserFactory() *userToken {  
   return &userToken{  
      //userJwt: my_jwt.CreateMyJWT(variable.ConfigYml.GetString("Token.JwtTokenSignKey")),  
      userJwt: my_jwt.CreateMyJWT("12314@"),  
   }  
}  
  
type userToken struct {  
   userJwt *my_jwt.JwtSign  
}  
  
// GenerateToken 生成token  
func (u *userToken) GenerateToken(userid uint64, username string, expireAt int64) (tokens string, err error) {  
  
   // 根据实际业务自定义token需要包含的参数,生成token,注意:用户密码请勿包含在token  
   customClaims := my_jwt.CustomClaims{  
      UserID:   userid,  
      Username: username,  
      // 特别注意,针对前文的匿名结构体,初始化的时候必须指定键名,并且不带 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)  
}  
  
// 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)  
   }  
}