package server import ( "context" "net/http" "strings" "agent-system-backend/internal/store" ) // ownerCtxKey 携带租户身份(认证中间件注入;未启用认证 = store.OwnerDefault)。 type ownerCtxKey struct{} // Authenticator 是 API key 认证(阶段 1:user 即租户)。 // Enabled=false(嵌入式档默认,回环本地单用户)时全部请求回落默认租户。 // SSE 的 EventSource 无法带 Authorization 头,允许 ?key= 查询参数兜底。 type Authenticator struct { st *store.Store Enabled bool } func NewAuthenticator(st *store.Store, enabled bool) *Authenticator { return &Authenticator{st: st, Enabled: enabled} } // SessionCookie 会话 cookie 名(HttpOnly;SSE 的 EventSource 同源自动携带)。 const SessionCookie = "as_session" func (a *Authenticator) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if a == nil || !a.Enabled { next.ServeHTTP(w, r.WithContext(withOwner(r, store.OwnerDefault))) return } // 豁免:静态前端(登录页须可加载)与登录入口本身。 p := r.URL.Path if !strings.HasPrefix(p, "/api/") || p == "/api/auth/login" { next.ServeHTTP(w, r) return } // 凭证二选一:Authorization: Bearer (程序化;?key= 兼容 SSE 老客户端) // 或会话 cookie(浏览器登录;2026-09-30 R2 管理员发放制)。 key := bearerKey(r) if key == "" { key = r.URL.Query().Get("key") } if key != "" { owner, ok := a.st.AuthOwner(key) if !ok { http.Error(w, "unauthorized: invalid or revoked API key", http.StatusUnauthorized) return } next.ServeHTTP(w, r.WithContext(withOwner(r, owner))) return } if c, err := r.Cookie(SessionCookie); err == nil { if owner, ok := a.st.SessionOwner(c.Value); ok { next.ServeHTTP(w, r.WithContext(withOwner(r, owner))) return } } http.Error(w, "unauthorized: login required", http.StatusUnauthorized) }) } // sessionToken 从请求取会话明文(无会话返回空)。 func sessionToken(r *http.Request) string { if c, err := r.Cookie(SessionCookie); err == nil { return c.Value } return "" } func withOwner(r *http.Request, owner string) context.Context { return context.WithValue(r.Context(), ownerCtxKey{}, owner) } func bearerKey(r *http.Request) string { h := r.Header.Get("Authorization") if strings.HasPrefix(h, "Bearer ") { return strings.TrimSpace(strings.TrimPrefix(h, "Bearer ")) } return "" } // owner 从请求上下文取租户身份(未注入回落默认)。 func (s *Server) owner(r *http.Request) string { if v, ok := r.Context().Value(ownerCtxKey{}).(string); ok && v != "" { return v } return store.OwnerDefault }