130 lines
4.9 KiB
Go
130 lines
4.9 KiB
Go
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 ""
|
||
}
|