package http import ( "context" "hyapp/services/gateway-service/internal/transport/http/httpkit" "log/slog" "net/http" "strings" "time" userv1 "hyapp.local/api/proto/user/v1" "hyapp/pkg/appcode" "hyapp/pkg/logx" "hyapp/pkg/xerr" "hyapp/services/gateway-service/internal/auth" ) // apiHandler 包装需要 app 解析和 access token 的业务 API。 func (h *Handler) apiHandler(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler { return httpkit.WithRequestID(withAccessLog(h.withResolvedApp(h.withAuth(jwtVerifier, next)))) } // profileAPIHandler 包装需要 app 解析、access token 和完整资料门禁的业务 API。 func (h *Handler) profileAPIHandler(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler { return httpkit.WithRequestID(withAccessLog(h.withResolvedApp(h.withAuth(jwtVerifier, httpkit.RequireCompletedProfile(next))))) } // publicAPIHandler 包装只需要 app 解析的公开 API;登录/注册会用解析结果创建对应 App 的用户。 func (h *Handler) publicAPIHandler(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler { return httpkit.WithRequestID(withAccessLog(h.withResolvedApp(h.withOptionalAuth(jwtVerifier, next)))) } // withAccessLog 统一记录 gateway 业务 API 的请求体、响应体、状态码和耗时。 // healthcheck 不经过该 middleware,避免探活日志淹没业务调用。 func withAccessLog(next http.Handler) http.Handler { return logx.HTTPMiddleware("gateway-service", httpkit.RequestIDFromContext, next) } // withAuth 在 transport 层完成鉴权失败响应写入。 // auth 包只返回身份校验结果,避免底层鉴权模块绑定 gateway 的 JSON envelope。 func (h *Handler) withAuth(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { claims, err := jwtVerifier.Verify(request.Header.Get("Authorization")) if err != nil { httpkit.WriteError(writer, request, http.StatusUnauthorized, httpkit.CodeUnauthorized, "unauthorized") return } if strings.TrimSpace(claims.SessionID) == "" { httpkit.WriteError(writer, request, http.StatusUnauthorized, string(xerr.SessionRevoked), "unauthorized") return } ctx := auth.WithClaims(request.Context(), claims) revoked, err := h.revokedSession(ctx, claims.AppCode, claims.SessionID) if err != nil { logx.Warn(ctx, "session_denylist_read_failed", slog.String("component", "gateway_auth"), slog.String("session_id", claims.SessionID), slog.String("error", err.Error())) } if revoked { httpkit.WriteError(writer, request.WithContext(ctx), http.StatusUnauthorized, string(xerr.SessionRevoked), "unauthorized") return } next.ServeHTTP(writer, request.WithContext(ctx)) }) } func (h *Handler) withOptionalAuth(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { header := strings.TrimSpace(request.Header.Get("Authorization")) if header == "" || jwtVerifier == nil { next.ServeHTTP(writer, request) return } claims, err := jwtVerifier.Verify(header) if err != nil || strings.TrimSpace(claims.SessionID) == "" { // 公开接口不能因为旧客户端带了过期或残缺 token 就失败;无效 token 只是不参与用户态配置覆盖。 next.ServeHTTP(writer, request) return } ctx := auth.WithClaims(request.Context(), claims) revoked, err := h.revokedSession(ctx, claims.AppCode, claims.SessionID) if err != nil { logx.Warn(ctx, "optional_session_denylist_read_failed", slog.String("component", "gateway_public_auth"), slog.String("session_id", claims.SessionID), slog.String("error", err.Error())) } if revoked { next.ServeHTTP(writer, request) return } next.ServeHTTP(writer, request.WithContext(ctx)) }) } func (h *Handler) revokedSession(ctx context.Context, appCode string, sessionID string) (bool, error) { if h.loginRiskCache == nil { return false, nil } return h.loginRiskCache.RevokedSessionExists(ctx, appCode, sessionID) } // withResolvedApp 在入口处把包名或显式 app_code 解析成内部租户键并写入 context。 // 公开接口依赖请求头解析;鉴权接口稍后会由 token claims 再覆盖一次,防止跨 App 串 token。 func (h *Handler) withResolvedApp(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { appCode, err := h.resolveRequestAppCode(request.Context(), request) if err != nil { httpkit.WriteRPCError(writer, request, err) return } ctx := appcode.WithContext(request.Context(), appCode) next.ServeHTTP(writer, request.WithContext(ctx)) }) } func (h *Handler) resolveRequestAppCode(ctx context.Context, request *http.Request) (string, error) { explicitAppCode := firstHeader(request, "X-App-Code", "X-HY-App-Code", "App-Code") packageName := firstHeader(request, "X-App-Package", "X-Package-Name", "X-App-Bundle-ID", "X-Bundle-ID") platform := firstHeader(request, "X-App-Platform", "X-Platform") if h.appRegistryClient != nil && (explicitAppCode != "" || packageName != "") { resp, err := h.appRegistryClient.ResolveApp(ctx, &userv1.ResolveAppRequest{ Meta: &userv1.RequestMeta{ RequestId: httpkit.RequestIDFromContext(ctx), Caller: "gateway-service", SentAtMs: time.Now().UnixMilli(), AppCode: explicitAppCode, }, PackageName: packageName, Platform: platform, AppCode: explicitAppCode, }) if err != nil { return "", err } if resp.GetApp() != nil { return appcode.Normalize(resp.GetApp().GetAppCode()), nil } } if strings.EqualFold(packageName, "com.org.laluparty") { // 开发阶段先保留用户明确给出的包名映射;生产仍以 user-service apps 表为准。 return "lalu", nil } return appcode.Normalize(explicitAppCode), nil } func firstHeader(request *http.Request, names ...string) string { for _, name := range names { value := strings.TrimSpace(request.Header.Get(name)) if value != "" { return value } } return "" }