jwt-go

编写jwt工具

我们需要编写一个jwt的工具,我们在pkg下的util目录新建jwt.go,写入文件内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package util

import (
	"time"

	"github.com/dgrijalva/jwt-go"
)

var jwtSecret = "dong_tech"// 加盐

//自定义payload结构体,不建议直接使用 dgrijalva/jwt-go jwt.StandardClaims结构体.因为他的payload包含的用户信息太少.
type Claims struct { // token里面添加用户信息,验证token后可能会用到用户信息
	jwt.StandardClaims
	UserID      int      `json:"user_id"`
	Password    string   `json:"password"`
	Username    string   `json:"username"`
	FullName    string   `json:"full_name"`
	Permissions []string `json:"permissions"`
}
//实现 `type Claims interface` 的 `Valid() error` 方法,自定义校验内容
func (c Claims) Valid() (err error) {
	if c.VerifyExpiresAt(time.Now().Unix(), true) == false {
		return  errors.New("token is expired")
	}
	if !c.VerifyIssuer(AppIss, true) {
		return  errors.New("token's issuer is wrong")
	}
	if c.User.Id < 1 {
		return errors.New("invalid user in jwt")
	}
	return
}
// GenerateToken generate tokens used for auth
func GenerateToken(username, password string) (string, error) {
	username := c.Param("username")
	password := c.Param("password")
	claims := &Claims{
		UserID:      1,
		Username:    username,
		Password:    password,
		FullName:    username,
		Permissions: []string{},
	}
	claims.IssuedAt = time.Now().Unix()
	claims.ExpiresAt = time.Now().Add(time.Second * time.Duration(ExpireTime)).Unix()
	tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	token, err := tokenClaims.SignedString(jwtSecret)
	return token, err
}

// ParseToken parsing token
func ParseToken(token string) (*Claims, error) {
	tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		return jwtSecret, nil
	})

	if tokenClaims != nil {
		if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
			return claims, nil
		}
	}

	return nil, err
}

在这个工具包,我们涉及到

  • NewWithClaims(method SigningMethod, claims Claims) method对应着SigningMethodHMAC struct{},其包含SigningMethodHS256、SigningMethodHS384、SigningMethodHS512三种crypto.Hash方案
  • func (t *Token) SignedString(key interface{}) 该方法内部生成签名字符串,再用于获取完整、已签名的token
  • func (p *Parser) ParseWithClaims 用于解析鉴权的声明,方法内部主要是具体的解码和校验的过程,最终返回*Token
  • func (m MapClaims) Valid() 验证基于时间的声明exp, iat, nbf,注意如果没有任何声明在令牌中,仍然会被认为是有效的。并且对于时区偏差没有计算方法,此处采用自己实现的方式

有了jwt工具包,接下来我们要编写要用于Gin的中间件,我们在middleware下新建jwt目录,新建jwt.go文件,写入内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package jwt

import (
	"net/http"

	"github.com/dgrijalva/jwt-go"
	"github.com/gin-gonic/gin"

	"github.com/EDDYCJY/go-gin-example/pkg/e"
	"github.com/EDDYCJY/go-gin-example/pkg/util"
)

// JWT is jwt middleware
func JWT() gin.HandlerFunc {
	return func(c *gin.Context) {
		var code int
		var data interface{}

		code = e.SUCCESS
		token := c.Query("token")
		if token == "" {
			code = e.INVALID_PARAMS
		} else {
			_, err := util.ParseToken(token)
			if err != nil {
				switch err.(*jwt.ValidationError).Errors {
				case jwt.ValidationErrorExpired:
					code = e.ERROR_AUTH_CHECK_TOKEN_TIMEOUT
				default:
					code = e.ERROR_AUTH_CHECK_TOKEN_FAIL
				}
			}
		}

		if code != e.SUCCESS {
			c.JSON(http.StatusUnauthorized, gin.H{
				"code": code,
				"msg":  e.GetMsg(code),
				"data": data,
			})

			c.Abort()
			return
		}

		c.Next()
	}
}

如何获取Token

那么我们如何调用它呢,我们还要获取Token呢?

我们要新增一个获取Token的API,在models下新建auth.go文件,写入内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package models

type Auth struct {
    ID int `gorm:"primary_key" json:"id"`
    Username string `json:"username"`
    Password string `json:"password"`
}

func CheckAuth(username, password string) bool {
    var auth Auth
    db.Select("id").Where(Auth{Username : username, Password : password}).First(&auth)
    if auth.ID > 0 {
        return true
    }

    return false
}

在routers下的api目录新建auth.go文件,写入内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package api

import (
    "log"
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/astaxie/beego/validation"

    "gin-blog/pkg/e"
    "gin-blog/pkg/util"
    "gin-blog/models"
)

type auth struct {
    Username string `valid:"Required; MaxSize(50)"`
    Password string `valid:"Required; MaxSize(50)"`
}

func GetAuth(c *gin.Context) {
    username := c.Query("username")
    password := c.Query("password")

    valid := validation.Validation{}
    a := auth{Username: username, Password: password}
    ok, _ := valid.Valid(&a)

    data := make(map[string]interface{})
    code := e.INVALID_PARAMS
    if ok {
        isExist := models.CheckAuth(username, password)
        if isExist {
            token, err := util.GenerateToken(username, password)
            if err != nil {
                code = e.ERROR_AUTH_TOKEN
            } else {
                data["token"] = token

                code = e.SUCCESS
            }

        } else {
            code = e.ERROR_AUTH
        }
    } else {
        for _, err := range valid.Errors {
            log.Println(err.Key, err.Message)
        }
    }

    c.JSON(http.StatusOK, gin.H{
        "code" : code,
        "msg" : e.GetMsg(code),
        "data" : data,
    })
}

我们打开routers目录下的router.go文件,修改文件内容(新增获取token的方法):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package routers

import (
    "github.com/gin-gonic/gin"

    "gin-blog/routers/api"
    "gin-blog/routers/api/v1"
    "gin-blog/pkg/setting"
)

func InitRouter() *gin.Engine {
    r := gin.New()

    r.Use(gin.Logger())

    r.Use(gin.Recovery())

    gin.SetMode(setting.RunMode)

    r.GET("/auth", api.GetAuth)

    apiv1 := r.Group("/api/v1")
    {
        ...
    }

    return r
}

将中间件接入Gin

接下来我们将中间件接入到Gin的访问流程中

我们打开routers目录下的router.go文件,修改文件内容(新增引用包和中间件引用)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package routers

import (
    "github.com/gin-gonic/gin"

    "gin-blog/routers/api"
    "gin-blog/routers/api/v1"
    "gin-blog/pkg/setting"
    "gin-blog/middleware/jwt"
)

func InitRouter() *gin.Engine {
    r := gin.New()

    r.Use(gin.Logger())

    r.Use(gin.Recovery())

    gin.SetMode(setting.RunMode)

    r.GET("/auth", api.GetAuth)

    apiv1 := r.Group("/api/v1")
    apiv1.Use(jwt.JWT())
    {
        ...
    }

    return r
}

当前目录结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
gin-blog/
├── conf
│   └── app.ini
├── main.go
├── middleware
│   └── jwt
│       └── jwt.go
├── models
│   ├── article.go
│   ├── auth.go
│   ├── models.go
│   └── tag.go
├── pkg
│   ├── e
│   │   ├── code.go
│   │   └── msg.go
│   ├── setting
│   │   └── setting.go
│   └── util
│       ├── jwt.go
│       └── pagination.go
├── routers
│   ├── api
│   │   ├── auth.go
│   │   └── v1
│   │       ├── article.go
│   │       └── tag.go
│   └── router.go
├── runtime

到这里,我们的JWT编写就完成啦!

验证功能

我们测试一下,访问

1
2
http://127.0.0.1:8000/api/v1/articles
http://127.0.0.1:8000/api/v1/articles?token=23131

正确的反馈应该是

1
2
3
4
5
{
  "code": 400,
  "data": null,
  "msg": "请求参数错误"
}
1
2
3
4
5
{
  "code": 20001,
  "data": null,
  "msg": "Token鉴权失败"
}

我们需要访问http://127.0.0.1:8000/auth?username=test&password=test123456,得到token

1
2
3
4
5
6
7
{
  "code": 200,
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJwYXNzd29yZCI6InRlc3QxMjM0NTYiLCJleHAiOjE1MTg3MjQ2OTMsImlzcyI6Imdpbi1ibG9nIn0.KSBY6TeavV_30kfmP7HWLRYKP5TPEDgHtABe9HCsic4"
  },
  "msg": "ok"
}

再用包含token的URL参数去访问我们的应用API,

访问http://127.0.0.1:8000/api/v1/articles?token=eyJhbGci...,检查接口返回值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
  "code": 200,
  "data": {
    "lists": [
      {
        "id": 2,
        "created_on": 1518700920,
        "modified_on": 0,
        "tag_id": 1,
        "tag": {
          "id": 1,
          "created_on": 1518684200,
          "modified_on": 0,
          "name": "tag1",
          "created_by": "",
          "modified_by": "",
          "state": 0
        },
        "content": "test-content",
        "created_by": "test-created",
        "modified_by": "",
        "state": 0
      }
    ],
    "total": 1
  },
  "msg": "ok"
}

返回正确,至此我们的jwt-go在Gin中的验证就完成了!

gin-jwt

gin-jwt已经帮我们封装成中间件了,我们只需要设置并实例化它就可以直接用了。

总的就是调用jwt.New函数来实例化一个jwt.GinJWTMiddleware

然后我们看下jwt.GinJWTMiddleware中定义的属性和方法

  • TokenLookup:token检索模式,用于提取token,默认值为header:Authorization。

  • SigningAlgorithm:签名算法,默认值为HS256

  • Timeout:token过期时间,默认值为time.Hour

  • MaxRefresh:token 更新时间

  • TimeFunc:测试或服务器在其他时区可设置该属性,默认值为time.Now

  • TokenHeadName:token在请求头时的名称,默认值为Bearer

  • IdentityKey:身份验证的key值,默认值为identity

  • Realm:可以理解成该中间件的名称,用于展示,默认值为gin jwt

  • CookieName:Cookie名称,默认值为jwt

  • privKey:私钥

  • pubKey:公钥

  • Authenticator函数:在登录接口中使用的验证方法,并返回验证成功后的用户对象。

  • PayloadFunc函数:添加额外业务相关的信息

  • IdentityHandler函数:解析并设置用户身份信息

  • Authorizator函数:接收用户信息并编写授权规则,本项目的API权限控制就是通过该函数编写授权规则的

  • Unauthorized函数:验证失败后设置错误信息

  • LoginResponse函数:完成登录后返回的信息,用户可自定义返回数据,默认返回

    1
    2
    3
    4
    5
    
    {
      "code": http.StatusOK,
      "token": token,
      "expire": expire.Format(time.RFC3339)
    }
    
  • RefreshResponse函数:刷新token后返回的信息,用户可自定义返回数据,默认返回

    1
    2
    3
    4
    5
    
    {
      "code": http.StatusOK,
      "token": token,
      "expire": expire.Format(time.RFC3339)
    }
    

到这里我们应该就知道如何使用这个中间件了。

代码示例:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package main

import (
	"log"
	"net/http"
	"os"
	"time"

	"github.com/appleboy/gin-jwt/v2"
	"github.com/gin-gonic/gin"
)

type login struct {
	Username string `form:"username" json:"username" binding:"required"`
	Password string `form:"password" json:"password" binding:"required"`
}

var identityKey = "id"

//定义一函数用来处理请求
func helloHandler(c *gin.Context) {
    //func ExtractClaims(c *gin.Context) jwt.MapClaims
	//ExtractClaims help to extract the JWT claims
	//用来将 Context 中的数据解析出来赋值给 claims
	//其实是解析出来了 JWT_PAYLOAD 里的内容
	/*
		func ExtractClaims(c *gin.Context) jwt.MapClaims {
		claims, exists := c.Get("JWT_PAYLOAD")
		if !exists {
			return make(jwt.MapClaims)
		}

		return claims.(jwt.MapClaims)
		}
	*/
	claims := jwt.ExtractClaims(c)
    user, _ := c.Get(identityKey)
	c.JSON(200, gin.H{
		"userID":   claims[identityKey],
		"userName": user.(*User).UserName,
		"text":     "Hello World.",
	})
}

// User demo
type User struct {
	UserName  string
	FirstName string
	LastName  string
}

func main() {
	port := os.Getenv("PORT")
	r := gin.New()
	r.Use(gin.Logger())
	r.Use(gin.Recovery())

	if port == "" {
		port = "8000"
	}

	// the jwt middleware
	authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
        //必要项,显示给用户看的域
        Realm:       "test zone",
        //用来进行签名的密钥,就是加盐用的
        Key:         []byte("secret key"),
        //JWT 的有效时间,默认为一小时
        Timeout:     time.Hour,
        //最长的刷新时间,用来给客户端自己刷新 token 用的
        MaxRefresh:  time.Hour,
        //区分身份的key,此key需要每个用户唯一
        IdentityKey: identityKey,
        //额外需要保存到MapClaims中的值,该函数接收Authenticator返回的interface{},返回jwt.MapClaims,该结构体将保存在context中.IdentityKey的值必须保存在其中
		PayloadFunc: func(data interface{}) jwt.MapClaims {
			if v, ok := data.(*User); ok {
				return jwt.MapClaims{
					identityKey: v.UserName,
				}
			}
			return jwt.MapClaims{}
        },
        //返回某个用户的身份验证信息,有默认函数
        // 	if mw.IdentityHandler == nil {
		// mw.IdentityHandler = func(c *gin.Context) interface{} {
		// 	claims := ExtractClaims(c)
		// 	return claims[mw.IdentityKey]
		// }
        // }
		IdentityHandler: func(c *gin.Context) interface{} {
			claims := jwt.ExtractClaims(c)
			return &User{
				UserName: claims[identityKey].(string),
			}
        },
        //必要项, 这个函数用来判断 User 信息是否合法,如果合法就反馈 true,否则就是 false, 认证的逻辑就在这里
		Authenticator: func(c *gin.Context) (interface{}, error) {
			var loginVals login
			if err := c.ShouldBind(&loginVals); err != nil {
				return "", jwt.ErrMissingLoginValues
			}
			userID := loginVals.Username
			password := loginVals.Password

			if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
				return &User{
					UserName:  userID,
					LastName:  "Bo-Yi",
					FirstName: "Wu",
				}, nil
			}

			return nil, jwt.ErrFailedAuthentication
        },
        //可选项,用来在 Authenticator 认证成功的基础上进一步的检验用户是否有权限,默认为 success
		Authorizator: func(data interface{}, c *gin.Context) bool {
			if v, ok := data.(*User); ok && v.UserName == "admin" {
				return true
			}

			return false
        },
        //可以用来定义如果认证不成功的的处理函数,其中s是Authenticator函数中的err的string形式
		Unauthorized: func(c *gin.Context, code int, message string) {
			c.JSON(code, gin.H{
				"code":    code,
				"message": message,
			})
        },
        //可以用来自定义登录成功后的返回
        LoginResponse: func(context *gin.Context, i int, s string, i2 time.Time) {
			c.JSON(code, gin.H{
				"code"   :    i,
                "message" : message,
                "token" : s,
                "expire" :i2.Unix(),
			})
		},
        //这个变量定义了从请求中解析 token 的格式
		// TokenLookup is a string in the form of "<source>:<name>" that is used
		// to extract token from the request.
		// Optional. Default value "header:Authorization".
		// Possible values:
		// - "header:<name>"
		// - "query:<name>"
		// - "cookie:<name>"
		// - "param:<name>"
		TokenLookup: "header: Authorization, query: token, cookie: jwt",
		// TokenLookup: "query:token",
		// TokenLookup: "cookie:token",

        //TokenHeadName 是一个头部信息中的字符串
		// TokenHeadName is a string in the header. Default value is "Bearer"
		TokenHeadName: "Bearer",

        // TimeFunc provides the current time. You can override it to use another time value. This is useful for testing or if your server uses a different time zone than your tokens.
        //这个指定了提供当前时间的函数,也可以自定义
		TimeFunc: time.Now,
	})

	if err != nil {
		log.Fatal("JWT Error:" + err.Error())
	}

	r.POST("/login", authMiddleware.LoginHandler)

	r.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) {
		claims := jwt.ExtractClaims(c)
		log.Printf("NoRoute claims: %#v\n", claims)
		c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
	})

	auth := r.Group("/auth")
	// Refresh time can be longer than token timeout
	auth.GET("/refresh_token", authMiddleware.RefreshHandler)
	auth.Use(authMiddleware.MiddlewareFunc())
	{
		auth.GET("/hello", helloHandler)
	}

	if err := http.ListenAndServe(":"+port, r); err != nil {
		log.Fatal(err)
	}
}

参考: https://segmentfault.com/a/1190000013297828 https://studygolang.com/articles/19838