2026-05-12 09:53:20 +08:00

130 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(next http.HandlerFunc) http.Handler {
return httpkit.WithRequestID(withAccessLog(h.withResolvedApp(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) 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 ""
}