Skip to content

介绍

fiber 是受 express 启发的轻量级高性能Go语言开发框架 由go语言编写, 以 fasthttp 为核心开发而来, 至今(2026) 已经有 3个大版本 主要学习记录 v3 版本

快速开始

sh
# 创建项目目录
mkdir fiber-demo && cd fiber-demo

# 初始化模块
go mod init fiber_demo

# 添加依赖
go get github.com/gofiber/fiber/v3

# 创建 main.go 程序入口
touch main.go

# 修改 main.go, 输入后续代码后编译运行
go run main.go
go
package main

import "github.com/gofiber/fiber/v3"

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

  app.Get("/", func(c fiber.Ctx) error {
      return c.SendString("Hello, World!")
  })

  app.Listen(":3000")
}

测试服务状态

打开浏览器访问 http://localhost:8080/ping 如果能够正常看到响应信息说明程序启动成功

自动重启

现在修改代码后, 必须手动停止服务, 然后再次执行 go run main.go 要想改动代码后, 立即自动重启, 就需要借助这两个工具, 达到类似 nodemon 的效果

  • air
  • just 根据配置文件执行命令
  • watchexec 监听文件变化, 自动重新执行某个命令
just
alias d := dev
alias s := start

# just d 或 just dev   会执行这个 dev
# just s 或 just start 会执行 start
# 监听所有文件扩展名为go的文件内容变化后自动重新执行 `go run main.go`
dev:
  watchexec -e go -r "go run main.go"

start:
  go run main.go

自定义配置

go
package main

import (
	"fmt"
	"net"

	"github.com/gofiber/fiber/v3"
)

func main() {
	app := fiber.New(fiber.Config{
		CaseSensitive: true, // 区分大小写
	})

	app.Get("/", func(c fiber.Ctx) error {
		return c.SendString("Hello, World!")
	})

	app.Listen(":3000", fiber.ListenConfig{
		ListenerAddrFunc: func(addr net.Addr) {
			// 监听服务启动时: 输出一个日志
			fmt.Println("[App]Server start on", addr)
		},
	})
}

路由

HTTP方法

go
package main

import (
	"github.com/gofiber/fiber/v3"
)

func main() {
	app := fiber.New()
	// 支持如下方法:
  // Get(...) Router
  // Head(...) Router
  // Post(...) Router
  // Put(...) Router
  // Delete(...) Router
  // Connect(...) Router
  // Options(...) Router
  // Trace(...) Router
  // Patch(...) Router
  // All(...) Router 这个比较特殊: 允许所有类型的请求方法

	// 仅允许 GET 方式访问
	app.Get("/get", func(c fiber.Ctx) error {
		return c.SendString("from get method")
	})

	// 注: 与 gin 不同的是, 方法名不是全大写, 而是大驼峰式写法
	app.Post("/post", func(c fiber.Ctx) error {
		return c.SendString("from post method")
	})

	app.Listen(":3000")
}

路由分组

fiber 的路由分组与 express 的路由分组类似

go
package main

import "github.com/gofiber/fiber/v3"

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

	api := app.Group("/api")
	v1 := api.Group("/v1", func(c fiber.Ctx) error {
		// /api/v1 应用分组中间件设置响应头
		c.Set("X-API-Version", "v1")
		return c.Next()
	})
	// POST /api/v1/login
	v1.Post("/login", func(c fiber.Ctx) error {
		return c.JSON(fiber.Map{
			"message": "from /api/v1/login",
		})
	})

	v2 := api.Group("/v2") // /api/v2
	// POST /api/v2/login
	v2.Post("/login", func(c fiber.Ctx) error {
		return c.JSON(fiber.Map{
			"message": "from /api/v2/login",
		})
	})

	app.Listen(":3000")
}
go
package main

import "github.com/gofiber/fiber/v3"

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

	app.Route("/api", func(apiRouter fiber.Router) {
		apiRouter.Route("/v1", func(v1Router fiber.Router) {
			v1Router.Post("/login", func(c fiber.Ctx) error {
				return c.JSON(fiber.Map{
					"message": "from /api/v1/login",
				})
			})
		})

		apiRouter.Route("/v2", func(v2Router fiber.Router) {
			v2Router.Post("/login", func(c fiber.Ctx) error {
				return c.JSON(fiber.Map{
					"message": "from /api/v2/login",
				})
			})
		})
	})

	app.Listen(":3000")
}

获取请求信息 Request

go
package main

import (
	"fmt"

	"github.com/gofiber/fiber/v3"
)

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

	////// 1.获取路径参数
	app.Get("/params/:id/:email?", func(c fiber.Ctx) error {
		id := c.Params("id")                              // 获取路径中的 :id 参数
		email := c.Params("email", "default@example.com") // 获取路径中的 :email? 可选参数, 并设置默认值
		return c.JSON(fiber.Map{
			"id":    id,
			"email": email,
		})

		// 测试:
		// curl -i http://127.0.0.1:3000/params/1001
		// curl -i http://127.0.0.1:3000/params/1002/test@example.com
	})

	////// 2.获取查询参数
	app.Get("/search", func(c fiber.Ctx) error {
		keyword := c.Query("keyword")   // 获取查询关键字
		page := c.Query("page", "1")    // 获取查询页码
		limit := c.Query("limit", "10") // 获取每页查询个数限制

		return c.JSON(fiber.Map{
			"keyword": keyword,
			"page":    page,
			"limit":   limit,
		})

		// 测试:
		// curl -i http://127.0.0.1:3000/search?keyword=golang
		// curl -i http://127.0.0.1:3000/search?keyword=golang&page=2
		// curl -i http://127.0.0.1:3000/search?keyword=golang&page=2&limit=20
	})

	////// 3.获取 form 数据
	app.Post("/form", func(c fiber.Ctx) error {
		email := c.FormValue("email")                           // 获取 form 中的 name 参数
		nickname := c.FormValue("nickname", "default-nickname") // 默认值

		return c.JSON(fiber.Map{
			"email":    email,
			"nickname": nickname,
		})
	})

	////// 4.获取 json 数据
	app.Post("/json", func(c fiber.Ctx) error {
		// 绑定时验证数据
		type LoginFormDto struct {
			Email    string `json:"email" validate:"required,email"`
			Password string `json:"password" validate:"required,min=6"`
		}

		// 绑定数据
		loginForm := new(LoginFormDto)
		if err := c.Bind().JSON(loginForm); err != nil {
			return err
		}

		return c.JSON(fiber.Map{
			"message":  "login success",
			"email":    loginForm.Email,
			"password": loginForm.Password,
		})

		// 测试:
		// curl --request POST \
		//   --url http://127.0.0.1:3000/json \
		//   --header 'Content-Type: application/json' \
		//   --data '{
		//   "email": "test@example.com",
		//   "password": "123456"
		// }'
	})

	////// 5.获取 文件上传数据
	app.Post("/upload", func(c fiber.Ctx) error {
		file, err := c.FormFile("file")
		if err != nil {
			return err
		}

		// 文件信息
		fmt.Println("文件名:", file.Filename)
		fmt.Println("文件大小:", file.Size)
		fmt.Println("文件类型:", file.Header.Get("Content-Type"))

		// 保存到本地(注:uploads目录必须存在, 否则报错)
		err = c.SaveFile(file, fmt.Sprintf("./uploads/%s", file.Filename))
		if err != nil {
			return err
		}

		return c.JSON(fiber.Map{
			"message":  "上传成功",
			"filepath": "./uploads/" + file.Filename,
			"filename": file.Filename,
			"size":     file.Size,
		})
	})

	////// 6.获取请求头
	app.Get("/headers", func(c fiber.Ctx) error {
		// 获取所有请求头字段
		headers := c.GetReqHeaders()

		// 获取单个字段
		accessToken := c.Get("Authorization")
		return c.JSON(fiber.Map{
			"headers": headers,
			"accessToken": accessToken,
		})
	})

	////// 7.获取 Request 实例
	app.Get("/request", func(c fiber.Ctx) error {
		fmt.Println("[Request]", c.Request())
		// [Request] GET /request HTTP/1.1
		// User-Agent: HTTPie
		// Host: 127.0.0.1:3000
		// Connection: close

		fmt.Println("[Request]", c.Request().URI())
		// [Request] http://127.0.0.1:3000/request

		return c.JSON(fiber.Map{
			"message": "ok",
		})
	})

	app.Listen(":3000")
}

设置响应信息 Response

go
package main

import (
	"net/http"

	"github.com/gofiber/fiber/v3"
)

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

	////// 1.响应字符串纯文本
	app.Get("/str", func(c fiber.Ctx) error {
		return c.SendString("hello world")
	})

	////// 2.响应JSON: 自动设置 Content-Type: application/json
	app.Get("/json", func(c fiber.Ctx) error {
		return c.JSON(fiber.Map{
			"message": "hello world",
		})
	})

	////// 3.响应HTML: 手动设置 Content-Type: text/html
	app.Get("/html", func(c fiber.Ctx) error {
		c.Set("Content-Type", "text/html")
		return c.SendString("<h1>hello world</h1>")
	})

	////// 4.响应文件下载
	app.Get("/download", func(c fiber.Ctx) error {
		// c.Attachment("./uploads/avatar.png")
		// return nil

		// 或者直接试用 Download 方法
		// 会自动设置响应头, 唤起浏览器下载
		// Content-Disposition: attachment; filename="文件.txt"; filename*=UTF-8''%E6%96%87%E4%BB%B6.txt
		return c.Download("./uploads/avatar.png")
	})

	////// 5.响应一个文件内容(非下载,而是在浏览器中打开)
	app.Get("/file", func(c fiber.Ctx) error {
		return c.SendFile("./uploads/avatar.png")
	})

	////// 6.响应重定向(临时/永久)
	app.Get("/redirect", func(c fiber.Ctx) error {
		// return c.Redirect().Status(http.StatusTemporaryRedirect).To("https://qq.com")
		return c.Redirect().Status(http.StatusPermanentRedirect).To("https://qq.com")
	})

	app.Listen(":3000")
}

数据绑定与验证

与 gin 非常类似, 可以直接将 HTTP 请求的内容解析绑定为 go 语言的 struct

而数据验证方面, 二者都是用的 validator 这个库, 所以验证的用法几乎一模一样

go
package main

import (
	"github.com/gofiber/fiber/v3"
)

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

	////// 0.绑定 params 为 golang 的 struct
	app.Get("/user/:id/:name?", func(c fiber.Ctx) error {
		type User struct {
			ID   int    `uri:"id" validate:"required"`
			Name string `uri:"name,default=test" validate:"required"`
		}

		user := new(User)
		if err := c.Bind().URI(user); err != nil {
			return err
		}

		return c.JSON(user)
	})

	////// 1.绑定 query 为 golang 的 struct
	app.Get("/search", func(c fiber.Ctx) error {
		type SearchQuery struct {
			Keyword string `query:"keyword" validate:"required"`
			Page    int    `query:"page,default=1"`
			Limit   int    `query:"page,default=10"`
		}

		query := new(SearchQuery)
		if err := c.Bind().Query(query); err != nil {
			return err
		}

		return c.JSON(query)
	})

	// 绑定json/绑定form/绑定request-header 等例子请直接查看文档
	app.Listen(":3000")
}

中间件

中间件分类

  • 局部中间件: 作用于单个路由
  • 分组中间件: 作用于某个路由分组
  • 全局中间件: 作用于整个服务的所有路由
go
package main

import (
	"fmt"

	"github.com/gofiber/fiber/v3"
)

func SetApiVersionHeaser(c fiber.Ctx) error {
	// 局部中间件
	fmt.Println("[SetApiVersionHeaser] Executing")
	c.Set("X-API-Version", "v1")
	return c.Next()
}

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

	app.Use(func(c fiber.Ctx) error {
		// 全局中间件
		fmt.Println("[GlobalMiddleware] Executing")
		return c.Next()
	})

	apiRouter := app.Group("/api", func(c fiber.Ctx) error {
		// 路由分组中间件
		fmt.Println("[APIRouter] Executing")
		return c.Next()
	})

	apiRouter.Get("/v1/hello", SetApiVersionHeaser, func(c fiber.Ctx) error {
		return c.JSON(fiber.Map{
			"message": "Hello World",
		})
	})

	// 控制台查看输出的调试信息
	// 测试: curl -i http://127.0.0.1:3000/api/v1/hello
	app.Listen(":3000")
}

CORS 跨域中间件

允许跨域中间件

go
package main

import (
	"github.com/gofiber/fiber/v3"
	"github.com/gofiber/fiber/v3/middleware/cors"
)

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

	app.Use(cors.New(cors.Config{
		AllowOrigins: []string{"*"},
		AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-API-VERSION"},
		AllowMethods: []string{
			fiber.MethodGet,
			fiber.MethodPost,
			fiber.MethodHead,
			fiber.MethodPut,
			fiber.MethodDelete,
			fiber.MethodPatch,
		},
	}))

	app.Get("/hello", func(c fiber.Ctx) error {
		return c.JSON(fiber.Map{
			"message": "Hello World",
		})
	})

	// 控制台查看输出的调试信息
	// 测试(注意,需要设置一些参数,否则无法触发跨域安全规则):
	// curl -i -X OPTIONS \
	//   -H "Origin: http://example.com" \
	//   -H "Access-Control-Request-Method: POST" \
	//   http://127.0.0.1:3000/hello
	app.Listen(":3000")
}

静态文件服务中间件

go
package main

import (
	"github.com/gofiber/fiber/v3"
	"github.com/gofiber/fiber/v3/middleware/static"
)

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

	// 提供单个文件
	app.Use("/favicon.png", static.New("./uploads/favicon.png"))

	// 提供整个目录
	app.Use("/uploads/*", static.New("./uploads"))

  // 请确保目录和文件存在, 才能正确访问到
	app.Listen(":3000")
}

实用技巧

自定义全局错误处理

go
package main

import (
	"github.com/gofiber/fiber/v3"
	recoverer "github.com/gofiber/fiber/v3/middleware/recover"
)

func main() {
	app := fiber.New(fiber.Config{
		ErrorHandler: func(ctx fiber.Ctx, err error) error {
			code := fiber.StatusInternalServerError
			message := err.Error()

			// 统一返回 json
			return ctx.Status(code).JSON(fiber.Map{
				"code":    code,
				"message": message,
			})
		},
	})

	// 这个中间件, 可以让程序 panic 后, 主程序不崩溃
	// 而是捕获错误, 统一交给 fiber.Conifg#ErrorHandler 处理
	app.Use(recoverer.New())

	app.Get("/error", func(c fiber.Ctx) error {
		// 这个文件不存在就会报错
		return c.SendFile("file-not-exists")
	})

	app.Get("/panic", func(c fiber.Ctx) error {
		// 手动让程序恐慌
		panic("custom panic")
	})

	app.Listen(":3000")
}

格式化响应体 JSON

  • 统一格式化响应体的 json 格式
go
package main

import (
	"github.com/gofiber/fiber/v3"
	recoverer "github.com/gofiber/fiber/v3/middleware/recover"
)

// 统一返回 json 的格式
type ResponseFmt struct {
	Errno   int    `json:"errno"`
	Message string `json:"message"`
	Data    any    `json:"data"`
}

// 直接作为工具函数使用
func Success(c fiber.Ctx, data any) error {
	return c.JSON(ResponseFmt{
		Errno:   0,
		Message: "success",
		Data:    data,
	})
}

func Failed(c fiber.Ctx, errno int, messages ...string) error {
	msg := "failed"
	if len(messages) > 0 {
		msg = messages[0]
	}
	return c.JSON(ResponseFmt{
		Errno:   errno,
		Message: msg,
		Data:    nil,
	})
}

func Error(c fiber.Ctx, errno int, message string) error {
	return c.JSON(ResponseFmt{
		Errno:   errno,
		Message: message,
		Data:    nil,
	})
}

func main() {
	app := fiber.New(fiber.Config{
		// 报错/panic也统一返回 json
		ErrorHandler: func(ctx fiber.Ctx, err error) error {
			status := fiber.StatusInternalServerError
			message := err.Error()
			ctx.Status(status)
			return Error(ctx, status, message)
		},
	})
	app.Use(recoverer.New())

	// 1.成功的响应
	app.Get("/success", func(c fiber.Ctx) error {
		return Success(c, fiber.Map{
			"count": 100,
			"items": []string{
				"item1",
				"item2",
				"item3",
				"item4",
			},
		})
	})

	// 2.失败响应
	app.Get("/failed", func(c fiber.Ctx) error {
		// return Failed(c, 1401)
		return Failed(c, 1401, "please login first")
	})

	// 3.程序错误响应
	app.Get("/error", func(c fiber.Ctx) error {
		return c.SendFile("file-not-exists")
	})

	// 4.程序恐慌响应
	app.Get("/panic", func(c fiber.Ctx) error {
		panic("custom panic message")
	})

	app.Listen(":3000")
}

登录认证中间件及登录接口设计

go
package main

import (
	"fmt"
	"strings"

	"github.com/gofiber/fiber/v3"
	recoverer "github.com/gofiber/fiber/v3/middleware/recover"
)

func Auth(c fiber.Ctx) error {
	// 验证 header
	authHeader := c.Get("Authorization")
	if authHeader == "" {
		return fiber.NewError(fiber.StatusUnauthorized, "please login first")
	}

	// 验证 jwt 是否正确 authorization => Bearer <token>
	accessToken := strings.TrimPrefix(authHeader, "Bearer ")
	fmt.Println("[Auth]accessToken:", accessToken)

	uid, email, err := VerifyAccessToken(accessToken)
	if err != nil {
		return fiber.NewError(fiber.StatusUnauthorized, err.Error())
	}

	c.Set("uid", string(uid))
	c.Set("email", email)

	return c.Next()
}

func main() {
	app := fiber.New(fiber.Config{
		// 报错/panic也统一返回 json
		ErrorHandler: func(c fiber.Ctx, err error) error {
			status := fiber.StatusInternalServerError
			message := err.Error()
			c.Status(status)
			return c.JSON(fiber.Map{
				"status":  status,
				"message": message,
			})
		},
	})
	app.Use(recoverer.New())

	// 1.登录接口
	app.Post("/login", func(c fiber.Ctx) error {
		// 获取 & 绑定 & 验证 请求参数
		type LoginFormDto struct {
			Email    string `json:"email" validate:"required,email"`
			Password string `json:"password" validate:"required,min=6"`
		}

		loginForm := new(LoginFormDto)
		if err := c.Bind().JSON(loginForm); err != nil {
			return err
		}

		// 模拟数据库查询
		expectedEmail := "admin@example.com"
		if loginForm.Email != expectedEmail || loginForm.Password != "123456" {
			return fiber.NewError(fiber.StatusUnauthorized, "用户名或密码错误")
		}

		// 签发访问令牌 jwt
		const userId = 1001
		accessToken, err := GenAccessToken(userId, expectedEmail)
		if err != nil {
			return err
		}

		refreshToken, err := GenRefreshToken(userId, expectedEmail)
		if err != nil {
			return err
		}

		return c.JSON(fiber.Map{
			"email":        loginForm.Email,
			"accessToken":  accessToken,
			"refreshToken": refreshToken,
		})
	})

	// 2.程序错误响应
	app.Get("/refresh_access_token", func(c fiber.Ctx) error {
		refreshToken := c.Query("refresh_token")
		if refreshToken == "" {
			return fiber.NewError(fiber.StatusBadRequest, "refresh_token 不能为空")
		}

		// 刷新 accessToken & refreshToken
		newAccessToken, newRefreshToken, err := RenewAccessToken(refreshToken)
		if err != nil {
			return err
		}

		return c.JSON(fiber.Map{
			"accessToken":  newAccessToken,
			"refreshToken": newRefreshToken,
		})
	})

	// 3.受保护的接口
	app.Get("/profile", Auth, func(c fiber.Ctx) error {
		return c.JSON(fiber.Map{
			"message": "form profile, protected api",
		})
	})

	app.Listen(":3000")
}
go
package main

import (
	"errors"
	"fmt"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"github.com/google/uuid"
)

// Token 过期时间配置
const (
	AccessTokenExpire  = time.Minute * 1    // AccessToken 1分钟
	RefreshTokenExpire = time.Hour * 24 * 7 // RefreshToken 7天

	// 实际项目中应该从环境变量读取
	AccessTokenSecret  = "access-token-secret-key-here"
	RefreshTokenSecret = "refresh-token-secret-key-here"
)

// Issuer 签发者
const Issuer = "app-name-here"

// TokenType Token 类型标识
type TokenType string

const (
	TokenTypeAccess  TokenType = "access"
	TokenTypeRefresh TokenType = "refresh"
)

// CustomClaims 自定义 JWT Claims
type CustomClaims struct {
	UserID    int       `json:"user_id"`
	Email     string    `json:"email"`
	TokenType TokenType `json:"token_type"`
	jwt.RegisteredClaims
}

// genAccessToken 生成访问令牌(短期有效)
func GenAccessToken(userId int, email string) (string, error) {
	claims := CustomClaims{
		UserID:    userId,
		Email:     email,
		TokenType: TokenTypeAccess,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(AccessTokenExpire)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			NotBefore: jwt.NewNumericDate(time.Now()),
			Issuer:    Issuer,
			Subject:   fmt.Sprintf("%d", userId),
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString([]byte(AccessTokenSecret))
}

// verifyAccessToken 验证访问令牌
func VerifyAccessToken(accessToken string) (int, string, error) {
	token, err := jwt.ParseWithClaims(accessToken, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		// 验证签名算法
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return []byte(AccessTokenSecret), nil
	})
	if err != nil {
		// 区分错误类型
		if errors.Is(err, jwt.ErrTokenExpired) {
			return 0, "", errors.New("token已过期")
		}
		if errors.Is(err, jwt.ErrTokenMalformed) {
			return 0, "", errors.New("token格式错误")
		}
		return 0, "", errors.New("token验证失败")
	}

	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		// 验证 Token 类型
		if claims.TokenType != TokenTypeAccess {
			return 0, "", errors.New("token类型错误,不是access token")
		}
		return claims.UserID, claims.Email, nil
	}

	return 0, "", errors.New("无效的token")
}

// genRefreshToken 生成刷新令牌(长期有效)
func GenRefreshToken(userId int, email string) (string, error) {
	claims := CustomClaims{
		UserID:    userId,
		Email:     email,
		TokenType: TokenTypeRefresh,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(RefreshTokenExpire)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			NotBefore: jwt.NewNumericDate(time.Now()),
			Issuer:    Issuer,
			Subject:   fmt.Sprintf("%d", userId),
			ID:        generateTokenID(), // 可选:用于黑名单机制
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString([]byte(RefreshTokenSecret))
}

// verifyRefreshToken 验证刷新令牌
func VerifyRefreshToken(refreshToken string) (int, string, error) {
	token, err := jwt.ParseWithClaims(refreshToken, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return []byte(RefreshTokenSecret), nil
	})
	if err != nil {
		if errors.Is(err, jwt.ErrTokenExpired) {
			return 0, "", errors.New("refresh token已过期,请重新登录")
		}
		return 0, "", errors.New("refresh token验证失败")
	}

	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		if claims.TokenType != TokenTypeRefresh {
			return 0, "", errors.New("token类型错误,不是refresh token")
		}
		return claims.UserID, claims.Email, nil
	}

	return 0, "", errors.New("无效的refresh token")
}

// generateTokenID 生成唯一Token ID(用于黑名单或刷新 token 轮换机制)
func generateTokenID() string {
	return uuid.New().String()
}

// RenewAccessToken 使用 Refresh Token 换取新的 Access Token
func RenewAccessToken(refreshToken string) (newAccessToken string, newRefreshToken string, err error) {
	userId, email, err := VerifyRefreshToken(refreshToken)
	if err != nil {
		return "", "", err
	}

	// 生成新的双 Token(Token 轮换机制,可选)
	accessToken, err := GenAccessToken(userId, email)
	if err != nil {
		return "", "", err
	}

	// 可选:同时刷新 refresh token(增强安全性)
	newRefresh, err := GenRefreshToken(userId, email)
	if err != nil {
		return "", "", err
	}

	return accessToken, newRefresh, nil
}
http
### kulala-config

@host = http://localhost:3000


### LOGIN_REQ

POST {{host}}/login HTTP/1.1
Content-Type: application/json

{
  "email": "admin@example.com",
  "password": "123456"
}


### refresh-access-token

GET {{host}}/refresh_access_token?refresh_token={{LOGIN_REQ.response.body.$.refreshToken}} HTTP/1.1


### profile

GET {{host}}/profile HTTP/1.1
Authorization: Bearer {{LOGIN_REQ.response.body.$.accessToken}}

Released under the MIT License.