package gateway import ( "encoding/json" "errors" "io" "net/http" "strings" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/auth" apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors" ) // defaultRefreshTTL 是 cfg.Auth.RefreshTTL 未设置时(测试中未解码的 // 配置)使用的 refresh token 生命周期,与 AUTH_REFRESH_TTL 默认值一致。 const defaultRefreshTTL = 720 * time.Hour // refreshRequest 是 POST /api/v1/auth/refresh 的请求体。 type refreshRequest struct { RefreshToken string `json:"refreshToken"` } // refreshResponse 是刷新成功的载荷:新的访问令牌加上 // 轮换后的 refresh token(配置禁用轮换时省略)。 type refreshResponse struct { Token string `json:"token"` ExpiresAt int64 `json:"expiresAt"` Role string `json:"role"` RefreshToken string `json:"refreshToken,omitempty"` } // logoutRequest 是 POST /api/v1/auth/logout 的请求体。 type logoutRequest struct { RefreshToken string `json:"refreshToken"` } // refreshTTL 返回已配置的 refresh token 生命周期,配置未解码 // (零值)时回退到 defaultRefreshTTL。 func (s *Server) refreshTTL() time.Duration { if s.cfg != nil && s.cfg.Auth.RefreshTTL > 0 { return s.cfg.Auth.RefreshTTL } return defaultRefreshTTL } // refreshRotate 报告是否启用一次性轮换。轮换是 // 默认;只有显式 AUTH_REFRESH_ROTATE_DISABLED=true 才选择退出。 func (s *Server) refreshRotate() bool { return s.cfg == nil || !s.cfg.Auth.RefreshRotateDisabled } // handleRefresh 用有效的非透明 refresh token 换取新的访问令牌, // 默认轮换 refresh token。它是公开的(refresh token 就是 // 凭据),对未知、已吊销或已过期的 token 返回 401 invalid_grant。 // 成功的轮换会被审计;token 本身绝不记日志。 func (s *Server) handleRefresh(w http.ResponseWriter, r *http.Request) { var req refreshRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid request body")) return } if s.refresh == nil { writeError(w, apperr.Wrap(apperr.ErrInternal, "refresh_unavailable", "refresh token service is not configured")) return } grant, newRefresh, err := s.refresh.Rotate(r.Context(), req.RefreshToken, auth.RotateOptions{ TTL: s.refreshTTL(), Rotate: s.refreshRotate(), }) if err != nil { if errors.Is(err, auth.ErrInvalidGrant) { writeError(w, apperr.Wrap(apperr.ErrUnauthorized, "invalid_grant", "invalid refresh token")) return } s.log.Error("refresh rotate", "err", err) writeError(w, apperr.Wrap(apperr.ErrInternal, "refresh_failed", "could not refresh token")) return } token, err := s.auth.Sign(grant.UserID, grant.Role, tokenTTL) if err != nil { s.log.Error("refresh sign", "err", err) writeError(w, apperr.Wrap(apperr.ErrInternal, "refresh_failed", "could not sign token")) return } s.audit(r.Context(), grant.UserID+":"+string(grant.Role), "auth.refresh", "ok") writeJSON(w, http.StatusOK, refreshResponse{ Token: token, ExpiresAt: time.Now().Add(tokenTTL).Unix(), Role: string(grant.Role), RefreshToken: newRefresh, }) } // handleLogout 吊销请求体中的 refresh token。它公开且 // 幂等:未知或已吊销的 token 返回 {"revoked": false} 与 // 200 而不是错误,因此客户端总能清理其本地状态。 // // 请求体可以携带 refreshToken(只吊销那一份授权),也可以不携带, // 此时 Authorization Bearer 访问令牌标识账号,该账号的 // 每一份 refresh 授权都被吊销(全设备登出)。两者都 // 没有的请求是 400。 func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { var req logoutRequest // 空请求体是合法的:它意味着「无 refreshToken」,账号由 // 下面的 bearer 访问令牌标识。 if err := json.NewDecoder(r.Body).Decode(&req); err != nil && !errors.Is(err, io.EOF) { writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid request body")) return } if s.refresh == nil { writeError(w, apperr.Wrap(apperr.ErrInternal, "refresh_unavailable", "refresh token service is not configured")) return } if strings.TrimSpace(req.RefreshToken) != "" { revoked, err := s.refresh.Revoke(r.Context(), req.RefreshToken) if err != nil { s.log.Error("logout revoke refresh token", "err", err) writeError(w, apperr.Wrap(apperr.ErrInternal, "logout_failed", "could not revoke refresh token")) return } writeJSON(w, http.StatusOK, map[string]any{"revoked": revoked}) return } access := bearerFromRequest(r) if access == "" { writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "refreshToken or Authorization bearer token is required")) return } claims, err := s.auth.Verify(access) if err != nil { writeError(w, apperr.Wrap(apperr.ErrUnauthorized, "invalid_grant", "invalid access token")) return } n, err := s.refresh.RevokeByUser(r.Context(), claims.Sub) if err != nil { s.log.Error("logout revoke user refresh tokens", "err", err) writeError(w, apperr.Wrap(apperr.ErrInternal, "logout_failed", "could not revoke refresh tokens")) return } writeJSON(w, http.StatusOK, map[string]any{"revoked": n > 0, "revokedCount": n}) } // bearerFromRequest 从 Authorization 头提取 Bearer 凭据, // 头缺失或畸形时返回 ""。 func bearerFromRequest(r *http.Request) string { const prefix = "Bearer " h := r.Header.Get("Authorization") if len(h) <= len(prefix) || !strings.EqualFold(h[:len(prefix)], prefix) { return "" } return strings.TrimSpace(h[len(prefix):]) }