fix: 支持同源播放器导航认证 (#54)

This commit is contained in:
QiuSW
2026-08-13 21:30:40 +08:00
parent 703148df58
commit 8bf9a6d002
3 changed files with 77 additions and 1 deletions
+37
View File
@@ -48,6 +48,21 @@ func Require(permission string, next http.Handler) http.Handler {
})
}
// RequireSameOriginFrame authenticates browser iframe navigation that cannot
// attach the X-Product header used by API clients. Fetch Metadata keeps this
// narrow: only a same-origin iframe navigation with a valid Sense session is
// accepted.
func RequireSameOriginFrame(permission string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
module := activeModule.Load()
if module == nil {
platform.WriteError(w, &platform.APIError{Status: http.StatusServiceUnavailable, Code: "identity_not_ready", Message: "身份服务尚未就绪"})
return
}
module.RequireSameOriginFrame(permission, next).ServeHTTP(w, r)
})
}
func PrincipalFromContext(ctx context.Context) (Principal, bool) {
principal, ok := ctx.Value(principalContextKey{}).(Principal)
return principal, ok
@@ -84,6 +99,28 @@ func (m *Module) Require(permission string, next http.Handler) http.Handler {
}))
}
func (m *Module) RequireSameOriginFrame(permission string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Sec-Fetch-Site") != "same-origin" ||
r.Header.Get("Sec-Fetch-Mode") != "navigate" ||
r.Header.Get("Sec-Fetch-Dest") != "iframe" {
platform.WriteError(w, &platform.APIError{Status: http.StatusUnauthorized, Code: "unauthorized", Message: "登录状态无效"})
return
}
cookie, err := r.Cookie(sessionCookieName)
if err != nil {
platform.WriteError(w, &platform.APIError{Status: http.StatusUnauthorized, Code: "unauthorized", Message: "登录状态无效"})
return
}
principal, err := m.service.Authenticate(r.Context(), cookie.Value)
if err != nil || !HasPermission(principal.Role, permission) {
platform.WriteError(w, &platform.APIError{Status: http.StatusUnauthorized, Code: "unauthorized", Message: "登录状态无效"})
return
}
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), principalContextKey{}, principal)))
})
}
func (m *Module) bootstrap(w http.ResponseWriter, r *http.Request) {
var request struct {
Username string `json:"username"`
@@ -21,6 +21,9 @@ func TestHTTPLoginCookieAndProductBoundary(t *testing.T) {
}
app := platform.NewApp(platform.Config{DatabaseMode: platform.DatabaseModeMemory}, nil, slog.New(slog.NewTextHandler(io.Discard, nil)))
NewModule(service, cfg).Register(app)
app.Handle("GET /same-origin-frame", RequireSameOriginFrame(PermissionMediaRead, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
})))
bootstrap := httptest.NewRequest(http.MethodPost, "/api/v1/identity/bootstrap", bytes.NewBufferString(`{"username":"admin","display_name":"管理员","password":"StrongPass2026"}`))
bootstrap.Header.Set("X-Sense-Bootstrap-Token", "bootstrap-test")
@@ -56,4 +59,40 @@ func TestHTTPLoginCookieAndProductBoundary(t *testing.T) {
if senseResult.Code != http.StatusOK {
t.Fatalf("Sense product header status = %d", senseResult.Code)
}
meWithoutProduct := httptest.NewRequest(http.MethodGet, "/api/v1/identity/me", nil)
meWithoutProduct.AddCookie(cookies[0])
missingProductResult := httptest.NewRecorder()
app.Handler().ServeHTTP(missingProductResult, meWithoutProduct)
if missingProductResult.Code != http.StatusUnauthorized {
t.Fatalf("missing product header status = %d", missingProductResult.Code)
}
frame := httptest.NewRequest(http.MethodGet, "/same-origin-frame", nil)
frame.AddCookie(cookies[0])
frame.Header.Set("Sec-Fetch-Site", "same-origin")
frame.Header.Set("Sec-Fetch-Mode", "navigate")
frame.Header.Set("Sec-Fetch-Dest", "iframe")
frameResult := httptest.NewRecorder()
app.Handler().ServeHTTP(frameResult, frame)
if frameResult.Code != http.StatusNoContent {
t.Fatalf("same-origin frame status = %d, body = %s", frameResult.Code, frameResult.Body.String())
}
frame.Header.Set("Sec-Fetch-Site", "cross-site")
crossSiteResult := httptest.NewRecorder()
app.Handler().ServeHTTP(crossSiteResult, frame)
if crossSiteResult.Code != http.StatusUnauthorized {
t.Fatalf("cross-site frame status = %d", crossSiteResult.Code)
}
missingCookie := httptest.NewRequest(http.MethodGet, "/same-origin-frame", nil)
missingCookie.Header.Set("Sec-Fetch-Site", "same-origin")
missingCookie.Header.Set("Sec-Fetch-Mode", "navigate")
missingCookie.Header.Set("Sec-Fetch-Dest", "iframe")
missingCookieResult := httptest.NewRecorder()
app.Handler().ServeHTTP(missingCookieResult, missingCookie)
if missingCookieResult.Code != http.StatusUnauthorized {
t.Fatalf("missing-cookie frame status = %d", missingCookieResult.Code)
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ func (m *Module) Register(app *platform.App) {
app.Handle("GET /api/v1/liveview/routes", identity.Require(identity.PermissionMediaRead, http.HandlerFunc(m.routes)))
app.Handle("POST /api/v1/liveview/sessions", identity.Require(identity.PermissionMediaRead, http.HandlerFunc(m.create)))
app.Handle("GET /api/v1/liveview/sessions/{id}", identity.Require(identity.PermissionMediaRead, http.HandlerFunc(m.get)))
app.Handle("GET /api/v1/liveview/sessions/{id}/player", identity.Require(identity.PermissionMediaRead, http.HandlerFunc(m.player)))
app.Handle("GET /api/v1/liveview/sessions/{id}/player", identity.RequireSameOriginFrame(identity.PermissionMediaRead, http.HandlerFunc(m.player)))
}
func (m *Module) routes(w http.ResponseWriter, r *http.Request) {
items, err := m.service.Routes(r.Context())