3.5 使用JWT进行身份校验

涉及知识点

    JWT

本文目标

在前面几节中,我们已经基本的完成了API's的编写,但是,还存在一些非常严重的问题,例如,我们现在的API是可以随意调用的,这显然还不安全全,在本文中我们通过 jwt-goGoDoc)的方式来简单解决这个问题。

下载依赖包

首先,我们下载 jwt-go的依赖包,如下:
1
go get -u github.com/dgrijalva/jwt-go
Copied!

编写 jwt 工具包

我们需要编写一个jwt的工具包,我们在pkg下的util目录新建jwt.go,写入文件内容:
1
package util
2
3
import (
4
"time"
5
6
jwt "github.com/dgrijalva/jwt-go"
7
8
"github.com/EDDYCJY/go-gin-example/pkg/setting"
9
)
10
11
var jwtSecret = []byte(setting.JwtSecret)
12
13
type Claims struct {
14
Username string `json:"username"`
15
Password string `json:"password"`
16
jwt.StandardClaims
17
}
18
19
func GenerateToken(username, password string) (string, error) {
20
nowTime := time.Now()
21
expireTime := nowTime.Add(3 * time.Hour)
22
23
claims := Claims{
24
username,
25
password,
26
jwt.StandardClaims {
27
ExpiresAt : expireTime.Unix(),
28
Issuer : "gin-blog",
29
},
30
}
31
32
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
33
token, err := tokenClaims.SignedString(jwtSecret)
34
35
return token, err
36
}
37
38
func ParseToken(token string) (*Claims, error) {
39
tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
40
return jwtSecret, nil
41
})
42
43
if tokenClaims != nil {
44
if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
45
return claims, nil
46
}
47
}
48
49
return nil, err
50
}
Copied!
在这个工具包,我们涉及到
    NewWithClaims(method SigningMethod, claims Claims)method对应着SigningMethodHMAC struct{},其包含SigningMethodHS256SigningMethodHS384SigningMethodHS512三种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
package jwt
2
3
import (
4
"time"
5
"net/http"
6
7
"github.com/gin-gonic/gin"
8
9
"github.com/EDDYCJY/go-gin-example/pkg/util"
10
"github.com/EDDYCJY/go-gin-example/pkg/e"
11
)
12
13
func JWT() gin.HandlerFunc {
14
return func(c *gin.Context) {
15
var code int
16
var data interface{}
17
18
code = e.SUCCESS
19
token := c.Query("token")
20
if token == "" {
21
code = e.INVALID_PARAMS
22
} else {
23
claims, err := util.ParseToken(token)
24
if err != nil {
25
code = e.ERROR_AUTH_CHECK_TOKEN_FAIL
26
} else if time.Now().Unix() > claims.ExpiresAt {
27
code = e.ERROR_AUTH_CHECK_TOKEN_TIMEOUT
28
}
29
}
30
31
if code != e.SUCCESS {
32
c.JSON(http.StatusUnauthorized, gin.H{
33
"code" : code,
34
"msg" : e.GetMsg(code),
35
"data" : data,
36
})
37
38
c.Abort()
39
return
40
}
41
42
c.Next()
43
}
44
}
Copied!

如何获取Token

那么我们如何调用它呢,我们还要获取Token呢?
1、 我们要新增一个获取Token的API
models下新建auth.go文件,写入内容:
1
package models
2
3
type Auth struct {
4
ID int `gorm:"primary_key" json:"id"`
5
Username string `json:"username"`
6
Password string `json:"password"`
7
}
8
9
func CheckAuth(username, password string) bool {
10
var auth Auth
11
db.Select("id").Where(Auth{Username : username, Password : password}).First(&auth)
12
if auth.ID > 0 {
13
return true
14
}
15
16
return false
17
}
Copied!
routers下的api目录新建auth.go文件,写入内容:
1
package api
2
3
import (
4
"log"
5
"net/http"
6
7
"github.com/gin-gonic/gin"
8
"github.com/astaxie/beego/validation"
9
10
"github.com/EDDYCJY/go-gin-example/pkg/e"
11
"github.com/EDDYCJY/go-gin-example/pkg/util"
12
"github.com/EDDYCJY/go-gin-example/models"
13
)
14
15
type auth struct {
16
Username string `valid:"Required; MaxSize(50)"`
17
Password string `valid:"Required; MaxSize(50)"`
18
}
19
20
func GetAuth(c *gin.Context) {
21
username := c.Query("username")
22
password := c.Query("password")
23
24
valid := validation.Validation{}
25
a := auth{Username: username, Password: password}
26
ok, _ := valid.Valid(&a)
27
28
data := make(map[string]interface{})
29
code := e.INVALID_PARAMS
30
if ok {
31
isExist := models.CheckAuth(username, password)
32
if isExist {
33
token, err := util.GenerateToken(username, password)
34
if err != nil {
35
code = e.ERROR_AUTH_TOKEN
36
} else {
37
data["token"] = token
38
39
code = e.SUCCESS
40
}
41
42
} else {
43
code = e.ERROR_AUTH
44
}
45
} else {
46
for _, err := range valid.Errors {
47
log.Println(err.Key, err.Message)
48
}
49
}
50
51
c.JSON(http.StatusOK, gin.H{
52
"code" : code,
53
"msg" : e.GetMsg(code),
54
"data" : data,
55
})
56
}
Copied!
我们打开routers目录下的router.go文件,修改文件内容(新增获取token的方法):
1
package routers
2
3
import (
4
"github.com/gin-gonic/gin"
5
6
"github.com/EDDYCJY/go-gin-example/routers/api"
7
"github.com/EDDYCJY/go-gin-example/routers/api/v1"
8
"github.com/EDDYCJY/go-gin-example/pkg/setting"
9
)
10
11
func InitRouter() *gin.Engine {
12
r := gin.New()
13
14
r.Use(gin.Logger())
15
16
r.Use(gin.Recovery())
17
18
gin.SetMode(setting.RunMode)
19
20
r.GET("/auth", api.GetAuth)
21
22
apiv1 := r.Group("/api/v1")
23
{
24
...
25
}
26
27
return r
28
}
Copied!

验证Token

获取token的API方法就到这里啦,让我们来测试下是否可以正常使用吧!
重启服务后,用GET方式访问http://127.0.0.1:8000/auth?username=test&password=test123456,查看返回值是否正确
1
{
2
"code": 200,
3
"data": {
4
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJwYXNzd29yZCI6InRlc3QxMjM0NTYiLCJleHAiOjE1MTg3MjAwMzcsImlzcyI6Imdpbi1ibG9nIn0.-kK0V9E06qTHOzupQM_gHXAGDB3EJtJS4H5TTCyWwW8"
5
},
6
"msg": "ok"
7
}
Copied!
我们有了token的API,也调用成功了

将中间件接入Gin

2、 接下来我们将中间件接入到Gin的访问流程中
我们打开routers目录下的router.go文件,修改文件内容(新增引用包和中间件引用)
1
package routers
2
3
import (
4
"github.com/gin-gonic/gin"
5
6
"github.com/EDDYCJY/go-gin-example/routers/api"
7
"github.com/EDDYCJY/go-gin-example/routers/api/v1"
8
"github.com/EDDYCJY/go-gin-example/pkg/setting"
9
"github.com/EDDYCJY/go-gin-example/middleware/jwt"
10
)
11
12
func InitRouter() *gin.Engine {
13
r := gin.New()
14
15
r.Use(gin.Logger())
16
17
r.Use(gin.Recovery())
18
19
gin.SetMode(setting.RunMode)
20
21
r.GET("/auth", api.GetAuth)
22
23
apiv1 := r.Group("/api/v1")
24
apiv1.Use(jwt.JWT())
25
{
26
...
27
}
28
29
return r
30
}
Copied!
当前目录结构:
1
go-gin-example/
2
├── conf
3
│ └── app.ini
4
├── main.go
5
├── middleware
6
│ └── jwt
7
│ └── jwt.go
8
├── models
9
│ ├── article.go
10
│ ├── auth.go
11
│ ├── models.go
12
│ └── tag.go
13
├── pkg
14
│ ├── e
15
│ │ ├── code.go
16
│ │ └── msg.go
17
│ ├── setting
18
│ │ └── setting.go
19
│ └── util
20
│ ├── jwt.go
21
│ └── pagination.go
22
├── routers
23
│ ├── api
24
│ │ ├── auth.go
25
│ │ └── v1
26
│ │ ├── article.go
27
│ │ └── tag.go
28
│ └── router.go
29
├── runtime
Copied!
到这里,我们的JWT编写就完成啦!

验证功能

我们来测试一下,再次访问
正确的反馈应该是
1
{
2
"code": 400,
3
"data": null,
4
"msg": "请求参数错误"
5
}
6
7
{
8
"code": 20001,
9
"data": null,
10
"msg": "Token鉴权失败"
11
}
Copied!
我们需要访问http://127.0.0.1:8000/auth?username=test&password=test123456,得到token
1
{
2
"code": 200,
3
"data": {
4
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJwYXNzd29yZCI6InRlc3QxMjM0NTYiLCJleHAiOjE1MTg3MjQ2OTMsImlzcyI6Imdpbi1ibG9nIn0.KSBY6TeavV_30kfmP7HWLRYKP5TPEDgHtABe9HCsic4"
5
},
6
"msg": "ok"
7
}
Copied!
再用包含token的URL参数去访问我们的应用API,
访问http://127.0.0.1:8000/api/v1/articles?token=eyJhbGci...,检查接口返回值
1
{
2
"code": 200,
3
"data": {
4
"lists": [
5
{
6
"id": 2,
7
"created_on": 1518700920,
8
"modified_on": 0,
9
"tag_id": 1,
10
"tag": {
11
"id": 1,
12
"created_on": 1518684200,
13
"modified_on": 0,
14
"name": "tag1",
15
"created_by": "",
16
"modified_by": "",
17
"state": 0
18
},
19
"content": "test-content",
20
"created_by": "test-created",
21
"modified_by": "",
22
"state": 0
23
}
24
],
25
"total": 1
26
},
27
"msg": "ok"
28
}
Copied!
返回正确,至此我们的jwt-goGin中的验证就完成了!

参考

本系列示例代码

关于

修改记录

    第一版:2018年02月16日发布文章
    第二版:2019年10月01日修改文章

如果有任何疑问或错误,欢迎在 issues 进行提问或给予修正意见,如果喜欢或对你有所帮助,欢迎 Star,对作者是一种鼓励和推进。

我的公众号

image
Last modified 2yr ago