关于golang:JWT身份认证附带源码讲解

47次阅读

共计 11633 个字符,预计需要花费 30 分钟才能阅读完成。

[TOC]

JWT(Json Web Token)验证(附带源码解说)

一天,正是午休时段

兵长路过胖 sir 座位,大吃一惊,明天胖 sir 竟然没有打呼噜,而是在低着头目不转睛盯着一本书

兵长凑近一看,胖 sir 竟然在看史书 …

兵长:(轻声道),你在看 么 ~~

胖 sir:我在想我要是穿梭到清朝,我会是啥身份?

what??~~~,能是啥身份,必定是重量级人物呗

胖 sir:我呸,明天我倒要给你讲讲啥叫身份

讲到身份,不得不说一下 cookie、session、Token 的区别,come on

1 cookie、session、Token 的区别

Cookie

Cookie 总是保留在客户端中,按在客户端中的存储地位,可分为 内存 Cookie硬盘 Cookie

内存 Cookie 由浏览器保护,保留在内存中,浏览器敞开后就隐没了,其存在工夫是短暂的。

硬盘 Cookie 保留在硬盘⾥,有⼀个过期工夫,除⾮⽤户⼿⼯清理或到了过期工夫,硬盘 Cookie 不会被删除,其存在工夫 是⻓期的。

所以,按存在工夫,可分为 ⾮长久 Cookie 长久 Cookie

那么 cookies 到底是什么呢?

cookie 是⼀个⾮常具体的东⻄,指的就是浏览器⾥⾯能永恒存储的⼀种数据,仅仅是浏览器实现的⼀种数

据存储性能。

cookie 由服务器⽣成,发送给浏览器,浏览器把 cookie 以 key-value 模式保留到某个⽬录下的⽂本⽂件

内,下⼀次申请同⼀⽹站时会把该 cookie 发送给服务器。因为 cookie 是存在客户端上的,所以浏览器加⼊

了⼀些限度确保 cookie 不会被歹意使⽤,同时不会占据太多磁盘空间,所以每个域的 cookie 数量是无限的。

Session

Session 字⾯意思是会话,次要⽤来标识⾃⼰的身份。

⽐如在⽆状态的 api 服务在屡次申请数据库时,如何 晓得是同⼀个⽤户,这个就能够通过 session 的机制,服务器要晓得以后发申请给⾃⼰的是谁,为了辨别客户端申请,服务端会给具体的客户端⽣成身份标识 session,而后客户端每次向服务器发申请 的时候,都带上这个“身份标识”,服务器就晓得这个申请来⾃于谁了。

⾄于客户端如何保留该标识,能够有很多⽅式,对于浏览器⽽⾔,⼀般都是使⽤ cookie 的⽅式,服务器使⽤ session 把⽤户信息长期保留了服务器上,⽤户来到⽹站就会销毁,这种凭证存储⽅式绝对于,cookie 来说更加平安。

然而 session 会有⼀个缺点: 如果 web 服务器做了负载平衡,那么下⼀个操作申请到 了另⼀台服务器的时候 session 会失落。

因而,通常企业⾥会使⽤ redis,memcached 缓存中间件来实现 session 的共享,此时 web 服务器就是⼀ 个齐全⽆状态的存在,所有的⽤户凭证能够通过共享 session 的⽅式存取,以后 session 的过期和销毁机制 须要⽤户做管制。

Token

token 的意思是“令牌”,是⽤户身份的验证⽅式,最简略的 token 组成: uid(⽤户唯⼀标识) + time(以后 工夫戳) + sign(签名, 由 token 的前⼏位 + 盐以哈希算法压缩成⼀定⻓度的⼗六进制字符串),同时还可 以将不变的参数也放进 token

这里说的 token 只的是 JWT(Json Web Token)

2 JWT 是个啥?

⼀般⽽⾔,⽤户注册登陆后会⽣成⼀个 jwt token 返回给浏览器,浏览器向服务端申请数据时携带 token,服务器端使⽤ signature 中定义的⽅式进⾏解码,进⽽对 token 进⾏解析和验证。

jwt token 的组成部分

  • header: ⽤来指定使⽤的算法 (HMAC SHA256 RSA) 和 token 类型(如 JWT)

    官网上能够找到各种语言的 jwt 库,例如咱们上面应用这个库进行编码,因为这个库应用的人是最多的,值得信赖

    go get github.com/dgrijalva/jwt-go

  • payload: 蕴含申明(要求),申明通常是⽤户信息或其余数据的申明,⽐如⽤户 id,名称,邮箱等. 申明。可分为三种: registered,public,private
  • signature: ⽤来保障 JWT 的真实性,能够使⽤不同的算法

header

token 的第一局部,如

{
  "alg": "HS256",
  "typ": "JWT"
}

对上⾯的 json 进⾏ base64 编码即可失去 JWT 的第⼀个局部

payload

token 第二局部如

  • registered claims: 预约义的申明,通常会搁置⼀些预约义字段,⽐如过期工夫,主题等(iss:issuer,exp:expiration time,sub:subject,aud:audience)
  • public claims: 能够设置公开定义的字段
  • private claims: ⽤于统⼀使⽤他们的各⽅之间的共享信息

不要在 header 和 payload 中搁置敏感信息,除⾮信息自身曾经做过脱敏解决,因为 payload 局部的具体数据是能够通过 token 来获取到的

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Signature

token 的第三局部

为了失去签名局部,必须有编码过的 header 和 payload,以及⼀个秘钥,签名算法使⽤ header 中指定的那 个,而后对其进⾏签名即可

HMACSHA256(base64UrlEncode(header)+”.”+base64UrlEncode(payload),secret)

签名是 ⽤于验证音讯在传递过程中有没有被更改,并且,对于使⽤私钥签名的 token,它还能够验证 JWT

的发送⽅是否为它所称的发送⽅。

签名的⽬的

最初⼀步签名的过程,实际上是对头部以及载荷内容进⾏签名。

⼀般⽽⾔,加密算法对于不同的输⼊ 产⽣的输入总是不⼀样的。对于两个不同的输⼊,产⽣同样的输入的概率极其地⼩。所以,咱们就把“不⼀样的输⼊产⽣不⼀样的输入”当做必然事件来对待。

所以,如果有⼈对头部以及载荷的内容解码之后进⾏批改,再进⾏编码的话,那么新的头部和载荷的 签名和之前的签名就将是不⼀样的。⽽且,如果不晓得服务器加密的时候⽤的密钥的话,得进去的签名也 ⼀定会是不⼀样的。

服务器应⽤在承受到 JWT 后,会⾸先对头部和载荷的内容⽤同⼀算法再次签名。那么服务器应⽤是怎 么晓得咱们⽤的是哪⼀种算法呢?

在 JWT 的头部中曾经⽤ alg 字段指明了咱们的加密算法了

如果服务器应⽤对头部和载荷再次以同样⽅法签名之后发现,⾃⼰计算出来的签名和承受到的签名不 ⼀样,那么就阐明这个 Token 的内容被别⼈动过的,咱们应该回绝这个 Token,

留神:在 JWT 中,不应该在载荷⾥⾯加⼊任何敏感的数据,⽐如⽤户的明码。具体起因上文曾经给过答案了

jwt.io ⽹站

在 jwt.io(https://jwt.io/#debugger-io)⽹站中,提供了⼀些 JWT token 的编码,验证以及⽣成 jwt 的⼯具。

下图就是⼀个典型的 jwt-token 的组成部分。

啥时候应用 JWT 呢?

咱们要明确的时候,JWT 是用作认证的,而不是用来做受权的。明确他的性能,那么对应 JWT 的利用场景就显而易见了

  • Authorization(受权): 典型场景,⽤户申请的 token 中蕴含了该令牌容许的路由,服务和资源。单点登录其实就是当初⼴泛使⽤ JWT 的⼀个个性
  • Information Exchange(信息替换): 对于平安的在各⽅之间传输信息⽽⾔,JSON Web Tokens ⽆疑 是⼀种很好的⽅式. 因为 JWTs 能够被签名

    例如,⽤公钥 / 私钥对,你能够确定发送⼈就是它们所说的 那个⼈。另外,因为签名是使⽤头和无效负载计算的,您还能够验证内容没有被篡改

JWT 工作形式是怎么的?

JWT 认证过程基本上整个过程分为两个阶段

  • 第⼀个阶段,客户端向服务端获取 token
  • 第⼆阶段,客户端带着该 token 去申请相干的资源

通常⽐较重要的是,服务端如何依据指定的规定进⾏ token 的⽣成。

在认证的时候,当⽤户⽤他们的凭证胜利登录当前,⼀个 JSON Web Token 将会被返回。尔后,token 就是⽤户凭证了,你必须⾮常⼩⼼以防⽌呈现平安问题。⼀般⽽⾔,你保留令牌的工夫不应该超过你所须要它的工夫。

⽆论何时⽤户想要拜访受爱护的路由或者资源的时候,⽤户代理(通常是浏览器)都应该带上 JWT,典型 的,通常放在 Authorization header 中,⽤ Bearer schema: Authorization: Bearer <token> 服务器上的受爱护的路由将会查看 Authorization header 中的 JWT 是否无效,如果无效,则⽤户能够拜访 受爱护的资源。

如果 JWT 蕴含⾜够多的必须的数据,那么就能够缩小对某些操作的数据库查问的须要,只管可能并不总是如此。如果 token 是在受权头(Authorization header)中发送的,那么跨源资源共享 (CORS) 将不会成为问题,因为它不使⽤ cookie。

来感触一张官网的图

获取 JWT 以及拜访 APIs 以及资源

  • 客户端向受权接⼝申请受权
  • 服务端受权后返回⼀个 access token 给客户端
  • 客户端使⽤ access token 拜访受爱护的资源

3 基于 Token 的身份认证和基于服务器的身份认证

1、给予服务器的身份认证,通常是基于服务器上的 session 来做用户认证,应用 session 会有如下几个问题

  • Sessions:认证通过后须要将⽤户的 session 数据保留在内存中,随着认证⽤户的减少,内存开销会⼤
  • 扩展性问题:因为 session 存储在内存中,扩展性会受限,尽管前期能够使⽤ redis,memcached 来缓存数据
  • CORS: 当多个终端拜访同⼀份数据时,可能会遇到禁⽌申请的问题
  • CSRF: ⽤户容易受到 CSRF 攻打(Cross Site Request Forgery, 跨站域申请伪造)

2、基于 Token 的身份认证证是⽆状态的,服务器或者 session 中不会存储任何⽤户信息.(很好的解决了共享 session 的问题)

  • ⽤户携带⽤户名和明码申请获取 token(接⼝数据中可使⽤ appId,appKey,或是本人协商好的某类数据)
  • 服务端校验⽤户凭证,并返回⽤户或客户端⼀个 Token
  • 客户端存储 token, 并在申请头中携带 Token
  • 服务端校验 token 并返回相应数据

须要留神几点:

  • 客户端申请服务器的时候,必须将 token 放到 header 中
  • 客户端申请服务器每一次都须要带上 token
  • 服务器须要设置 为接管所有域的申请: Access-Control-Allow-Origin: *

3、Session 和 JWT Token 的有啥不一样的?

  • 他俩都能够存储用户相干的信息
  • session 存储在服务器,JWT 存储在客户端

4 ⽤ Token 有什么益处呢?

  • 他是 无状态 的 且 可扩展性好
  • 他绝对平安:防⽌ CSRF 攻打,token 过期从新认证

上文有说说,JWT 是用于做身份认证的而不是做受权的,那么在这里列举一下 做认证和做受权别离用在哪里呢?

  • 例如 OAuth2 是⼀种受权框架,是用于受权,次要用在 使⽤第三⽅账号登录的状况 (⽐如使⽤ weibo, qq, github 登录某个 app)
  • JWT 是⼀种认证协定,⽤在 前后端拆散 , 须要简略的对后盾 API 进⾏爱护时使⽤
  • 无论是受权还是认证,都须要记住应用 HTTPS 来爱护数据的安全性

5 理论看看 JWT 如何做身份验证

  • jwt 做身份验证,这里次要讲如何依据 header,payload,signature 生成 token
  • 客户端带着 token 来服务器做申请,如何校验?

上面实例代码,次要做了 2 个接口

用到的技术点:

  • gin

    • 路由分组
    • 中间件的应用
  • gorm

    • 简略操作 mysql 数据库,插入,查问
  • jwt

    • 生成 token
    • 解析 token

登录接口

拜访 url:http://127.0.0.1:9999/v1/login

性能:

  • 用户登录
  • 生成 jwt,并返回给到客户端
  • gorm 对数据库的操作

认证后 Hello 接口

拜访 url:http://127.0.0.1:9999/v1/auth…

性能:

  • 校验 客户端申请服务器携带 token
  • 返回客户端所申请的数据

代码构造如下图

main.go

package main

import (
   "github.com/gin-gonic/gin"
   "my/controller"
   "my/myauth"
)

func main() {
   // 连贯数据库
   conErr := controller.InitMySQLCon()
   if conErr != nil {panic(conErr)
   }

   // 须要应用到 gorm,因而须要先做一个初始化
   controller.InitModel()
   defer controller.DB.Close()


   route := gin.Default()

   // 路由分组
   v1 := route.Group("/v1/")
   {
      // 登录(为了不便,将注册和登录性能写在了一起)v1.POST("/login", controller.Login)
   }


   v2 := route.Group("/v1/auth/")
   // 一个身份验证的中间件
   v2.Use(myauth.JWTAuth())
   {
      // 带着 token 申请服务器
      v2.POST("/hello", controller.Hello)
   }

   // 监听 9999 端口
   route.Run(":9999")

}

controller.go

文件中根本的数据结构定义为:

文件中波及的处理函数:

理论源码:

package controller

import (
   "errors"
   "fmt"
   jwtgo "github.com/dgrijalva/jwt-go"
   "github.com/gin-gonic/gin"
   "github.com/jinzhu/gorm"
   "log"
   "net/http"
   "time"
   _ "github.com/jinzhu/gorm/dialects/mysql"
)

// 登录申请信息
type ReqInfo struct {
   Name   string `json:"name"`
   Passwd string `json:"passwd"`
}

// 结构用户表
type MyInfo struct {
   Id        int32  `gorm:"AUTO_INCREMENT"`
   Name      string `json:"name"`
   Passwd    string `json:"passwd"`
   CreatedAt *time.Time
   UpdateTAt *time.Time
}

//Myclaims
// 定义载荷
type Myclaims struct {
   Name string `json:"userName"`
   // StandardClaims 构造体实现了 Claims 接口 (Valid() 函数)
   jwtgo.StandardClaims
}
// 密钥
type JWT struct {SigningKey []byte
}
//hello 接口
func Hello(c *gin.Context) {claims, _ := c.MustGet("claims").(*Myclaims)
   if claims != nil {
      c.JSON(http.StatusOK, gin.H{
         "status": 0,
         "msg":    "Hello wrold",
         "data":   claims,
      })
   }
}

var (
   DB               *gorm.DB
   secret                 = "iamsecret"
   TokenExpired     error = errors.New("Token is expired")
   TokenNotValidYet error = errors.New("Token not active yet")
   TokenMalformed   error = errors.New("That's not even a token")
   TokenInvalid     error = errors.New("Couldn't handle this token:")
)
// 数据库连贯
func InitMySQLCon() (err error) {
   // 能够在 api 包里设置成 init 函数
   connStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", "root", "123456", "127.0.0.1", 3306, "mygorm")
   fmt.Println(connStr)
   DB, err = gorm.Open("mysql", connStr)

   if err != nil {return err}

   return DB.DB().Ping()
}
// 初始化 gorm 对象映射
func InitModel() {DB.AutoMigrate(&MyInfo{})
}
func NewJWT() *JWT {
   return &JWT{[]byte(secret),
   }
}
// 登陆后果
type LoginResult struct {
   Token string `json:"token"`
   Name string `json:"name"`
}
// 创立 Token(基于用户的根本信息 claims)
// 应用 HS256 算法进行 token 生成
// 应用用户根本信息 claims 以及签名 key(signkey)生成 token
func (j *JWT) CreateToken(claims Myclaims) (string, error) {
   // 返回一个 token 的构造体指针
   token := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, claims)
   return token.SignedString(j.SigningKey)
}
// 生成 token
func generateToken(c *gin.Context, info ReqInfo) {
   // 结构 SignKey: 签名和解签名须要应用一个值
   j := NewJWT()

   // 结构用户 claims 信息(负荷)
   claims := Myclaims{
      info.Name,
      jwtgo.StandardClaims{NotBefore: int64(time.Now().Unix() - 1000), // 签名失效工夫
         ExpiresAt: int64(time.Now().Unix() + 3600), // 签名过期工夫
         Issuer:    "pangsir",                       // 签名颁发者
      },
   }

   // 依据 claims 生成 token 对象
   token, err := j.CreateToken(claims)
   if err != nil {
      c.JSON(http.StatusOK, gin.H{
         "status": -1,
         "msg":    err.Error(),
         "data":   nil,
      })
   }

   log.Println(token)
   // 返回用户相干数据
   data := LoginResult{
      Name:  info.Name,
      Token: token,
   }

   c.JSON(http.StatusOK, gin.H{
      "status": 0,
      "msg":    "登陆胜利",
      "data":   data,
   })

   return
}
// 解析 token
func (j *JWT) ParserToken(tokenstr string) (*Myclaims, error) {
   // 输出 token
   // 输入自定义函数来解析 token 字符串为 jwt 的 Token 构造体指针
   // Keyfunc 是匿名函数类型: type Keyfunc func(*Token) (interface{}, error)
   // func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {}
   token, err := jwtgo.ParseWithClaims(tokenstr, &Myclaims{}, func(token *jwtgo.Token) (interface{}, error) {return j.SigningKey, nil})

   fmt.Println(token, err)
   if err != nil {
      // jwt.ValidationError 是一个有效 token 的谬误构造
      if ve, ok := err.(*jwtgo.ValidationError); ok {
         // ValidationErrorMalformed 是一个 uint 常量,示意 token 不可用
         if ve.Errors&jwtgo.ValidationErrorMalformed != 0 {
            return nil, TokenMalformed
            // ValidationErrorExpired 示意 Token 过期
         } else if ve.Errors&jwtgo.ValidationErrorExpired != 0 {
            return nil, TokenExpired
            // ValidationErrorNotValidYet 示意有效 token
         } else if ve.Errors&jwtgo.ValidationErrorNotValidYet != 0 {return nil, TokenNotValidYet} else {return nil, TokenInvalid}

      }
   }

   // 将 token 中的 claims 信息解析进去和用户原始数据进行校验
   // 做以下类型断言,将 token.Claims 转换成具体用户自定义的 Claims 构造体
   if claims, ok := token.Claims.(*Myclaims); ok && token.Valid {return claims, nil}

   return nil, errors.New("token NotValid")
}
// 登录
func Login(c *gin.Context) {
   var reqinfo ReqInfo
   var userInfo MyInfo

   err := c.BindJSON(&reqinfo)

   if err == nil {fmt.Println(reqinfo)

      if reqinfo.Name == ""|| reqinfo.Passwd ==""{
         c.JSON(http.StatusOK, gin.H{
            "status": -1,
            "msg":    "账号密码不能为空",
            "data":   nil,
         })
         c.Abort()
         return
      }
      // 校验数据库中是否有该用户
      err := DB.Where("name = ?", reqinfo.Name).Find(&userInfo)
      if err != nil {fmt.Println("数据库中没有该用户,能够进行增加用户数据")
         // 增加用户到数据库中
         info := MyInfo{
            Name:   reqinfo.Name,
            Passwd: reqinfo.Passwd,
         }
         dberr := DB.Model(&MyInfo{}).Create(&info).Error
         if dberr != nil {
            c.JSON(http.StatusOK, gin.H{
               "status": -1,
               "msg":    "登录失败,数据库操作谬误",
               "data":   nil,
            })
            c.Abort()
            return
         }
      }else{
         if userInfo.Name != reqinfo.Name || userInfo.Passwd != reqinfo.Passwd{
            c.JSON(http.StatusOK, gin.H{
               "status": -1,
               "msg":    "账号密码谬误",
               "data":   nil,
            })
            c.Abort()
            return
         }
      }
      // 创立 token
      generateToken(c, reqinfo)
   } else {
      c.JSON(http.StatusOK, gin.H{
         "status": -1,
         "msg":    "登录失败,数据申请谬误",
         "data":   nil,
      })
   }
}

myauth.go

package myauth

import (
   "fmt"
   "github.com/gin-gonic/gin"
   "my/controller"
   "net/http"
)

// 身份认证
func JWTAuth() gin.HandlerFunc {return func(c *gin.Context) {
      // 拿到 token
      token := c.Request.Header.Get("token")
      if token == "" {
         c.JSON(http.StatusOK, gin.H{
            "status": -1,
            "msg":    "token 为空,请携带 token",
            "data":   nil,
         })
         c.Abort()
         return
      }

      fmt.Println("token =", token)

      // 解析出理论的载荷
      j := controller.NewJWT()

      claims, err := j.ParserToken(token)
      if err != nil {
         // token 过期
         if err == controller.TokenExpired {
            c.JSON(http.StatusOK, gin.H{
               "status": -1,
               "msg":    "token 受权已过期,请从新申请受权",
               "data":   nil,
            })
            c.Abort()
            return
         }
         // 其余谬误
         c.JSON(http.StatusOK, gin.H{
            "status": -1,
            "msg":    err.Error(),
            "data":   nil,
         })
         c.Abort()
         return
      }

      // 解析到具体的 claims 相干信息
      c.Set("claims", claims)
   }
}

myauth.go

package myauth

import (
   "fmt"
   "github.com/gin-gonic/gin"
   "my/controller"
   "net/http"
)

// 身份认证
func JWTAuth() gin.HandlerFunc {return func(c *gin.Context) {
      // 拿到 token
      token := c.Request.Header.Get("token")
      if token == "" {
         c.JSON(http.StatusOK, gin.H{
            "status": -1,
            "msg":    "token 为空,请携带 token",
            "data":   nil,
         })
         c.Abort()
         return
      }

      fmt.Println("token =", token)

      // 解析出理论的载荷
      j := controller.NewJWT()

      claims, err := j.ParserToken(token)
      if err != nil {
         // token 过期
         if err == controller.TokenExpired {
            c.JSON(http.StatusOK, gin.H{
               "status": -1,
               "msg":    "token 受权已过期,请从新申请受权",
               "data":   nil,
            })
            c.Abort()
            return
         }
         // 其余谬误
         c.JSON(http.StatusOK, gin.H{
            "status": -1,
            "msg":    err.Error(),
            "data":   nil,
         })
         c.Abort()
         return
      }

      // 解析到具体的 claims 相干信息
      c.Set("claims", claims)
   }
}

6 jwt 是如何将 header,paylaod,signature 组装在一起的?

1> 咱们从创立 token 的函数开始看起

CreateToken 用 JWT 对象绑定,对象中蕴含密钥,函数的参数是载荷

2> NewWithClaims 函数参数是加密算法,载荷

NewWithClaims 的具体作用是是初始化一个 Token 对象

3> SignedString 函数,参数为密钥

次要是失去一个残缺的 token

SigningString 将 header 与 载荷 解决后拼接在一起

Sign 将密钥计算一个 hash 值,与 header,载荷拼接在一起,进而制作成 token

此处的 Sign 办法具体是调用哪一个实现,请持续往下看

4> SigningString

将 header 通过 json 序列化之后应用 base64 加密

同样的也将载荷通过 json 序列化之后应用 base64 加密

将这俩加密后的字符串拼接在一起

5> 回到创立 token 函数的地位

func (j *JWT) CreateToken(claims Myclaims) (string, error) {
   // 返回一个 token 的构造体指针
   token := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, claims)
   return token.SignedString(j.SigningKey)
}

SigningMethodHS256 对应这一个构造SigningMethodHMAC,如下

看到这里,便解开了上述第 4 点 Sign 办法具体在哪里实现的问题

7> 成果查看

登录 & 注册接口

数据库展现(若对编码中的 gorm 有疑难,能够看小魔童哪吒的上一期 gorm 的整顿)

Hello 接口

以上为本期全部内容,如有疑难能够在评论区或后盾提出你的疑难,咱们一起交换,一起成长。

好家伙要是文章对你还有点作用的话,请帮忙点个关注,分享到你的朋友圈,分享技术,分享高兴

技术是凋谢的,咱们的心态,更应是凋谢的。拥抱变动,背阴而生,致力向前行。

作者:小魔童哪吒

正文完
 0