JWT
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
}
}