2026-09-23 09:40:59 +08:00
|
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"net/http"
|
2026-09-24 20:50:17 +08:00
|
|
|
|
"strings"
|
2026-09-23 09:40:59 +08:00
|
|
|
|
|
|
|
|
|
|
"github.com/fonu/fonu/internal/auth"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type contextKey string
|
|
|
|
|
|
|
|
|
|
|
|
const sessionContextKey contextKey = "session_id"
|
|
|
|
|
|
|
|
|
|
|
|
func SessionMiddleware(authSvc *auth.Service) func(http.Handler) http.Handler {
|
|
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
cookie, err := r.Cookie("fonu_session")
|
|
|
|
|
|
if err == nil && cookie.Value != "" {
|
|
|
|
|
|
ctx := context.WithValue(r.Context(), sessionContextKey, cookie.Value)
|
|
|
|
|
|
r = r.WithContext(ctx)
|
|
|
|
|
|
}
|
|
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-24 20:50:17 +08:00
|
|
|
|
// RequireAuth 接受两种凭据:浏览器的会话 cookie,以及 Authorization: Bearer <api token>。
|
|
|
|
|
|
// 令牌分读写两种范围:read 只允许 GET/HEAD,write 与会话同权。
|
2026-09-23 09:40:59 +08:00
|
|
|
|
func RequireAuth(authSvc *auth.Service) func(http.Handler) http.Handler {
|
|
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2026-09-24 20:50:17 +08:00
|
|
|
|
if token := bearerToken(r); token != "" {
|
|
|
|
|
|
scope, err := authSvc.ValidateAPIToken(r.Context(), token)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
writeError(r, w, http.StatusUnauthorized, "API Token 无效或已过期")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// 账号与令牌管理只允许浏览器会话:否则一个 write 令牌可以自己给自己提权
|
|
|
|
|
|
if tokenDeniedPath(r.URL.Path) {
|
|
|
|
|
|
writeError(r, w, http.StatusForbidden, "该操作需要使用登录会话")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if scope != auth.TokenScopeWrite && r.Method != http.MethodGet && r.Method != http.MethodHead {
|
|
|
|
|
|
writeError(r, w, http.StatusForbidden, "该 Token 只读,不能执行写操作")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-23 09:40:59 +08:00
|
|
|
|
sessionID, _ := r.Context().Value(sessionContextKey).(string)
|
|
|
|
|
|
if err := authSvc.ValidateSession(r.Context(), sessionID); err != nil {
|
|
|
|
|
|
writeError(r, w, http.StatusUnauthorized, "未登录或会话已过期")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-24 20:50:17 +08:00
|
|
|
|
// bearerToken 取出 Authorization: Bearer 里的明文令牌;没有或格式不对返回空串。
|
|
|
|
|
|
func bearerToken(r *http.Request) string {
|
|
|
|
|
|
header := strings.TrimSpace(r.Header.Get("Authorization"))
|
|
|
|
|
|
const prefix = "Bearer "
|
|
|
|
|
|
if len(header) <= len(prefix) || !strings.EqualFold(header[:len(prefix)], prefix) {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
return strings.TrimSpace(header[len(prefix):])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// tokenDeniedPath 是只允许浏览器会话的路径(账号与令牌管理)。
|
|
|
|
|
|
func tokenDeniedPath(path string) bool {
|
|
|
|
|
|
if path == "/api/auth/password" || path == "/api/auth/logout" {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
2026-09-27 18:43:34 +08:00
|
|
|
|
return strings.HasPrefix(path, "/api/settings/tokens") ||
|
|
|
|
|
|
strings.HasPrefix(path, "/api/system/update")
|
2026-09-24 20:50:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-23 09:40:59 +08:00
|
|
|
|
func sessionIDFromContext(ctx context.Context) string {
|
|
|
|
|
|
v, _ := ctx.Value(sessionContextKey).(string)
|
|
|
|
|
|
return v
|
|
|
|
|
|
}
|