feat(auth): 鉴权片2 —— 前端登录闭环 + 保护路由 + 去掉 header 兜底
把 JWT 鉴权从后端核心闭环到端到端: 后端: - middleware.RequireAuth:上下文无已验证 uid 则 401;挂在 owner 作用域业务路由组。 - 路由拆 公开/受保护:公开=auth/health + 按 task_id 寻址的 SSE 与报告导出 (EventSource/下载无法带 Bearer);受保护=tasks/memory/kb*/agents/reports/billing。 - userID(c) 去掉 X-User-ID 兜底,仅信任 JWT 注入的 uid。 - 修 CORS:Allow-Headers 增 Authorization(否则浏览器拦截带 Bearer 的请求)。 前端: - lib/api:token 存 localStorage + Bearer 头(不再发 X-User-ID)+ authRegister/Login/Me + 401 清令牌并广播 sdx:logout;submitTask/report/memory/列表加载走 Bearer 与 401 守卫。 - views/Login:登录/注册全屏门。 - App:启动校验令牌 → 无则渲染 Login,有则进主应用;identity.userId=已验证 user.id; 监听 sdx:logout 回登录页。 - TopBar:去掉可编辑身份输入,改显登录用户 + 登出。 实跑验证(docker+gateway+preview): - RequireAuth:无 token /kb/list、/agents → 401;/health → 200;带 token → 200。 - 前端:无 token 显登录门;注入有效 token 重载 → 进主应用、顶栏显 Dexter、KB 加载本人库、 隔离徽标显雪花 uid。控制台无错、生产构建通过。 - 过程中发现并修复 CORS 缺 Authorization 头的真实 bug。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -184,17 +184,14 @@ func (h *Handler) SetMemory(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "message": res.Content})
|
||||
}
|
||||
|
||||
// userID 取当前用户标识:优先 JWT 鉴权中间件注入的已验证 uid;
|
||||
// 兜底 X-User-ID 头(开发期 / 前端尚未接登录),都没有则匿名。
|
||||
// userID 取当前用户标识 —— 仅信任 JWT 鉴权中间件注入的已验证 uid(不再认 header)。
|
||||
// 受保护路由有 RequireAuth 兜底,此处理论上不会返回 anonymous。
|
||||
func userID(c *gin.Context) string {
|
||||
if v, ok := c.Get("uid"); ok {
|
||||
if s, _ := v.(string); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
if u := c.GetHeader("X-User-ID"); u != "" {
|
||||
return u
|
||||
}
|
||||
return "anonymous"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -25,3 +26,17 @@ func Auth() gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAuth 在 Auth 之后使用:上下文无已验证 userID 则 401 拒绝。
|
||||
// 用于 owner 作用域的业务路由;SSE/导出等按 task_id 寻址的端点不挂(EventSource 无法带头)。
|
||||
func RequireAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if v, ok := c.Get(CtxUserID); ok {
|
||||
if s, _ := v.(string); s != "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "需要登录"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,36 +22,40 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
||||
h := handler.New(db, cache, bus, blobStore)
|
||||
api := r.Group("/api/v1")
|
||||
{
|
||||
// —— 公开:鉴权端点 / 健康 / 按 task_id 寻址的 SSE 与导出(EventSource/下载无法带 Bearer)——
|
||||
api.POST("/auth/register", h.Register) // 注册 + 签发 JWT
|
||||
api.POST("/auth/login", h.Login) // 登录 + 签发 JWT
|
||||
api.GET("/auth/me", h.Me) // 当前登录用户
|
||||
api.GET("/auth/me", h.Me) // 当前登录用户(无效令牌 → 401)
|
||||
api.GET("/health", h.Health) // 依赖健康聚合(顶栏五盏灯)
|
||||
api.GET("/tasks/:id/stream", h.StreamTask) // SSE 回流 Token Stream(task_id 寻址)
|
||||
api.GET("/tasks/:id/exec", h.StreamExec) // SSE 回流执行轨迹(task_id 寻址)
|
||||
api.GET("/kb/ingest/:id/stream", h.KbIngestStream) // 入库进度 SSE(job_id 寻址)
|
||||
api.GET("/reports/:id/export", h.ExportReport) // 按需导出(report_id 寻址)
|
||||
api.GET("/reports/:id/download", h.ExportReport) // 兼容旧入口(默认 docx)
|
||||
|
||||
api.POST("/tasks", h.SubmitTask) // 1. 解析 DSL 并 Publish 到 NATS
|
||||
api.GET("/tasks/:id/stream", h.StreamTask) // 4. SSE/WS 回流 Token Stream
|
||||
api.GET("/tasks/:id/exec", h.StreamExec) // 4b. SSE 回流执行轨迹事件(运行·观测)
|
||||
api.PUT("/memory", h.SetMemory) // 偏好记忆登记(→ mcp-go memory_upsert)
|
||||
api.GET("/kb/list", h.KbList) // 当前用户的知识库列表(owner 隔离)
|
||||
api.POST("/kb/create", h.KbCreate) // 新建知识库(项目/案件/文件夹/通用)
|
||||
api.POST("/kb/ingest", h.KbIngest) // 知识库入库(文本,→ mcp-go kb_ingest)
|
||||
api.POST("/kb/ingest_file", h.KbIngestFile) // 文件入库(docx/xlsx/pdf… 异步)
|
||||
api.GET("/kb/ingest/:id/stream", h.KbIngestStream) // 入库进度 SSE(实时监控)
|
||||
api.POST("/kb/search", h.KbSearch) // 知识库检索台(→ mcp-go kb_search)
|
||||
api.GET("/kb/vault", h.KbVault) // 文库:文档列表(仅元数据+预览)
|
||||
api.GET("/kb/doc", h.KbDoc) // 取单篇文档全文(按需加载)
|
||||
api.GET("/kb/links", h.KbLinks) // 某库全部 [[双链]](反链/笔记关系图)
|
||||
api.POST("/kb/note", h.KbSaveNote) // 新建/编辑笔记(落库 + 按 doc 重入库)
|
||||
api.GET("/kb/graph", h.KbGraph) // 知识图谱三元组(→ mcp-go kb_graph,Neo4j)
|
||||
// —— 受保护:owner 作用域业务,必须携带有效 JWT ——
|
||||
p := api.Group("", middleware.RequireAuth())
|
||||
{
|
||||
p.POST("/tasks", h.SubmitTask) // 解析 DSL 并 Publish 到 NATS(带已验证 uid)
|
||||
p.PUT("/memory", h.SetMemory) // 偏好记忆登记(→ mcp-go memory_upsert)
|
||||
p.GET("/kb/list", h.KbList) // 当前用户的知识库列表(owner 隔离)
|
||||
p.POST("/kb/create", h.KbCreate) // 新建知识库
|
||||
p.POST("/kb/ingest", h.KbIngest) // 文本入库
|
||||
p.POST("/kb/ingest_file", h.KbIngestFile) // 文件入库
|
||||
p.POST("/kb/search", h.KbSearch) // 检索台
|
||||
p.GET("/kb/vault", h.KbVault) // 文库列表
|
||||
p.GET("/kb/doc", h.KbDoc) // 取单篇文档
|
||||
p.GET("/kb/links", h.KbLinks) // 某库双链
|
||||
p.POST("/kb/note", h.KbSaveNote) // 新建/编辑笔记
|
||||
p.GET("/kb/graph", h.KbGraph) // 知识图谱三元组
|
||||
p.GET("/agents", h.AgentList) // 我的编排列表(owner 隔离)
|
||||
p.POST("/agents", h.AgentSave) // 保存/更新编排
|
||||
p.DELETE("/agents", h.AgentDelete) // 删除编排
|
||||
p.POST("/reports", h.GenerateReport) // 报告生成
|
||||
p.GET("/billing", h.Billing)
|
||||
}
|
||||
|
||||
api.GET("/agents", h.AgentList) // 我的 Agent 编排列表(owner 隔离)
|
||||
api.POST("/agents", h.AgentSave) // 保存/更新编排
|
||||
api.DELETE("/agents", h.AgentDelete) // 删除编排
|
||||
api.POST("/reports", h.GenerateReport) // 报告生成(intent=report 任务 → Dispatcher 专用编排)
|
||||
api.GET("/reports/:id/export", h.ExportReport) // 按需导出(format=docx|md;默认 docx)
|
||||
api.GET("/reports/:id/download", h.ExportReport) // 兼容旧入口(默认 docx)
|
||||
api.GET("/health", h.Health) // 依赖健康聚合(顶栏五盏灯)
|
||||
api.GET("/billing", h.Billing)
|
||||
|
||||
// 运维控制面:LLM 模型配置(独立运维控制台调用)。
|
||||
// 运维控制面:LLM 模型配置(独立运维控制台调用;鉴权待后续接管理员角色)。
|
||||
admin := api.Group("/admin")
|
||||
{
|
||||
admin.GET("/models", h.ListModels)
|
||||
@@ -69,7 +73,7 @@ func cors() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, X-User-ID, X-Session-ID")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Session-ID, X-User-ID")
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user