跳到主要内容
版本: websocket_v1.x.x

JWT

Release Discord Test

JWT 返回一个 JSON Web Token (JWT) 认证中间件。对于有效的令牌,它会在 Ctx.Locals 中设置用户并调用下一个处理程序。对于无效的令牌,它返回 "401 - Unauthorized" 错误。对于丢失的令牌,它返回 "400 - Bad Request" 错误。

特别感谢 Echo

注意:需要 Go 1.19 及以上版本

安装

此中间件支持 Fiber v1 和 v2,请相应地安装。

go get -u github.com/gofiber/fiber/v2
go get -u github.com/gofiber/contrib/jwt
go get -u github.com/golang-jwt/jwt/v5

签名

jwtware.New(config ...jwtware.Config) func(*fiber.Ctx) error

配置

属性类型描述默认值
过滤器func(*fiber.Ctx) bool用于跳过特定请求的中间件执行的函数。可选。nil
成功处理程序fiber.Handler令牌验证成功时执行的处理程序。可选。nil
错误处理程序fiber.ErrorHandler令牌验证失败时执行的处理程序。允许自定义 JWT 错误响应。可选。401 无效或过期的 JWT
签名密钥签名密钥用于验证令牌的主密钥。如果 SigningKeys 为空,则用作备用。KeyFunc, JWKSetURLs, SigningKeys 或 SigningKey 至少需要提供一个。nil
签名密钥集map[string]SigningKey用于验证带“kid”字段的令牌的密钥映射。KeyFunc, JWKSetURLs, SigningKeys 或 SigningKey 至少需要提供一个。nil
上下文键string用于在上下文中存储用户信息的主键。可选。"user"
声明jwt.Claims定义令牌声明的结构。可扩展用于自定义声明数据。可选。jwt.MapClaims{}
令牌查找string指定如何从请求中提取令牌。格式:"<来源>:<名称>"(例如,"header:Authorization"、"query:token"、"param:token"、"cookie:token")。可选。"header:Authorization"
令牌处理器函数func(token string) (string, error)处理使用 TokenLookup 提取的令牌。可选。nil
认证方案string在 Authorization 头部中使用的方案。仅用于默认的 TokenLookup。可选。"Bearer"
密钥函数func() jwt.Keyfunc为 JWT 验证提供公钥,处理算法验证和密钥选择。KeyFunc, JWKSetURLs, SigningKeys 或 SigningKey 至少需要提供一个。jwtKeyFunc
JWKSetURL列表[]string包含用于签名验证的 JSON Web Key Sets (JWKS) 的 URL 列表。推荐使用 HTTPS。JWT 头部和 JWKS 中都必须包含 "kid" 字段。默认行为包括每小时刷新、遇到新的 "kid" 时自动刷新、速率限制和超时。nil

HS256 示例

package main

import (
"time"

"github.com/gofiber/fiber/v2"

jwtware "github.com/gofiber/contrib/jwt"
"github.com/golang-jwt/jwt/v5"
)

func main() {
app := fiber.New()

// Login route
app.Post("/login", login)

// Unauthenticated route
app.Get("/", accessible)

// JWT Middleware
app.Use(jwtware.New(jwtware.Config{
SigningKey: jwtware.SigningKey{Key: []byte("secret")},
}))

// Restricted Routes
app.Get("/restricted", restricted)

app.Listen(":3000")
}

func login(c *fiber.Ctx) error {
user := c.FormValue("user")
pass := c.FormValue("pass")

// Throws Unauthorized error
if user != "john" || pass != "doe" {
return c.SendStatus(fiber.StatusUnauthorized)
}

// Create the Claims
claims := jwt.MapClaims{
"name": "John Doe",
"admin": true,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}

// Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}

return c.JSON(fiber.Map{"token": t})
}

func accessible(c *fiber.Ctx) error {
return c.SendString("Accessible")
}

func restricted(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
name := claims["name"].(string)
return c.SendString("Welcome " + name)
}

HS256 测试

使用用户名和密码登录以获取令牌。

curl --data "user=john&pass=doe" http://localhost:3000/login

响应

{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY"
}

在 Authorization 请求头中使用令牌请求受限制的资源。

curl localhost:3000/restricted -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY"

响应

Welcome John Doe

RS256 示例

package main

import (
"crypto/rand"
"crypto/rsa"
"log"
"time"

"github.com/gofiber/fiber/v2"

"github.com/golang-jwt/jwt/v5"

jwtware "github.com/gofiber/contrib/jwt"
)

var (
// Obviously, this is just a test example. Do not do this in production.
// In production, you would have the private key and public key pair generated
// in advance. NEVER add a private key to any GitHub repo.
privateKey *rsa.PrivateKey
)

func main() {
app := fiber.New()

// Just as a demo, generate a new private/public key pair on each run. See note above.
rng := rand.Reader
var err error
privateKey, err = rsa.GenerateKey(rng, 2048)
if err != nil {
log.Fatalf("rsa.GenerateKey: %v", err)
}

// Login route
app.Post("/login", login)

// Unauthenticated route
app.Get("/", accessible)

// JWT Middleware
app.Use(jwtware.New(jwtware.Config{
SigningKey: jwtware.SigningKey{
JWTAlg: jwtware.RS256,
Key: privateKey.Public(),
},
}))

// Restricted Routes
app.Get("/restricted", restricted)

app.Listen(":3000")
}

func login(c *fiber.Ctx) error {
user := c.FormValue("user")
pass := c.FormValue("pass")

// Throws Unauthorized error
if user != "john" || pass != "doe" {
return c.SendStatus(fiber.StatusUnauthorized)
}

// Create the Claims
claims := jwt.MapClaims{
"name": "John Doe",
"admin": true,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}

// Create token
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)

// Generate encoded token and send it as response.
t, err := token.SignedString(privateKey)
if err != nil {
log.Printf("token.SignedString: %v", err)
return c.SendStatus(fiber.StatusInternalServerError)
}

return c.JSON(fiber.Map{"token": t})
}

func accessible(c *fiber.Ctx) error {
return c.SendString("Accessible")
}

func restricted(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
name := claims["name"].(string)
return c.SendString("Welcome " + name)
}

RS256 测试

RS256 测试实际上与上面的 HS256 测试相同。

JWK Set 测试

这些测试与上面的基本 JWT 测试相同,不同之处在于需要提供指向采用 JSON Web Key (JWK) Set 格式的有效公钥集合的 JWKSetURLs。参见 RFC 7517

自定义 KeyFunc 示例

KeyFunc 定义了一个用户自定义函数,用于为令牌验证提供公钥。该函数负责验证签名算法并选择适当的密钥。如果令牌由外部方颁发,用户自定义的 KeyFunc 将非常有用。

当提供用户自定义的 KeyFunc 时,将忽略 SigningKey、SigningKeys 和 SigningMethod。这是提供令牌验证密钥的三种选项之一。优先顺序是用户自定义的 KeyFunc、SigningKeys 和 SigningKey。如果既未提供 SigningKeys 也未提供 SigningKey,则此项是必需的。默认为内部实现,用于验证签名算法并选择适当的密钥。

package main

import (
"fmt"
"github.com/gofiber/fiber/v2"

jwtware "github.com/gofiber/contrib/jwt"
"github.com/golang-jwt/jwt/v5"
)

func main() {
app := fiber.New()

app.Use(jwtware.New(jwtware.Config{
KeyFunc: customKeyFunc(),
}))

app.Get("/ok", func(c *fiber.Ctx) error {
return c.SendString("OK")
})
}

func customKeyFunc() jwt.Keyfunc {
return func(t *jwt.Token) (interface{}, error) {
// Always check the signing method
if t.Method.Alg() != jwtware.HS256 {
return nil, fmt.Errorf("Unexpected jwt signing method=%v", t.Header["alg"])
}

// TODO custom implementation of loading signing key like from a database
signingKey := "secret"

return []byte(signingKey), nil
}
}