diff --git a/AGENTS.md b/AGENTS.md index 8fe5e7b4..7b1f1ad1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ 本文件是 OpenFlare 的 AI 接手入口,不承载详细设计、规范和计划。接手项目时,请根据以下分层文档指引进行阅读与开发: -### 面向 AI 的开发指导规范 (AI Guidelines) 必须阅读 ⚠️ +### 面向 AI 的开发指导规范 (AI Guidelines) 必须阅读 为了理解 OpenFlare 的设计理念、产品边界、核心机制以及代码编写的工程约束,**AI 在接手项目时必须首先且完整阅读以下文档**: @@ -66,5 +66,5 @@ * 产品启动、部署、升级、联调方式变化:更新 `docs/guide/quick-start.md`、`docs/deployment/deployment.md` 和 `README.md` * 用户操作路径、常见场景变化:更新 `docs/guide/usage.md` * 本地开发、测试、构建方式变化:更新 `docs/design/development.md` -* 常见故障、排查路径变化:更新 `docs/guide/troubleshooting.md` * 环境变量、命令行参数、运行时配置、Agent 配置变化:更新 `docs/reference/configuration.md` +* **任何代码、配置或文档变更完成后:必须在 [`docs/changelog/index.md`](./docs/changelog/index.md) 的 `[Unreleased]` 区块补充对应条目(新增 / 变更 / 修复),格式遵循文件内已有模板。** diff --git a/docs/changelog/index.md b/docs/changelog/index.md new file mode 100644 index 00000000..be151053 --- /dev/null +++ b/docs/changelog/index.md @@ -0,0 +1,411 @@ +--- +sidebar: false +--- + +# 更新日志 + +本文件记录 OpenFlare 每个版本的重要变更。 + +格式基于 [Keep a Changelog](http://keepachangelog.com/),版本号遵循 [语义化版本](http://semver.org/)。 + +## 重大变更 + +> [!IMPORTANT] +> 2.3.2 开始使用 JWT_SECRET 环境变量替代 SESSION_SECRET 进行管理端 API 的 JWT 签名密钥管理。SESSION_SECRET 将会在之后的版本中逐步废弃,请务必尽快迁移到 JWT_SECRET。 + + +## [Unreleased] + +### 说明 + +### 新增 + +### 变更 + +--- + +## [v2.3.2] - 2026-06-04 + +### 说明 + +> [!IMPORTANT] +> 2.3.2 开始使用 JWT_SECRET 环境变量替代 SESSION_SECRET 进行管理端 API 的 JWT 签名密钥管理。SESSION_SECRET 将会在之后的版本中逐步废弃,请务必尽快迁移到 JWT_SECRET。 + +### 新增 + +- 新增 `JWT_SECRET` 环境变量,专用于管理端 API JWT 签名密钥;生产环境必须显式配置 +- 新增 VitePress 更新日志页面(`docs/changelog/index.md`),记录所有版本变更历史 + +### 变更 + +- 管理端 API 鉴权框架迁移至 `gin-jwt` +- 认证方式变更为 Headers 认证. +- `JWT_SECRET` 优先于 `SESSION_SECRET` 用于 JWT 签名;未配置时回退到 `SESSION_SECRET`,向下兼容 +- 屏蔽手动升级入口(`/api/update/manual-upload`、`/api/update/manual-upgrade`),前端隐藏对应 UI 组件 + +--- + +## [v2.3.1] - 2026-06-03 + +### 变更 + +- 屏蔽手动升级入口,前端隐藏对应 UI 组件 +- POW 与 WAF 规则合并, 统一逻辑处理 + +--- + +## [v2.3.0] - 2026-06-03 + +### 新增 + +- WAF IP 组支持订阅模式,可从远程文本或 JSON 源定时同步 +- 新增 Pages 静态站点托管,支持 SPA fallback 路由配置 +- Agent 实现 WebSocket 实时推送,Server 发布配置后立即通知在线 Agent + +### 变更 + +- Agent 数据面与 OpenResty 合并为集成镜像部署方式 +- 访问日志与观测数据支持数据库分片,按 ID 分片替代原有逻辑 + +--- + +## [v2.2.8] - 2026-06-03 + +### 修复 + +- 修复多域名部署场景下跨域认证绕过安全漏洞 + +--- + +## [v2.2.6] - 2026-06-02 + +### 新增 + +- 新增 Uptime Kuma 集成,支持自动同步监控任务 +- WAF 新增 PoW(工作量证明)防护能力,可配置有效期 + +### 变更 + +- 内网穿透支持 TunnelRelay 中继节点(frps),新增 OpenFlared 客户端(frpc) + +--- + +## [v2.2.5] - 2026-06-02 + +### 新增 + +- 新增 WAF 自动 IP 组,支持基于 Expr 规则定时聚合请求日志更新名单 +- WAF IP 组黑白名单支持直接引用 IP 组对象 + +### 变更 + +- WAF 规则组与网站解耦,支持全局规则组和自定义规则组独立管理 + +--- + +## [v2.2.4] - 2026-06-02 + +### 新增 + +- WAF 规则组新增拦截返回配置 Tab + +### 修复 + +- 修复 WAF 配置发布后部分规则不生效的问题 + +--- + +## [v2.2.3] - 2026-06-02 + +### 新增 + +- 新增 WAF 安全防护模块,支持 IP 黑白名单和地域拦截规则 + +--- + +## [v2.2.2] - 2026-06-01 + +### 变更 + +- 观测数据支持按时间窗口自动清理,新增数据库自动清理调度器 + +--- + +## [v2.2.1] - 2026-06-01 + +### 修复 + +- 修复仪表板概览数据压缩与规范化问题 + +--- + +## [v2.2.0] - 2026-06-01 + +### 新增 + +- 新增 TLS 证书转换为 ACME 托管证书的接口(`/convert-acme`) +- 新增 ACME 账号与 DNS 账号管理页面 +- 支持 Let's Encrypt 自动申请与续期 + +--- + +## [v2.1.1] - 2026-06-01 + +### 变更 + +- Agent 架构调整,采用集成镜像方式内置 OpenResty + +--- + +## [v2.0.3] - 2026-05-31 + +### 修复 + +- 修复版本号生成逻辑,确保使用当日最大序列号 + +--- + +## [v2.0.1] - 2026-05-30 + +### 修复 + +- 修复 GitHub 登录逻辑异常 + +--- + +## [v2.0.0] - 2026-05-30 + +### 新增 + +- 全面重构发布模型,引入配置版本不可变快照机制 +- 支持配置版本回滚(重新激活旧版本) +- 新增 `source_config_json` 与 `support_files` 供 Agent 获取完整配置包 +- 新增节点专属 Agent Token 与 Discovery Token 双轨鉴权 + +### 变更 + +- 数据库迁移框架切换至 goose,统一管理版本升级步骤 +- Agent API 与管理端 API 鉴权完全分离 + +--- + +## [v1.9.3] - 2026-05-30 + +### 修复 + +- 修复节点 IP 自动探测逻辑,优先使用公网地址 + +--- + +## [v1.9.2] - 2026-05-29 + +### 变更 + +- Agent 心跳超时后自动退回 HTTP 轮询模式 + +--- + +## [v1.9.1] - 2026-05-29 + +### 修复 + +- 修复 Agent WebSocket 升级失败时的重连逻辑 + +--- + +## [v1.9.0] - 2026-05-29 + +### 新增 + +- Agent 支持 WebSocket 长连接,Server 发布后实时推送配置变更 + +--- + +## [v1.8.0] - 2026-05-26 + +### 新增 + +- 支持自定义 DNS 解析器(`OpenRestyResolvers`) +- 新增历史配置快照清理功能 + +### 变更 + +- CORS 配置支持动态源与凭证 +- 上游统一渲染为命名 `upstream` 并启用 keepalive + +--- + +## [v1.7.0] - 2026-05-25 + +### 新增 + +- 新增 ACME 和 DNS 账号管理功能,支持证书申请与续期 + +### 变更 + +- 移除新用户注册功能 +- 更新 Go 版本要求至 1.25+ + +--- + +## [v1.6.1] - 2026-05-13 + +### 修复 + +- 修复个人设置页无法查看第三方认证源及解绑功能 + +--- + +## [v1.6.0] - 2026-05-13 + +### 新增 + +- 支持 OIDC 单点登录(SSO) + +--- + +## [v1.5.0] - 2026-04-25 + +### 新增 + +- 集成 PoW(Anubis)防护,支持有效期配置 + +--- + +## [v1.4.0] - 2026-04-01 + +### 新增 + +- 支持域名级别独立绑定 TLS 证书,每个域名可单独选择证书 +- 新增批量更新配置项接口 +- 新增 Agent 卸载脚本 + +### 变更 + +- 禁用新用户自助注册 +- 默认服务器块新增 HTTPS 握手拒绝支持 + +--- + +## [v1.3.2] - 2026-03-30 + +### 新增 + +- 网站配置支持多域名绑定与共享设置 +- 新增抽屉式规则创建组件 + +--- + +## [v1.3.1] - 2026-03-20 + +### 新增 + +- 新增源站管理功能,支持源站创建、更新与删除 + +### 变更 + +- 重构代理路由页面,优化输入组件与样式 + +--- + +## [v1.3.0] - 2026-03-19 + +### 新增 + +- 新增数据库观测数据手动和自动清理策略 +- 节点访问日志支持数据库分片,按 ID 分片 + +### 变更 + +- 数据库版本管理与迁移逻辑重构 + +--- + +## [v1.2.0] - 2026-03-19 + +### 新增 + +- 支持多上游地址负载均衡 +- 新增缓存策略配置(路径前缀、精确路径) +- 节点健康事件清理功能 + +### 变更 + +- 上游渲染改为命名 upstream 并启用 keepalive +- 更新 HTTPS 配置,启用 reuseport 与 epoll 事件模型 + +--- + +## [v1.1.2] - 2026-03-18 + +### 变更 + +- HTTPS 启用 HTTP/2 支持 + +--- + +## [v1.1.1] - 2026-03-18 + +### 新增 + +- 新增获取配置版本详情 API + +### 变更 + +- 仪表板概览数据结构优化,添加压缩与规范化 + +--- + +## [v1.1.0] - 2026-03-18 + +### 新增 + +- 新增应用日志分页查询与清理功能 +- 新增访问日志 IP 汇总与趋势查询 +- 新增 OpenResty DNS 解析器指令支持 +- Docker 部署支持在运行中容器内执行 reload + +### 修复 + +- 修复应用结果警告逻辑 +- Lua 和证书文件管理重构,优化文件同步与清理机制 + +--- + +## [v1.0.2] - 2026-03-17 + +### 新增 + +- 支持 PostgreSQL 数据库,添加数据库迁移逻辑 +- 新增 Docker Compose 配置,支持 PostgreSQL 联动部署 + +### 变更 + +- 多个管理端 API 请求方法从 PUT/DELETE 统一改为 POST + +--- + +## [v1.0.1] - 2026-03-16 + +### 新增 + +- 新增 `origin_host` 字段,支持覆盖回源请求的 Host 头 + +### 修复 + +- 修复代理配置中 SSL 服务器名称和主机头覆盖逻辑 + +--- + +## [v1.0.0] - 2026-03-15 + +OpenFlare 首个正式版本发布。 + +### 新增 + +- 管理端 UI、管理 API、Agent API 基础功能 +- 反向代理配置管理与 OpenResty 配置渲染 +- 配置版本发布与 Agent 同步 +- TLS 证书导入与管理 +- 节点注册、心跳与状态观测 +- SQLite 数据库支持 diff --git a/docs/config.ts b/docs/config.ts index 331c18e3..f31aee02 100644 --- a/docs/config.ts +++ b/docs/config.ts @@ -10,7 +10,8 @@ export default defineAdditionalConfig({ sidebar: { '/guide/': { base: '/guide/', items: sidebarGuide() }, '/reference/': { base: '/reference/', items: sidebarReference() }, - '/design/': { base: '/design/', items: sidebarDesign() } + '/design/': { base: '/design/', items: sidebarDesign() }, + '/changelog/': false }, editLink: { @@ -57,7 +58,8 @@ function nav(): DefaultTheme.NavItem[] { return [ { text: '指南', link: '/guide/', activeMatch: '/guide/' }, { text: '参考', link: '/reference/', activeMatch: '/reference/' }, - { text: '设计', link: '/design/', activeMatch: '/design/' } + { text: '设计', link: '/design/', activeMatch: '/design/' }, + { text: '更新日志', link: '/changelog/', activeMatch: '/changelog/' } ] } diff --git a/docs/deployment/deployment.md b/docs/deployment/deployment.md index 388679a8..9bd44559 100644 --- a/docs/deployment/deployment.md +++ b/docs/deployment/deployment.md @@ -2,7 +2,7 @@ 你会学到:OpenFlare 的推荐部署方式、Server 与 Agent 的运行要求、源码启动方式、联调步骤、升级与卸载入口。 -生产环境建议使用 PostgreSQL 作为 Server 数据库,并为 Server 显式配置 `SESSION_SECRET`。Agent 部署方式推荐为 Docker 部署(即直接使用内置 OpenResty 的 Agent 镜像);亦支持通过安装脚本或手动本地运行。 +生产环境建议使用 PostgreSQL 作为 Server 数据库,并为 Server 显式配置 `JWT_SECRET`。Agent 部署方式推荐为 Docker 部署(即直接使用内置 OpenResty 的 Agent 镜像);亦支持通过安装脚本或手动本地运行。 ## 部署拓扑 @@ -107,7 +107,7 @@ services: ports: - "3000:3000" environment: - SESSION_SECRET: replace-with-a-long-random-string + JWT_SECRET: replace-with-a-long-random-string DSN: postgres://openflare:replace-with-strong-password@postgres:5432/openflare?sslmode=disable GIN_MODE: release LOG_LEVEL: info @@ -144,7 +144,7 @@ pnpm build ```bash cd openflare_server -export SESSION_SECRET='replace-with-a-long-random-string' +export JWT_SECRET='replace-with-a-long-random-string' export SQLITE_PATH='./openflare.db' export LOG_LEVEL='info' # 可选:设置后优先使用 PostgreSQL。 diff --git a/docs/deployment/server.md b/docs/deployment/server.md index 4ce2c9ae..02667a36 100644 --- a/docs/deployment/server.md +++ b/docs/deployment/server.md @@ -13,7 +13,7 @@ OpenFlare Server 是 Gin + GORM 单体控制面,负责管理端 UI、管理 AP | pnpm | 推荐通过 `corepack enable` 使用项目声明的 pnpm | | 数据库 | SQLite 文件目录可写,或可访问的 PostgreSQL 实例 | -生产环境建议显式配置 `SESSION_SECRET`,并优先使用 PostgreSQL。 +生产环境必须显式配置 `JWT_SECRET`,并优先使用 PostgreSQL。 ## 构建管理端前端 @@ -38,7 +38,7 @@ pnpm test ```bash cd openflare_server -export SESSION_SECRET='replace-with-a-long-random-string' +export JWT_SECRET='replace-with-a-long-random-string' export SQLITE_PATH='./openflare.db' export LOG_LEVEL='info' go run . @@ -54,7 +54,7 @@ http://localhost:3000 ```bash cd openflare_server -export SESSION_SECRET='replace-with-a-long-random-string' +export JWT_SECRET='replace-with-a-long-random-string' export DSN='postgres://openflare:secret@127.0.0.1:5432/openflare?sslmode=disable' export LOG_LEVEL='info' go run . @@ -81,7 +81,7 @@ docker run -d \ --name openflare-server \ -p 3000:3000 \ -v $(pwd)/openflare-data:/data \ - -e SESSION_SECRET='replace-with-a-long-random-string' \ + -e JWT_SECRET='replace-with-a-long-random-string' \ -e SQLITE_PATH='/data/openflare.db' \ -e GIN_MODE='release' \ -e LOG_LEVEL='info' \ @@ -91,7 +91,7 @@ docker run -d \ 启动参数说明: * **`-p 3000:3000`**:映射宿主机 `3000` 端口到容器内 `3000` 端口。 * **`-v $(pwd)/openflare-data:/data`**:挂载本地目录到容器的 `/data`,确保数据库文件 `openflare.db` 在重启或重建容器时不丢失。 -* **`SESSION_SECRET`**:建议配置的临时 Session 签名密钥,主要用于 OAuth 状态等非管理端 API 鉴权流程;管理端 API 登录凭证通过 `OPENFLARE_TOKEN` 请求头传递。 +* **`JWT_SECRET`**:管理端 API 登录令牌的 JWT 签名密钥,生产环境必须配置,避免重启后已登录令牌全部失效。 --- @@ -127,7 +127,7 @@ services: ports: - "3000:3000" environment: - SESSION_SECRET: replace-with-random-string + JWT_SECRET: replace-with-random-string SQLITE_PATH: /data/openflare.db DSN: postgres://openflare:replace-with-strong-password@postgres:5432/openflare?sslmode=disable GIN_MODE: release diff --git a/docs/design/development.md b/docs/design/development.md index c5ce4e4e..8aa980c1 100644 --- a/docs/design/development.md +++ b/docs/design/development.md @@ -39,7 +39,7 @@ SQLite 模式: ```bash cd openflare_server -export SESSION_SECRET='dev-session-secret' +export JWT_SECRET='dev-jwt-secret' export SQLITE_PATH='./openflare-dev.db' export LOG_LEVEL='debug' go run . @@ -49,7 +49,7 @@ PostgreSQL 模式: ```bash cd openflare_server -export SESSION_SECRET='dev-session-secret' +export JWT_SECRET='dev-jwt-secret' export DSN='postgres://openflare:secret@127.0.0.1:5432/openflare?sslmode=disable' export LOG_LEVEL='debug' go run . diff --git a/docs/guide/quick-start.md b/docs/guide/quick-start.md index a5db80a4..01ad4d1b 100644 --- a/docs/guide/quick-start.md +++ b/docs/guide/quick-start.md @@ -54,7 +54,7 @@ services: ports: - "3000:3000" environment: - SESSION_SECRET: replace-with-a-long-random-string + JWT_SECRET: replace-with-a-long-random-string DSN: postgres://openflare:replace-with-strong-password@postgres:5432/openflare?sslmode=disable GIN_MODE: release LOG_LEVEL: info diff --git a/docs/guide/usage.md b/docs/guide/usage.md index 3ccda5a5..229bb7a0 100644 --- a/docs/guide/usage.md +++ b/docs/guide/usage.md @@ -164,7 +164,7 @@ WAF 规则组、网站绑定或 PoW 配置修改后,需要重新发布并激 ## 推荐实践 -* 生产环境显式配置 `SESSION_SECRET`,并优先使用 PostgreSQL。 +* 生产环境必须显式配置 `JWT_SECRET`,并优先使用 PostgreSQL。 * 修改网站配置后先看预览或 diff,再发布。 * 每次发布后检查节点详情与应用记录。 * 多节点部署时保持 Agent 到 Server 的网络路径稳定。 diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 2b9dc749..de953cf4 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -8,7 +8,7 @@ ```bash cd openflare_server -export SESSION_SECRET='replace-with-random-string' +export JWT_SECRET='replace-with-random-string' export SQLITE_PATH='./openflare.db' export LOG_LEVEL='info' go run . diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 45a11d67..1a0e18a1 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -64,7 +64,7 @@ go run . --port 3000 --log-dir ./logs | `PORT` | Server 监听端口 | `3000` | | `GIN_MODE` | Gin 运行模式 | 非 `debug` 时按 release | | `LOG_LEVEL` | 日志等级 | `info` | -| `SESSION_SECRET` | 临时 Session 签名密钥,主要用于 OAuth 状态等非管理端 API 鉴权流程 | 启动时随机生成 | +| `JWT_SECRET` | 管理端 API 登录令牌的 JWT 签名密钥,生产环境必须显式配置 | 启动时随机生成 | | `SQLITE_PATH` | SQLite 数据库文件路径 | `openflare.db` | | `DSN` | PostgreSQL DSN,设置后优先于 SQLite | 空 | | `SQL_DSN` | 兼容旧命名的 PostgreSQL DSN,优先级低于 `DSN` | 空 | @@ -76,7 +76,7 @@ go run . --port 3000 --log-dir ./logs * `DSN` 与 `SQL_DSN` 同时存在时优先使用 `DSN`。 * `DSN` 或 `SQL_DSN` 与 `SQLITE_PATH` 同时存在时优先使用 PostgreSQL。 * 当目标 PostgreSQL 数据库为空且本地 `SQLITE_PATH` 文件存在时,Server 启动阶段会自动迁移 SQLite 数据,并在日志中输出按表迁移进度。 -* `SESSION_SECRET` 生产环境建议显式配置,避免 OAuth 授权状态等临时会话在重启后失效;管理端 API 登录凭证不再通过 Cookie Session 传递,而是使用 `OPENFLARE_TOKEN` 请求头。 +* `JWT_SECRET` 用于管理端 API 登录令牌的签名与验证,生产环境必须显式配置,避免重启后所有已登录令牌失效。 * `REDIS_CONN_STRING` 未配置时,相关能力回退为进程内实现。 ## 运行时 Option @@ -295,7 +295,7 @@ OpenResty 性能参数与缓存参数继续统一保存在 `Option` 表。当前 ### 生产 Server + PostgreSQL ```bash -export SESSION_SECRET='replace-with-a-long-random-string' +export JWT_SECRET='replace-with-a-long-random-string' export DSN='postgres://openflare:replace-with-strong-password@postgres:5432/openflare?sslmode=disable' export GIN_MODE='release' export LOG_LEVEL='info' @@ -304,7 +304,7 @@ export LOG_LEVEL='info' ### 本地 Server + SQLite ```bash -export SESSION_SECRET='dev-session-secret' +export JWT_SECRET='dev-jwt-secret' export SQLITE_PATH='./openflare-dev.db' export LOG_LEVEL='debug' go run . diff --git a/openflare_server/common/constants.go b/openflare_server/common/constants.go index 25c22e9b..589db47b 100644 --- a/openflare_server/common/constants.go +++ b/openflare_server/common/constants.go @@ -17,6 +17,7 @@ var HomePageLink = "" // Any options with "Secret", "Token" in its key won't be return by GetOptions var SessionSecret = uuid.New().String() +var JWTSecret = "" // if empty, falls back to SessionSecret; set via JWT_SECRET env var var SQLitePath = "openflare.db" var SQLDSN = "" diff --git a/openflare_server/common/init.go b/openflare_server/common/init.go index ff932412..fcd4b7a1 100644 --- a/openflare_server/common/init.go +++ b/openflare_server/common/init.go @@ -48,6 +48,9 @@ func ParseFlags() { if os.Getenv("SESSION_SECRET") != "" { SessionSecret = os.Getenv("SESSION_SECRET") } + if os.Getenv("JWT_SECRET") != "" { + JWTSecret = os.Getenv("JWT_SECRET") + } if os.Getenv("SQLITE_PATH") != "" { SQLitePath = os.Getenv("SQLITE_PATH") } diff --git a/openflare_server/controller/user.go b/openflare_server/controller/user.go index 73a720cc..fe02e45c 100644 --- a/openflare_server/controller/user.go +++ b/openflare_server/controller/user.go @@ -1,16 +1,14 @@ package controller import ( - "errors" "openflare/common" + "openflare/middleware" "openflare/model" "openflare/utils/security" "openflare/utils/validation" "strconv" - "strings" "github.com/gin-gonic/gin" - "github.com/google/uuid" ) type LoginRequest struct { @@ -47,17 +45,22 @@ func Login(c *gin.Context) { // setup token and then return user info func setLoginToken(user *model.User) (*model.User, error) { - token, err := ensureUserOpenFlareToken(user) + // Generate a signed JWT using gin-jwt middleware + tokenString, _, err := middleware.JWTMiddleware.TokenGenerator(user) if err != nil { return nil, err } + // Persist JWT in DB so we can invalidate it on logout + if err := model.DB.Model(user).Update("token", tokenString).Error; err != nil { + return nil, err + } cleanUser := &model.User{ Id: user.Id, Username: user.Username, DisplayName: user.DisplayName, Role: user.Role, Status: user.Status, - Token: token, + Token: tokenString, } return cleanUser, nil } @@ -85,24 +88,6 @@ func Logout(c *gin.Context) { respondSuccessMessage(c, "") } -func ensureUserOpenFlareToken(user *model.User) (string, error) { - if user.Token != "" { - return user.Token, nil - } - for i := 0; i < 3; i++ { - token := strings.Replace(uuid.New().String(), "-", "", -1) - if model.DB.Where("token = ?", token).First(&model.User{}).RowsAffected != 0 { - continue - } - if err := model.DB.Model(user).Update("token", token).Error; err != nil { - return "", err - } - user.Token = token - return token, nil - } - return "", errors.New("生成登录凭证失败,请重试") -} - func currentUserFromOpenFlareToken(c *gin.Context) *model.User { token := c.GetHeader("OpenFlare-Token") if token == "" { @@ -163,19 +148,17 @@ func GenerateToken(c *gin.Context) { respondFailure(c, err.Error()) return } - user.Token = uuid.New().String() - user.Token = strings.Replace(user.Token, "-", "", -1) - - if model.DB.Where("token = ?", user.Token).First(user).RowsAffected != 0 { - respondFailure(c, "请重试,系统生成的 UUID 竟然重复了!") + // Generate a fresh JWT for the user + tokenString, _, err := middleware.JWTMiddleware.TokenGenerator(user) + if err != nil { + respondFailure(c, "生成 Token 失败: "+err.Error()) return } - + user.Token = tokenString if err := user.Update(false); err != nil { respondFailure(c, err.Error()) return } - respondSuccess(c, user.Token) } diff --git a/openflare_server/go.mod b/openflare_server/go.mod index 217f270d..e6ff2ba9 100644 --- a/openflare_server/go.mod +++ b/openflare_server/go.mod @@ -9,10 +9,10 @@ require ( github.com/gin-contrib/cors v1.6.0 github.com/gin-contrib/sessions v0.0.5 github.com/gin-contrib/static v0.0.1 - github.com/gin-gonic/gin v1.9.1 + github.com/gin-gonic/gin v1.10.0 github.com/glebarez/sqlite v1.11.0 github.com/go-acme/lego/v4 v4.35.2 - github.com/go-playground/validator/v10 v10.23.0 + github.com/go-playground/validator/v10 v10.25.0 github.com/go-redis/redis/v8 v8.11.5 github.com/google/uuid v1.6.0 github.com/oschwald/maxminddb-golang v1.13.1 @@ -32,16 +32,19 @@ require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/appleboy/gin-jwt/v2 v2.10.3 // indirect github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect - github.com/bytedance/sonic v1.11.2 // indirect + github.com/bytedance/sonic v1.12.9 // indirect + github.com/bytedance/sonic/loader v0.2.3 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-contrib/sse v1.0.0 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect @@ -50,7 +53,8 @@ require ( github.com/go-openapi/swag v0.19.15 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/gomodule/redigo v2.0.0+incompatible // indirect github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/securecookie v1.1.1 // indirect @@ -63,7 +67,7 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/longbridgeapp/sqlparser v0.3.1 // indirect github.com/mailru/easyjson v0.7.6 // indirect @@ -73,13 +77,14 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/ncruces/go-strftime v1.0.0 // indirect - github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/arch v0.7.0 // indirect + golang.org/x/arch v0.14.0 // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/sync v0.20.0 // indirect diff --git a/openflare_server/go.sum b/openflare_server/go.sum index fd804ba7..4dccddfd 100644 --- a/openflare_server/go.sum +++ b/openflare_server/go.sum @@ -6,6 +6,8 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/appleboy/gin-jwt/v2 v2.10.3 h1:KNcPC+XPRNpuoBh+j+rgs5bQxN+SwG/0tHbIqpRoBGc= +github.com/appleboy/gin-jwt/v2 v2.10.3/go.mod h1:LDUaQ8mF2W6LyXIbd5wqlV2SFebuyYs4RDwqMNgpsp8= github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04= github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= @@ -14,6 +16,11 @@ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1 github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.11.2 h1:ywfwo0a/3j9HR8wsYGWsIWl2mvRsI950HyoxiBERw5A= github.com/bytedance/sonic v1.11.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/bytedance/sonic v1.12.9 h1:Od1BvK55NnewtGaJsTDeAOSnLVO2BTSLOe0+ooKokmQ= +github.com/bytedance/sonic v1.12.9/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= +github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -25,6 +32,9 @@ github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpV github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -52,11 +62,15 @@ github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfm github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U= github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= @@ -87,6 +101,8 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91 github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= +github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= @@ -95,6 +111,10 @@ github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= @@ -134,6 +154,8 @@ github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= @@ -180,6 +202,8 @@ github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5 github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -196,6 +220,7 @@ github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLy github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -204,6 +229,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= @@ -218,12 +244,16 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4= +golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= diff --git a/openflare_server/main.go b/openflare_server/main.go index cbccd136..21c7c5b8 100644 --- a/openflare_server/main.go +++ b/openflare_server/main.go @@ -73,6 +73,7 @@ func main() { // Initialize options model.InitOptionMap() + middleware.InitJWTMiddleware() geoip.InitGeoIP(common.GeoIPProvider) backgroundCtx, cancelBackgroundTasks := context.WithCancel(context.Background()) defer cancelBackgroundTasks() diff --git a/openflare_server/middleware/auth.go b/openflare_server/middleware/auth.go index e570574f..434d6605 100644 --- a/openflare_server/middleware/auth.go +++ b/openflare_server/middleware/auth.go @@ -1,17 +1,19 @@ package middleware import ( - "github.com/gin-gonic/gin" "net/http" "openflare/common" "openflare/model" + + jwt "github.com/appleboy/gin-jwt/v2" + "github.com/gin-gonic/gin" ) const OpenFlareTokenHeader = "OpenFlare-Token" func authHelper(c *gin.Context, minRole int) { - token := c.GetHeader(OpenFlareTokenHeader) - if token == "" { + tokenStr := c.GetHeader(OpenFlareTokenHeader) + if tokenStr == "" { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "message": "无权进行此操作,未登录或 token 无效", @@ -20,16 +22,49 @@ func authHelper(c *gin.Context, minRole int) { return } - user := model.ValidateUserToken(token) - if user == nil || user.Username == "" { + token, err := JWTMiddleware.ParseTokenString(tokenStr) + if err != nil { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, - "message": "无权进行此操作,token 无效", + "message": "无权进行此操作,token 无效: " + err.Error(), }) c.Abort() return } - if user.Status == common.UserStatusDisabled { + + claims := jwt.ExtractClaimsFromToken(token) + id, ok := claims["id"].(float64) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{ + "success": false, + "message": "无权进行此操作,token 格式错误", + }) + c.Abort() + return + } + + dbUser := &model.User{} + dbErr := model.DB.Select([]string{"id", "username", "display_name", "role", "status", "token"}). + First(dbUser, "id = ?", int(id)).Error + if dbErr != nil || dbUser.Username == "" { + c.JSON(http.StatusUnauthorized, gin.H{ + "success": false, + "message": "无权进行此操作,用户不存在", + }) + c.Abort() + return + } + + if dbUser.Token != tokenStr { + c.JSON(http.StatusUnauthorized, gin.H{ + "success": false, + "message": "无权进行此操作,token 已失效或已登出", + }) + c.Abort() + return + } + + if dbUser.Status == common.UserStatusDisabled { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "用户已被封禁", @@ -37,7 +72,8 @@ func authHelper(c *gin.Context, minRole int) { c.Abort() return } - if user.Role < minRole { + + if int(dbUser.Role) < minRole { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "无权进行此操作,权限不足", @@ -45,9 +81,10 @@ func authHelper(c *gin.Context, minRole int) { c.Abort() return } - c.Set("username", user.Username) - c.Set("role", user.Role) - c.Set("id", user.Id) + + c.Set("username", dbUser.Username) + c.Set("role", dbUser.Role) + c.Set("id", dbUser.Id) c.Set("authByToken", true) c.Next() } diff --git a/openflare_server/middleware/jwt.go b/openflare_server/middleware/jwt.go new file mode 100644 index 00000000..a32be3e4 --- /dev/null +++ b/openflare_server/middleware/jwt.go @@ -0,0 +1,73 @@ +package middleware + +import ( + "log" + "openflare/common" + "openflare/model" + "time" + + jwt "github.com/appleboy/gin-jwt/v2" + "github.com/gin-gonic/gin" +) + +var JWTMiddleware *jwt.GinJWTMiddleware + +// jwtSigningKey returns JWT_SECRET when set, falling back to SESSION_SECRET +// for backward compatibility with deployments that only configure SESSION_SECRET. +func jwtSigningKey() []byte { + if common.JWTSecret != "" { + return []byte(common.JWTSecret) + } + return []byte(common.SessionSecret) +} + +func InitJWTMiddleware() { + var err error + JWTMiddleware, err = jwt.New(&jwt.GinJWTMiddleware{ + Realm: "openflare", + Key: jwtSigningKey(), + Timeout: 24 * time.Hour, + MaxRefresh: 24 * time.Hour, + IdentityKey: "identity", + PayloadFunc: func(data interface{}) jwt.MapClaims { + if v, ok := data.(*model.User); ok { + return jwt.MapClaims{ + "id": v.Id, + "username": v.Username, + "role": v.Role, + } + } + return jwt.MapClaims{} + }, + IdentityHandler: func(c *gin.Context) interface{} { + claims := jwt.ExtractClaims(c) + id, ok := claims["id"].(float64) + if !ok { + return nil + } + username, _ := claims["username"].(string) + role, _ := claims["role"].(float64) + return &model.User{ + Id: int(id), + Username: username, + Role: int(role), + } + }, + Authorizator: func(data interface{}, c *gin.Context) bool { + return data != nil + }, + Unauthorized: func(c *gin.Context, code int, message string) { + c.JSON(code, gin.H{ + "success": false, + "message": "无权进行此操作,未登录或 token 无效: " + message, + }) + }, + TokenLookup: "header: OpenFlare-Token", + TokenHeadName: "", // Empty for raw token value directly + SendCookie: false, + }) + + if err != nil { + log.Fatalf("JWT Init Error: %s", err.Error()) + } +} diff --git a/openflare_server/model/user.go b/openflare_server/model/user.go index d0ebc3cc..a060d840 100644 --- a/openflare_server/model/user.go +++ b/openflare_server/model/user.go @@ -4,7 +4,6 @@ import ( "errors" "openflare/common" "openflare/utils/security" - "strings" ) // User if you add sensitive fields, don't forget to clean them in setupLogin function. @@ -150,11 +149,13 @@ func (user *User) FillUserByUsername() error { return nil } +// ValidateUserToken looks up a user by their stored JWT token string. +// JWT signature verification is handled by middleware/auth.go; this +// function is used by Logout to find and clear the token from DB. func ValidateUserToken(token string) (user *User) { if token == "" { return nil } - token = strings.Replace(token, "Bearer ", "", 1) user = &User{} if DB.Where("token = ?", token).First(user).RowsAffected == 1 { return user diff --git a/openflare_server/router/api_phase1_test.go b/openflare_server/router/api_phase1_test.go index a91278be..823783f2 100644 --- a/openflare_server/router/api_phase1_test.go +++ b/openflare_server/router/api_phase1_test.go @@ -17,6 +17,7 @@ import ( "net/http" "net/http/httptest" "openflare/common" + "openflare/middleware" "openflare/model" "openflare/router" "openflare/service" @@ -485,6 +486,7 @@ func setupTestDB(t *testing.T) { if err := model.InitDB(); err != nil { t.Fatalf("failed to init db: %v", err) } + middleware.InitJWTMiddleware() t.Cleanup(func() { if err := model.CloseDB(); err != nil { t.Fatalf("failed to close db: %v", err) @@ -498,11 +500,15 @@ func prepareRootToken(t *testing.T) string { if err := user.FillUserByUsername(); err != nil { t.Fatalf("failed to load root user: %v", err) } - user.Token = "phase1-test-token" - if err := model.DB.Model(user).Update("token", user.Token).Error; err != nil { + // Generate a proper JWT so auth middleware can validate it + tokenString, _, err := middleware.JWTMiddleware.TokenGenerator(user) + if err != nil { + t.Fatalf("failed to generate JWT for root user: %v", err) + } + if err := model.DB.Model(user).Update("token", tokenString).Error; err != nil { t.Fatalf("failed to set root token: %v", err) } - return user.Token + return tokenString } func performJSONRequest(t *testing.T, engine http.Handler, token string, method string, path string, body any) apiResponse {