使用 JWT 进行身份校验

项目地址:https://github.com/EDDYCJY/go-gin-example

涉及知识点

  • JWT

本文目标

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

下载依赖包

首先,我们下载 jwt-go的依赖包,如下:

  1. go get -u github.com/dgrijalva/jwt-go

编写 jwt 工具包

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

  1. package util
  2. import (
  3. "time"
  4. jwt "github.com/dgrijalva/jwt-go"
  5. "github.com/EDDYCJY/go-gin-example/pkg/setting"
  6. )
  7. var jwtSecret = []byte(setting.JwtSecret)
  8. type Claims struct {
  9. Username string `json:"username"`
  10. Password string `json:"password"`
  11. jwt.StandardClaims
  12. }
  13. func GenerateToken(username, password string) (string, error) {
  14. nowTime := time.Now()
  15. expireTime := nowTime.Add(3 * time.Hour)
  16. claims := Claims{
  17. username,
  18. password,
  19. jwt.StandardClaims {
  20. ExpiresAt : expireTime.Unix(),
  21. Issuer : "gin-blog",
  22. },
  23. }
  24. tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
  25. token, err := tokenClaims.SignedString(jwtSecret)
  26. return token, err
  27. }
  28. func ParseToken(token string) (*Claims, error) {
  29. tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
  30. return jwtSecret, nil
  31. })
  32. if tokenClaims != nil {
  33. if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
  34. return claims, nil
  35. }
  36. }
  37. return nil, err
  38. }

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

  • 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. import (
  3. "time"
  4. "net/http"
  5. "github.com/gin-gonic/gin"
  6. "github.com/EDDYCJY/go-gin-example/pkg/util"
  7. "github.com/EDDYCJY/go-gin-example/pkg/e"
  8. )
  9. func JWT() gin.HandlerFunc {
  10. return func(c *gin.Context) {
  11. var code int
  12. var data interface{}
  13. code = e.SUCCESS
  14. token := c.Query("token")
  15. if token == "" {
  16. code = e.INVALID_PARAMS
  17. } else {
  18. claims, err := util.ParseToken(token)
  19. if err != nil {
  20. code = e.ERROR_AUTH_CHECK_TOKEN_FAIL
  21. } else if time.Now().Unix() > claims.ExpiresAt {
  22. code = e.ERROR_AUTH_CHECK_TOKEN_TIMEOUT
  23. }
  24. }
  25. if code != e.SUCCESS {
  26. c.JSON(http.StatusUnauthorized, gin.H{
  27. "code" : code,
  28. "msg" : e.GetMsg(code),
  29. "data" : data,
  30. })
  31. c.Abort()
  32. return
  33. }
  34. c.Next()
  35. }
  36. }

如何获取Token

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

1、 我们要新增一个获取Token的API

models下新建auth.go文件,写入内容:

  1. package models
  2. type Auth struct {
  3. ID int `gorm:"primary_key" json:"id"`
  4. Username string `json:"username"`
  5. Password string `json:"password"`
  6. }
  7. func CheckAuth(username, password string) bool {
  8. var auth Auth
  9. db.Select("id").Where(Auth{Username : username, Password : password}).First(&auth)
  10. if auth.ID > 0 {
  11. return true
  12. }
  13. return false
  14. }

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

  1. package api
  2. import (
  3. "log"
  4. "net/http"
  5. "github.com/gin-gonic/gin"
  6. "github.com/astaxie/beego/validation"
  7. "github.com/EDDYCJY/go-gin-example/pkg/e"
  8. "github.com/EDDYCJY/go-gin-example/pkg/util"
  9. "github.com/EDDYCJY/go-gin-example/models"
  10. )
  11. type auth struct {
  12. Username string `valid:"Required; MaxSize(50)"`
  13. Password string `valid:"Required; MaxSize(50)"`
  14. }
  15. func GetAuth(c *gin.Context) {
  16. username := c.Query("username")
  17. password := c.Query("password")
  18. valid := validation.Validation{}
  19. a := auth{Username: username, Password: password}
  20. ok, _ := valid.Valid(&a)
  21. data := make(map[string]interface{})
  22. code := e.INVALID_PARAMS
  23. if ok {
  24. isExist := models.CheckAuth(username, password)
  25. if isExist {
  26. token, err := util.GenerateToken(username, password)
  27. if err != nil {
  28. code = e.ERROR_AUTH_TOKEN
  29. } else {
  30. data["token"] = token
  31. code = e.SUCCESS
  32. }
  33. } else {
  34. code = e.ERROR_AUTH
  35. }
  36. } else {
  37. for _, err := range valid.Errors {
  38. log.Println(err.Key, err.Message)
  39. }
  40. }
  41. c.JSON(http.StatusOK, gin.H{
  42. "code" : code,
  43. "msg" : e.GetMsg(code),
  44. "data" : data,
  45. })
  46. }

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

  1. package routers
  2. import (
  3. "github.com/gin-gonic/gin"
  4. "github.com/EDDYCJY/go-gin-example/routers/api"
  5. "github.com/EDDYCJY/go-gin-example/routers/api/v1"
  6. "github.com/EDDYCJY/go-gin-example/pkg/setting"
  7. )
  8. func InitRouter() *gin.Engine {
  9. r := gin.New()
  10. r.Use(gin.Logger())
  11. r.Use(gin.Recovery())
  12. gin.SetMode(setting.RunMode)
  13. r.GET("/auth", api.GetAuth)
  14. apiv1 := r.Group("/api/v1")
  15. {
  16. ...
  17. }
  18. return r
  19. }

验证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. }

我们有了token的API,也调用成功了

将中间件接入Gin

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

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

  1. package routers
  2. import (
  3. "github.com/gin-gonic/gin"
  4. "github.com/EDDYCJY/go-gin-example/routers/api"
  5. "github.com/EDDYCJY/go-gin-example/routers/api/v1"
  6. "github.com/EDDYCJY/go-gin-example/pkg/setting"
  7. "github.com/EDDYCJY/go-gin-example/middleware/jwt"
  8. )
  9. func InitRouter() *gin.Engine {
  10. r := gin.New()
  11. r.Use(gin.Logger())
  12. r.Use(gin.Recovery())
  13. gin.SetMode(setting.RunMode)
  14. r.GET("/auth", api.GetAuth)
  15. apiv1 := r.Group("/api/v1")
  16. apiv1.Use(jwt.JWT())
  17. {
  18. ...
  19. }
  20. return r
  21. }

当前目录结构:

  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

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

验证功能

我们来测试一下,再次访问

正确的反馈应该是

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

我们需要访问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. }

再用包含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. }

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

参考

本系列示例代码

关于

修改记录

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

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

我的公众号

image