前言

在Golang语言中,jwt-go库提供了一些jwt编码和验证的工具,因此我们很容易使用该库来实现token认证。另外,gin框架中支持用户自定义middleware,我们可以很好的将jwt相关的逻辑封装在middleware中,然后对具体的接口进行认证。

用户登陆 -> token

定义路由

Screen Shot 2021-03-11 at 10.49.18 PM.png

  1. v1 := router.Group("/apis/v1/")
  2. {
  3. v1.POST("/register", controller.RegisterUser) // 完成用户的注册
  4. v1.POST("/login", controller.Login) // 实现用户的登陆
  5. }

请求处理

用户校验

  1. // 登陆逻辑校验
  2. isPass, user, err := model.LoginCheck(loginReq)
  3. // LoginCheck验证
  4. func LoginCheck(login LoginReq) (bool, User, error) {
  5. //...
  6. var user User
  7. dbErr := DB.Where("name = ?", login.Name).Find(&user).Error
  8. //...
  9. if login.Name == user.Name && login.Pwd == user.Pwd {
  10. //...
  11. }
  12. //...
  13. }

token 生成器

  1. // 构造SignKey: 签名和解签名需要使用一个值
  2. j := md.NewJWT()
  3. // 构造用户claims信息(负荷)
  4. claims := middleware.CustomClaims{
  5. user.Name,
  6. user.Email,
  7. jwtgo.StandardClaims{
  8. NotBefore: int64(time.Now().Unix() - 1000), // 签名生效时间
  9. ExpiresAt: int64(time.Now().Unix() + 3600), // 签名过期时间
  10. Issuer: "bgbiao.top", // 签名颁发者
  11. },
  12. }
  13. // 根据claims生成token对象
  14. token, err := j.CreateToken(claims)

返回 token

  1. c.JSON(http.StatusOK, gin.H{
  2. "status": 0,
  3. "msg": "登陆成功",
  4. "data": data, // 存放userId, userName, token...
  5. })

自定义中间件

创建 token

  1. // 创建Token(基于用户的基本信息claims)
  2. // 使用HS256算法进行token生成
  3. // 使用用户基本信息claims以及签名key(signkey)生成token
  4. func (j *JWT) CreateToken(claims CustomClaims) (string, error) {
  5. // https://gowalker.org/github.com/dgrijalva/jwt-go#Token
  6. // 返回一个token的结构体指针
  7. token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
  8. return token.SignedString(j.SigningKey)
  9. }

token -> 用户信息

image.png

定义路由

  1. // secure v1
  2. sv1 := router.Group("/apis/v1/auth/")
  3. sv1.Use(middleware.JWTAuth())
  4. {
  5. sv1.GET("/userInfo", controller.GetUserInfo)
  6. }

请求过滤

JWTAuth 中间件,检查token

获取 token

  1. token := c.Request.Header.Get("Authorization")

解析 token

  1. // token 解析
  2. func (j *JWT) ParserToken(tokenString string) (*CustomClaims, error) {
  3. // https://gowalker.org/github.com/dgrijalva/jwt-go#ParseWithClaims
  4. // 输入用户自定义的Claims结构体对象,token,以及自定义函数来解析token字符串为jwt的Token结构体指针
  5. // Keyfunc是匿名函数类型: type Keyfunc func(*Token) (interface{}, error)
  6. // func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {}
  7. token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
  8. return j.SigningKey, nil
  9. })
  10. if err != nil {
  11. // https://gowalker.org/github.com/dgrijalva/jwt-go#ValidationError
  12. // jwt.ValidationError 是一个无效token的错误结构
  13. if ve, ok := err.(*jwt.ValidationError); ok {
  14. // ValidationErrorMalformed是一个uint常量,表示token不可用
  15. if ve.Errors&jwt.ValidationErrorMalformed != 0 {
  16. return nil, TokenMalformed
  17. // ValidationErrorExpired表示Token过期
  18. } else if ve.Errors&jwt.ValidationErrorExpired != 0 {
  19. return nil, TokenExpired
  20. // ValidationErrorNotValidYet表示无效token
  21. } else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
  22. return nil, TokenNotValidYet
  23. } else {
  24. return nil, TokenInvalid
  25. }
  26. }
  27. }
  28. // 将token中的claims信息解析出来和用户原始数据进行校验
  29. // 做以下类型断言,将token.Claims转换成具体用户自定义的Claims结构体
  30. if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
  31. return claims, nil
  32. }
  33. return nil, TokenInvalid
  34. }
  35. func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {
  36. return new(Parser).ParseWithClaims(tokenString, claims, keyFunc)
  37. }

请求响应

  1. claims := c.MustGet("claims").(*md.CustomClaims)
  2. if claims != nil {
  3. c.JSON(http.StatusOK, gin.H{
  4. "status": 0,
  5. "msg": "token有效",
  6. "data": claims,
  7. })
  8. }

jwt-go 源码分析

github.com/dgrijalva/jwt-go
我在这里通过dubug调试,去了解了jwt-go toekn 的实现过程。挑出一些函数分享一下:

tolen 结构体

  1. // A JWT Token. Different fields will be used depending on whether you're
  2. // creating or parsing/verifying a token.
  3. type Token struct {
  4. Raw string // The raw token. Populated when you Parse a token
  5. Method SigningMethod // The signing method used or to be used
  6. Header map[string]interface{} // The first segment of the token
  7. Claims Claims // The second segment of the token
  8. Signature string // The third segment of the token. Populated when you Parse a token
  9. Valid bool // Is the token valid? Populated when you Parse/Verify a token
  10. }

返回一个token的结构体指针

  1. func NewWithClaims(method SigningMethod, claims Claims) *Token {
  2. return &Token{
  3. Header: map[string]interface{}{
  4. "typ": "JWT",
  5. "alg": method.Alg(),
  6. },
  7. Claims: claims,
  8. Method: method,
  9. }
  10. }

token 签名

SignKey: 签名信息应该设置成动态从库中获取,提高 token 的安全性。

  1. // Get the complete, signed token
  2. func (t *Token) SignedString(key interface{}) (string, error) {
  3. var sig, sstr string
  4. var err error
  5. if sstr, err = t.SigningString(); err != nil {
  6. return "", err
  7. }
  8. if sig, err = t.Method.Sign(sstr, key); err != nil {
  9. return "", err
  10. }
  11. return strings.Join([]string{sstr, sig}, "."), nil
  12. }

token 解析

会对token进行解析,信息存放在claim.

  1. func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {
  2. return new(Parser).ParseWithClaims(tokenString, claims, keyFunc)
  3. }

token.go Structure

image.png

总结

在go中使用jwt,可以引入 github.com/dgrijalva/jwt-go,这个包是官方推荐的也是使用比较多的。在项目中集成 jwt-go 需要注意jwt并不是完全安全的,如果劫持者获取到多个token,有可能通过规则知道你的签名,会解密得到用户信息。因此SignKey 应该设置成动态从库中获取,其次必须设置一个过期时间,在token过期的时候可以自动更新token, 当然用户是不知道的,也就是说不用重复登陆。