「容器管理系统」开发篇: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 中间件的使用
相关推荐
小_太_阳44 分钟前
Scala_【1】概述
开发语言·后端·scala·intellij-idea
智慧老师1 小时前
Spring基础分析13-Spring Security框架
java·后端·spring
搬码后生仔2 小时前
asp.net core webapi项目中 在生产环境中 进不去swagger
chrome·后端·asp.net
凡人的AI工具箱2 小时前
每天40分玩转Django:Django国际化
数据库·人工智能·后端·python·django·sqlite
桃园码工3 小时前
5-Gin 静态文件服务 --[Gin 框架入门精讲与实战案例]
gin·实战案例·静态文件服务·入门精讲
Lx3523 小时前
Pandas数据重命名:列名与索引为标题
后端·python·pandas
小池先生3 小时前
springboot启动不了 因一个spring-boot-starter-web底下的tomcat-embed-core依赖丢失
java·spring boot·后端
百罹鸟3 小时前
【vue高频面试题—场景篇】:实现一个实时更新的倒计时组件,如何确保倒计时在页面切换时能够正常暂停和恢复?
vue.js·后端·面试
小蜗牛慢慢爬行4 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
wm10435 小时前
java web springboot
java·spring boot·后端