「容器管理系统」开发篇:3. JWT(JSON Web Token) 的应用

回顾

项目已开源:基于 Golang 的 容器管理系统

背景

上节已经说到了统一返回的封装, 本节咱们就讲讲 JWT 鉴权,这里有几个问题:

  • JWT 是什么?

JSON Web Token (JWT) 是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。它通常被用于对用户进行身份验证和授权。

  • JWT 的组成

    • Header(头部): 包含了令牌的元数据和加密算法信息。
    • Payload(载荷): 存储了要传输的数据,如用户的身份信息和一些声明。Payload有三种类型:注册的声明(Reserved claims)、公共的声明(Public claims)和私有的声明(Private claims)
    • Signature(签名): 使用头部指定的加密算法对头部和载荷进行签名,以确保令牌在传输过程中没有被篡改。
  • JWT 的使用场景有哪些?

    • Authorization (授权) :这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的 JWT 的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
    • Information Exchange (信息交换) :对于安全的在各方之间传输信息而言,JSON Web Token 无疑是一种很好的方式。因为 JWT 可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。
  • JWT 和 OAuth 的区别

    • OAuth2是一种授权框架 ,JWT是一种认证协议。
    • 无论使用哪种方式切记用HTTPS来保证数据的安全性。
    • OAuth2用在使用第三方账号登录的情况(比如使用weibo,qq,github登录某个app),而JWT是用在前后端分离,,需要简单的对后台API进行保护时使用。

为什么要使用JWT?

  • 跨平台和语言:JWT是基于JSON的标准,因此它在不同的编程语言和平台之间都可以轻松传递和解析。
  • 状态无关性:传统的会话认证在服务端需要保存用户的会话状态,而JWT是无状态的,所有信息都被包含在令牌本身中,这使得服务端不需要保存任何状态信息,从而降低了服务端的负担。
  • 安全性:JWT的签名保证了令牌的完整性和真实性,确保信息不会在传输过程中被篡改或伪造。同时,由于JWT是基于标准的,可以使用加密算法来保护敏感信息,确保令牌只能被可信的接收方解密。
  • 扩展性:由于JWT允许在Payload中添加自定义的声明,因此可以在令牌中携带更多的用户信息和相关权限,满足不同应用的需求。

JWT 库的选择

Go语言中已实现多个可用JWT库,比较常用的有jwt-go、jwt-auth两个:

  • jwt-go :Golang implementation of JSON Web Tokens (JWT)

go get github.com/dgrijalva/jwt-go

  • jwt-auth:JWT middleware for Golang http servers with many configuration options

go get github.com/adam-hanna/jwt-auth

这里咱们使用的是另外一个: golang-jwt

golang-jwt 源自于 jwt-go, 是 jwt-go 的开源升级版本,不过 golang-jwt 这个对 golang 的版本有要求,如果低于 1.18 的版本建议使用 jwt-go

go get github.com/golang-jwt/jwt/v5

封装 JWT 包

工具包名定义为:jwt 总共定义了 2 个文件:

  • auth.go
  • jwt.go

auth.go

定义 jwt Claims, 将 Claims 编码为 Token

go 复制代码
package jwt

import (
    "encoding/json"
    "errors"
    "github.com/golang-jwt/jwt/v5"
    "strconv"
    "time"
)

const (
    TypeJWT = "jwt"
)

type Auth struct {
    Foo  string `json:"foo"`
    UID  int64  `json:"uid"`
    Type string `json:"type"`
    jwt.RegisteredClaims
}

type ValidFunc func(c *Auth) error

var validFuncs = make(map[string]ValidFunc)

// 初始化注册
func init() {
    RegisterValidFunc(TypeJWT, defaultJWTValidFunc)
}

// RegisterValidFunc 注册校验函数
func RegisterValidFunc(authType string, validFunc ValidFunc) {
    validFuncs[authType] = validFunc
}

func defaultJWTValidFunc(a *Auth) error {
    if a.UID == 0 {
       return errors.New("uid is empty")
    }
    return nil
}

// Valid 校验Auth
func (a *Auth) Valid() error {
    if a == nil {
       return errors.New("auth is empty")
    }
    if a.ExpiresAt.Unix() < time.Now().Unix() {
       return errors.New("auth is expired")
    }
    if valid, ok := validFuncs[a.Type]; ok {
       return valid(a)
    }
    return errors.New("unknown auth type")
}

// Encode 将Auth编码成Token
func (a *Auth) Encode(sign string) (Token, error) {
    if a.ExpiresAt == nil || a.ExpiresAt.Unix() <= 0 {
       a.ExpiresAt = jwt.NewNumericDate(time.Unix(time.Now().Unix()+DefaultDuration, 0))
    }
    if a.IssuedAt == nil || a.IssuedAt.Unix() <= 0 {
       a.IssuedAt = jwt.NewNumericDate(time.Unix(time.Now().Unix(), 0))
    }
    if a.NotBefore == nil || a.NotBefore.Unix() <= 0 {
       a.NotBefore = jwt.NewNumericDate(time.Unix(time.Now().Unix(), 0))
    }
    a.ID = strconv.FormatInt(a.UID, 10)
    a.Subject = a.Type
    a.Issuer = a.Type
    a.Audience = []string{sign}
    // 验证 Auth
    if err := a.Valid(); err != nil {
       return "", err
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, a)
    t, err := token.SignedString([]byte(sign))
    return Token(t), err
}

// 将Auth转换为Json
func (a *Auth) String() string {
    data, _ := json.Marshal(a)
    return string(data)
}

jwt.go

解码 Token 并返回 Claims,放入 Header中,并封装获取函数

go 复制代码
package jwt

import (
    "fmt"
    "github.com/CodeLine-95/go-cloud-native/tools/logz"
    "github.com/golang-jwt/jwt/v5"
    "net/http"
)

const (
    DefaultDuration = int64(2 * 3600)
)

type Token string

// GetToken 从请求中获取jwt Token
func GetToken(r *http.Request, cookieName string) Token {
    token := r.Header.Get("X-Auth")
    if token == "" {
       cookie, err := r.Cookie(cookieName)
       if err == nil && cookie.Value != "" {
          token = cookie.Value
       }
    }
    return Token(token)
}

// Decode 将Token解码成Auth结构体, verify为true表示进行,校验失败则返回nil
func (t Token) Decode(sign string, verify bool) *Auth {
    claims := &Auth{}
    parser := &jwt.Parser{}
    if verify {
       parser = jwt.NewParser(jwt.WithoutClaimsValidation())
    }
    token, err := parser.ParseWithClaims(string(t), claims, func(token *jwt.Token) (interface{}, error) {
       if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
          return nil, fmt.Errorf("not authorization")
       }
       return []byte(sign), nil
    })
    if err != nil {
       logz.Error("jwt token decode", logz.F("error", err.Error()))
       return nil
    }
    if token == nil || !token.Valid {
       return nil
    }
    return claims
}

// SetCookie 将jwt Token保存到cookie中
func (t Token) SetCookie(w http.ResponseWriter, cookieName string) {
    w.Header().Set("Set-Cookie", fmt.Sprintf("%s=%s", cookieName, string(t)))
}

// SetHeader 将jwt Token保存到请求返回的X-Auth头部
func (t Token) SetHeader(w http.ResponseWriter) {
    w.Header().Set("X-Auth", string(t))
}

// String jwt Token转成字符串
func (t Token) String() string {
    return string(t)
}

Gin 中间件验证 JWT

通过 Gin 的中间件特性,用来统一验证 Token 实现 JWT 鉴权

定义 JWTLogin.go

go 复制代码
package middleware

import (
    "github.com/CodeLine-95/go-cloud-native/internal/app/constant"
    "github.com/CodeLine-95/go-cloud-native/internal/pkg/base"
    "github.com/CodeLine-95/go-cloud-native/internal/pkg/jwt"
    "github.com/CodeLine-95/go-cloud-native/internal/pkg/response"
    "github.com/CodeLine-95/go-cloud-native/internal/pkg/xlog"
    "github.com/CodeLine-95/go-cloud-native/tools/logz"
    "github.com/CodeLine-95/go-cloud-native/tools/traceId"
    "github.com/gin-gonic/gin"
    "net/http"
    "strings"
)

func JWTLogin() gin.HandlerFunc {
    return func(c *gin.Context) {
       var name string
       nameInfo, err := c.Request.Cookie("userName")
       if err == nil && nameInfo.Value != "" {
          name = nameInfo.Value
       }
       xlog.Info(traceId.GetLogContext(c, "JWTLogin", logz.F("name", name)))

       if userName, ok := c.Get(constant.UserName); ok {
          xlog.Info(traceId.GetLogContext(c, "JWTLogin", logz.F("name", name), logz.F("userName", userName)))
          c.Next()
       } else {
          accept := c.Request.Header.Get("Accept")
          if strings.Index(accept, "html") > -1 {
             c.Abort()
             return
          } else {
             // 获取 token
             token := jwt.GetToken(c.Request, "")
             // 验证token非空
             if token == "" {
                response.Error(c, http.StatusOK, err, constant.ErrorMsg[constant.ErrorNotLogin])
                return
             }
             // token验证是否失效
             auth := token.Decode(base.JwtSignKey, false)
             if auth == nil {
                response.Error(c, http.StatusOK, err, constant.ErrorMsg[constant.ErrorNotLogin])
                return
             }
             // 设置到上下文
             c.Set("auth", auth)
             c.Next()
          }
       }

    }
}

使用 Gin 中间件

go 复制代码
var handlersFuncMap []gin.HandlerFunc

func init() {
    // 注册 JWTLogin 中间件
    handlersFuncMap = append(handlersFuncMap, middleware.JWTLogin())
}

func InitRouter(r *gin.Engine) *gin.Engine {
    ...
    versionRouter := r.Group(fmt.Sprintf("/%s", viper.GetString("app.apiVersion")))
    ...

    // 批量设置中间件:  jwt登录验证
    versionRouter.Use(handlersFuncMap...)
    ...
    return r
}

结束语

本节知识点汇总:

  • JWT 是什么?
  • JWT 的组成
  • JWT 的使用场景
  • JWT 和 OAuth 的区别
  • 为什么要使用JWT?
  • JWT 的应用
  • Gin 中间件的使用
相关推荐
demo007x4 分钟前
如何让 Podman 使用国内镜像源,这是我见过最牛的方法
后端·程序员
疯狂踩坑人11 分钟前
别再说我不懂Node"流"了
后端·node.js
aricvvang12 分钟前
🚀 NestJS 使用 cache-manager-redis-store 缓存无效?真相在这里!
javascript·后端·nestjs
SimonKing12 分钟前
【开发者必备】Spring Boot 2.7.x:WebMvcConfigurer配置手册来了(一)!
java·后端·程序员
oak隔壁找我13 分钟前
Java Collection 包使用指南
java·后端
oak隔壁找我21 分钟前
Spring Boot MongoDB 使用技巧
java·后端
倚栏听风雨28 分钟前
RAG检索增强生成(Retrieval Augmented Generation)
后端
倚栏听风雨40 分钟前
召回率 精准率 F1 概念解释
后端
间彧42 分钟前
消息队列在流量削峰场景下如何设置合理的队列长度和消费速率?
后端