feat: 实现 OIDC 服务端会话与请求刷新

使用 AES-256-GCM 保存认证中心令牌,并以随机 Cookie 哈希关联 PostgreSQL 会话。加入闲置与绝对期限、Refresh Token 轮换以及多实例刷新锁。
This commit is contained in:
2026-07-13 19:09:10 +08:00
parent a81a7b5200
commit d345c070ae
16 changed files with 1804 additions and 58 deletions
+31 -9
View File
@@ -58,6 +58,18 @@ const userContextKey contextKey = "easyai-auth-user"
var ErrUnauthorized = errors.New("unauthorized")
type RequestAuthError struct {
Status int
Code string
Message string
}
func (e *RequestAuthError) Error() string { return e.Code }
func NewRequestAuthError(status int, code, message string) error {
return &RequestAuthError{Status: status, Code: code, Message: message}
}
type Authenticator struct {
JWTSecret string
ServerMainBaseURL string
@@ -67,6 +79,7 @@ type Authenticator struct {
HTTPClient *http.Client
LocalAPIKeyVerifier func(ctx context.Context, apiKey string) (*User, error)
OIDCVerifier *OIDCVerifier
OIDCSessionResolver func(ctx context.Context, sessionID string) (*User, error)
LegacyJWTEnabled bool
}
@@ -95,13 +108,23 @@ func (a *Authenticator) Require(permission Permission, next http.Handler) http.H
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, err := a.Authenticate(r)
if err != nil {
if strings.HasPrefix(err.Error(), ErrUnauthorized.Error()+":") {
slog.WarnContext(r.Context(), "OIDC authentication rejected", "reason", err.Error())
}
if permission == PermissionPublic {
next.ServeHTTP(w, r)
return
}
var requestError *RequestAuthError
if errors.As(err, &requestError) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(requestError.Status)
_ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{
"message": requestError.Message, "status": requestError.Status, "code": requestError.Code,
}})
return
}
if strings.HasPrefix(err.Error(), ErrUnauthorized.Error()+":") {
slog.WarnContext(r.Context(), "OIDC authentication rejected", "reason", err.Error())
}
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
@@ -115,7 +138,6 @@ func (a *Authenticator) Require(permission Permission, next http.Handler) http.H
func (a *Authenticator) Authenticate(r *http.Request) (*User, error) {
token := extractBearer(r.Header.Get("Authorization"))
fromOIDCSessionCookie := false
if token == "" {
token = strings.TrimSpace(r.Header.Get("x-comfy-api-key"))
}
@@ -127,16 +149,16 @@ func (a *Authenticator) Authenticate(r *http.Request) (*User, error) {
}
if token == "" {
if cookie, err := r.Cookie(OIDCSessionCookieName); err == nil {
token = strings.TrimSpace(cookie.Value)
fromOIDCSessionCookie = token != ""
sessionID := strings.TrimSpace(cookie.Value)
if sessionID == "" || a.OIDCSessionResolver == nil {
return nil, ErrUnauthorized
}
return a.OIDCSessionResolver(r.Context(), sessionID)
}
}
if token == "" {
return nil, ErrUnauthorized
}
if fromOIDCSessionCookie && jwtAlgorithm(token) != "RS256" && jwtAlgorithm(token) != "ES256" {
return nil, ErrUnauthorized
}
if strings.HasPrefix(token, "sk-") {
return a.verifyAPIKey(r.Context(), token)
}