2026-05-09 21:47:33 +08:00

154 lines
5.7 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"
"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 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 withRequestID(withAccessLog(h.withResolvedApp(h.withAuth(jwtVerifier, requireCompletedProfile(next)))))
}
// publicAPIHandler 包装只需要 app 解析的公开 API登录/注册会用解析结果创建对应 App 的用户。
func (h *Handler) publicAPIHandler(next http.HandlerFunc) http.Handler {
return withRequestID(withAccessLog(h.withResolvedApp(next)))
}
// withAccessLog 统一记录 gateway 业务 API 的请求体、响应体、状态码和耗时。
// healthcheck 不经过该 middleware避免探活日志淹没业务调用。
func withAccessLog(next http.Handler) http.Handler {
return logx.HTTPMiddleware("gateway-service", 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 {
writeError(writer, request, http.StatusUnauthorized, codeUnauthorized, "unauthorized")
return
}
if strings.TrimSpace(claims.SessionID) == "" {
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 {
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 {
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: 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 ""
}
// requireMethod 在业务 handler 之前固定 HTTP method避免 path-only mux 让错误 method 进入命令链路。
func requireMethod(method string, next http.HandlerFunc) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
if request.Method != method {
writeError(writer, request, http.StatusMethodNotAllowed, codeInvalidArgument, "invalid argument")
return
}
next(writer, request)
}
}
// requireCompletedProfile 在 gateway 层执行外部准入门禁。
// 这里故意不访问 user-service依赖 access token 快照,避免所有高频房间入口同步打用户服务。
func requireCompletedProfile(next http.HandlerFunc) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
if !auth.ProfileCompletedFromContext(request.Context()) {
writeError(writer, request, http.StatusForbidden, codeProfileRequired, "profile required")
return
}
next.ServeHTTP(writer, request)
}
}