mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
[优化] 更换 JWT 认证机制
This commit is contained in:
@@ -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]` 区块补充对应条目(新增 / 变更 / 修复),格式遵循文件内已有模板。**
|
||||
|
||||
@@ -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 数据库支持
|
||||
+4
-2
@@ -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/' }
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 .
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -164,7 +164,7 @@ WAF 规则组、网站绑定或 PoW 配置修改后,需要重新发布并激
|
||||
|
||||
## 推荐实践
|
||||
|
||||
* 生产环境显式配置 `SESSION_SECRET`,并优先使用 PostgreSQL。
|
||||
* 生产环境必须显式配置 `JWT_SECRET`,并优先使用 PostgreSQL。
|
||||
* 修改网站配置后先看预览或 diff,再发布。
|
||||
* 每次发布后检查节点详情与应用记录。
|
||||
* 多节点部署时保持 Agent 到 Server 的网络路径稳定。
|
||||
|
||||
@@ -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 .
|
||||
|
||||
@@ -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 .
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+13
-8
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -73,6 +73,7 @@ func main() {
|
||||
|
||||
// Initialize options
|
||||
model.InitOptionMap()
|
||||
middleware.InitJWTMiddleware()
|
||||
geoip.InitGeoIP(common.GeoIPProvider)
|
||||
backgroundCtx, cancelBackgroundTasks := context.WithCancel(context.Background())
|
||||
defer cancelBackgroundTasks()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user