什么是JWT?

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案,也是目前前后端分离项目中普遍使用的认证技术. 本文介绍如何在Golang Gin Web框架中使用JWT认证中间件以及模拟测试, 以供参考, 关于JWT详细原理可以参考:

  • JWT RFC: https://tools.ietf.org/html/rfc7519
  • JWT IETF: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html
  • JSON Web Token入门教程: http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

主要流程

  • 初始化Gin引擎
  • 定义获取Token的接口, 访问该接口, 内部自动生成JWT令牌, 并返回给前端
  • 定义需要认证的路由接口, 使用JWT中间件进行认证, 中间件由
  • 利用GoConvey(Golang的测试框架,集成go test, 支持终端和浏览器模式), 构造客户端, 填写Token, 模拟前端访问
  • JWT中间件进行认证, 认证通过则返回消息体, 否则直接返回401或其他错误

流程图


该流程图描述了服务端代码中的Token构造, 以及认证流程.

服务端代码

main.go中填充以下代码, 运行go run main.go, 开启Web服务.

  1. package main 
  2.  
  3. import ( 
  4.   jwt_lib "github.com/dgrijalva/jwt-go" 
  5.   "github.com/dgrijalva/jwt-go/request" 
  6.   "github.com/gin-gonic/gin" 
  7.   "log" 
  8.   "time" 
  9.  
  10. var ( 
  11.   mysupersecretpassword = "unicornsAreAwesome" 
  12.  
  13. func Auth(secret string) gin.HandlerFunc { 
  14.   return func(c *gin.Context) { 
  15.     //log.Printf("Request:\n%+v", c.Request) 
  16.     // ParseFromRequest方法提取路径请求中的JWT令牌, 并进行验证 
  17.     token, err := request.ParseFromRequest(c.Request, request.OAuth2Extractor, func(token *jwt_lib.Token) (interface{}, error) { 
  18.       b := ([]byte(secret)) 
  19.       //log.Printf("b:%+v", b) 
  20.       return b, nil 
  21.     }) 
  22.  
  23.     log.Printf("token:%+v", token) 
  24.     if err != nil { 
  25.       c.AbortWithError(401, err) 
  26.     } 
  27.   } 
  28.  
  29. func main() { 
  30.   r := gin.Default() 
  31.  
  32.   public := r.Group("/api") 
  33.  
  34.   // 定义根路由, 访问http://locahost:8080/api/可以获取到token 
  35.   public.GET("/", func(c *gin.Context) { 
  36.     // Create the token New方法接受一个签名方法的接口类型(SigningMethod)参数, 返回一个Token结构指针 
  37.     // GetSigningMethod(签名算法algorithm) 
  38.     token := jwt_lib.New(jwt_lib.GetSigningMethod("HS256")) //默认是签名算法是HMAC SHA256(写成 HS256) 
  39.     log.Printf("token:%+v", token) 
  40.     //2020/12/10 22:32:02 token:&{Raw: Method:0xc00000e2a0 Header:map[alg:HS256 typ:JWT] Claims:map[] Signature: Valid:false} 
  41.  
  42.     // Set some claims 设置Id和过期时间字段, MapClaims实现了Clainms接口 
  43.     token.Claims = jwt_lib.MapClaims{ 
  44.       "Id":  "Christopher", 
  45.       "exp": time.Now().Add(time.Hour * 1).Unix(), 
  46.     } 
  47.     // Sign and get the complete encoded token as a string // 签名并得到完整编码后的Token字符串 
  48.     tokenString, err := token.SignedString([]byte(mysupersecretpassword)) 
  49.     //{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJZCI6IkNocmlzdG9waGVyIiwiZXhwIjoxNjA3NjE0MzIyfQ.eQd7ztDn3706GrpitgnikKgOtzx-RHnq7cr2eqUlsZo"} 
  50.     if err != nil { 
  51.       c.JSON(500, gin.H{"message": "Could not generate token"}) 
  52.     } 
  53.     c.JSON(200, gin.H{"token": tokenString}) 
  54.   }) 
  55.  
  56.   // 定义需要Token验证通过才能访问的私有接口组http://localhost:8080/api/private 
  57.   private := r.Group("/api/private") 
  58.   private.Use(Auth(mysupersecretpassword)) // 使用JWT认证中间件(带参数) 
  59.  
  60.   /* 
  61.     Set this header in your request to get here. 
  62.     Authorization: Bearer `token` 
  63.   */ 
  64.  
  65.   // 定义具体的私有根接口:http://localhost:8080/api/private/ 
  66.   private.GET("/", func(c *gin.Context) { 
  67.     c.JSON(200, gin.H{"message": "Hello from private"}) 
  68.   }) 
  69.  
  70.   r.Run("localhost:8080") 

客户端代码

新建jwt_test.go文件, 填充以下代码, 运行go test执行单元测试.

  1. package test_test 
  2.  
  3. import ( 
  4.   "encoding/json" 
  5.   . "github.com/smartystreets/goconvey/convey" //https://github.com/smartystreets/goconvey GoConvey是Golang的测试框架,集成go test, 支持终端和浏览器模式. 
  6.   "io/ioutil" 
  7.   "log" 
  8.   "net/http" 
  9.   "strings" 
  10.   "testing" 
  11.  
  12. type User struct { 
  13.   Username string `json:"username"` 
  14.   Password string `json:"password"` 
  15.  
  16. type Response struct { 
  17.   Token string `json:"token"` 
  18.  
  19. func createNewsUser(username, password string) *User { 
  20.   return &User{username, password} 
  21.  
  22. func TestLogin(t *testing.T) { 
  23.   Convey("Should be able to login", t, func() { 
  24.     user := createNewsUser("jonas", "1234") 
  25.     jsondata, _ := json.Marshal(user) 
  26.     userData := strings.NewReader(string(jsondata)) 
  27.     log.Printf("userData:%+v", userData) 
  28.     // 这里模拟用户登录, 实际上后台没有使用用户名和密码, 该接口直接返回内部生成的Token 
  29.     req, _ := http.NewRequest("GET", "http://localhost:8080/api/", userData) 
  30.     req.Header.Set("Content-Type", "application/json") 
  31.     client := &http.Client{} 
  32.     res, _ := client.Do(req) 
  33.     //log.Printf("res:%+v", res) 
  34.     So(res.StatusCode, ShouldEqual, 200) //对响应码进行断言, 期望得到状态码为200 
  35.  
  36.     Convey("Should be able to parse body", func() { //解析响应体 
  37.       body, err := ioutil.ReadAll(res.Body) 
  38.       defer res.Body.Close() 
  39.       So(err, ShouldBeNil) 
  40.       Convey("Should be able to get json back", func() { 
  41.         responseData := new(Response) 
  42.         err := json.Unmarshal(body, responseData) 
  43.         So(err, ShouldBeNil) 
  44.         log.Printf("responseData:%s", responseData) 
  45.         Convey("Should be able to be authorized", func() { 
  46.           token := responseData.Token //提取Token 
  47.           log.Printf("token:%s", token) 
  48.           // 构造带Token的请求 
  49.           req, _ := http.NewRequest("GET", "http://localhost:8080/api/private", nil) 
  50.           req.Header.Set("Authorization", "Bearer "+token) //设置认证头 
  51.           client = &http.Client{} 
  52.           res, _ := client.Do(req) 
  53.           body, err := ioutil.ReadAll(res.Body) 
  54.           if err != nil { 
  55.             log.Printf("Read body failed, %s", err.Error()) 
  56.           } 
  57.           log.Printf("Body:%s", string(body)) 
  58.           So(res.StatusCode, ShouldEqual, 200) 
  59.         }) 
  60.       }) 
  61.     }) 
  62.   }) 

参考文档

gin-gonic/contrib/jwt中间件: https://github.com/gin-gonic/contrib/tree/master/jwt