使用go实现JWT认证 | 青训营

84 阅读3分钟

1.jwt

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案

jwt官网

1.1 原理

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。这样服务器就不用保存session,实现无状态认证,可扩展性更强

1.2 数据结构

JWT主要分为三个部分

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

image-20230204214410681

可以看出来jwt的组成也十分简单明了

Header

header中alg(algorithm)决定了签名使用的加密算法,默认为HS256,也可以根据需要修改加密算法(官网提供了多种加密算法),typ属性表示这个令牌token的类型(type),JWT 令牌统一写为JWT

Payload

payload就是要传递的数据,也是一个json对象,JWT 规定了7个官方字段,供选用。

 iss (issuer):签发人
 exp (expiration time):过期时间
 sub (subject):主题
 aud (audience):受众
 nbf (Not Before):生效时间
 iat (Issued At):签发时间
 jti (JWT ID):编号

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。

 {
   "sub": "1234567890",
   "name": "John Doe",
   "iat": 1516239022
 }

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。这个 JSON 对象也要使用 Base64URL 算法转成字符串。

Signature

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

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

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

1.3 go实现jwt

 package jwt
 ​
 import (
     "crypto/hmac"
     "crypto/sha256"
     "encoding/base64"
     "encoding/json"
     "errors"
     "fmt"
     "strings"
 )
 ​
 type header struct {
     Alg string `json:"alg"`
     Typ string `json:"typ"`
 }
 ​
 const (
     HS256 = "HS256"
 )
 ​
 var alg = HS256
 var Secret string
 ​
 func hs256(secret, data []byte) (ret string, err error) {
     hasher := hmac.New(sha256.New, secret)
     _, err = hasher.Write(data)
     if err != nil {
         return "", err
     }
     r := hasher.Sum(nil)
     return base64.RawURLEncoding.EncodeToString(r), nil
 }
 ​
 func Sign(payload interface{}) (ret string, err error) {
     h := header{
         Alg: alg,
         Typ: "JWT",
     }
     marshal, err := json.Marshal(h)
     if err != nil {
         return "", err
     }
     bh := base64.RawURLEncoding.EncodeToString(marshal)
 ​
     marshal, err = json.Marshal(payload)
     if err != nil {
         return "", err
     }
 ​
     bp := base64.RawURLEncoding.EncodeToString(marshal)
     // 将header和payload 拼接在一起
     s := fmt.Sprintf("%s.%s", bh, bp)
 ​
     ret, err = hs256([]byte(Secret), []byte(s))
     if err != nil {
         return "", err
     }
 ​
     return fmt.Sprintf("%s.%s.%s", bh, bp, ret), nil
 ​
 }
 ​
 func Verify(token string) (err error) {
     parts := strings.Split(token, ".")
     data := strings.Join(parts[0:2], ".")
     hasher := hmac.New(sha256.New, []byte(Secret))
     _, err = hasher.Write([]byte(data))
     if err != nil {
         return err
     }
     sig, err := base64.RawURLEncoding.DecodeString(parts[2])
     if err != nil {
         return err
     }
     if hmac.Equal(sig, hasher.Sum(nil)) {
         return nil
     }
     return errors.New("verify is invalid")
 }
 ​

2.gin框架实现jwt身份认证

 package main
 ​
 import (
     "fmt"
     "github.com/gin-gonic/gin"
     "gorm.io/gorm"
     "gotoken/db"
     "gotoken/jwt"
     "net/http"
 )
 ​
 type AccountToken struct {
     Name string `json:"name"`
 }
 ​
 type User struct{
     gorm.Model
     Username string `json:"username"`
     Password string `json:"password"`
 }
 ​
 func main() {
 ​
     db := db.GetDatabase()
     user := User{}
 ​
     //fmt.Println(user)
     jwt.Secret = "123456"
 ​
     r := gin.Default()
     // 中间件 相当于拦截器进行鉴权处理
     r.Use(func(c *gin.Context) {
         if c.Request.RequestURI == "/login" {
             // 如果访问的是login则放行,从而获取token
             return
         }
         token, ok := c.Request.Header["Token"]
         fmt.Println(token)
         if ok {
             err := jwt.Verify(token[0])
             if err != nil {
                 c.AbortWithStatusJSON(403, "Forbidden1") //jwt toke验证不通过 不放行
             }
         } else {
             c.AbortWithStatusJSON(403, "Forbidden2")
         }
     })
 ​
     // 第一次访问该路由获取token
     r.POST("/login", func(c *gin.Context) {
         userNew := User{}
         c.ShouldBind(&userNew)
         if userNew.Username == "" || userNew.Password == "" {
             c.JSON(http.StatusForbidden, gin.H{
                 "message": "username or password is empty",
             })
             return
         }
         db.First(&user, "username = ?", userNew.Username)
         if user.Username == userNew.Username && user.Password == userNew.Password {
             sign, _ := jwt.Sign(user)
 ​
             c.JSON(http.StatusOK, gin.H{
                 "message": "login successfully!",
                 "data": sign,
             })
         } else {
             c.JSON(http.StatusForbidden, gin.H{
                 "message": "username or password is invalid",
             })
             return
         }
 ​
     })
     // 访问此路由不需要token
     r.GET("/index", func(c *gin.Context) {
         c.JSON(http.StatusOK, "index")
     })
 ​
     r.Run(":8081")
 }
 ​

数据库初始化文件

 package db
 ​
 import (
     "gorm.io/driver/mysql"
     "gorm.io/gorm"
 )
 ​
 var Db  *gorm.DB
 ​
 func init() {
     // 初始化数据库
     dsn := "root:root@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local"
     db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
 ​
     if err != nil {
         panic(err)
     }
     Db = db
 }
 ​
 func GetDatabase() *gorm.DB{
     return Db
 }
 ​