package http import ( "net" "net/http" "net/netip" "strings" "time" userv1 "hyapp.local/api/proto/user/v1" "hyapp/pkg/appcode" "hyapp/services/gateway-service/internal/auth" ) // authTokenData 是 gateway 对外 auth API 的扁平响应格式。 type authTokenData struct { AppCode string `json:"app_code"` UserID string `json:"user_id,omitempty"` DisplayUserID string `json:"display_user_id,omitempty"` DefaultDisplayUserID string `json:"default_display_user_id,omitempty"` DisplayUserIDKind string `json:"display_user_id_kind,omitempty"` DisplayUserIDExpiresAtMs int64 `json:"display_user_id_expires_at_ms"` SessionID string `json:"session_id"` AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token,omitempty"` ExpiresInSec int64 `json:"expires_in_sec"` TokenType string `json:"token_type"` IsNewUser *bool `json:"is_new_user,omitempty"` ProfileCompleted bool `json:"profile_completed"` OnboardingStatus string `json:"onboarding_status"` } // loginPassword 处理账号密码登录;账号就是当前有效短号。 func (h *Handler) loginPassword(writer http.ResponseWriter, request *http.Request) { var body struct { Account string `json:"account"` Password string `json:"password"` DeviceID string `json:"device_id"` Platform string `json:"platform"` Language string `json:"language"` Timezone string `json:"timezone"` } if !decode(writer, request, &body) { return } account := strings.TrimSpace(body.Account) if !h.allowPublicAuthRequest(writer, request, authRateInput{ operation: authOperationPassword, deviceID: body.DeviceID, displayUserID: account, }) { return } if !h.allowLoginRisk(writer, request, loginRiskInput{ Channel: "account", Platform: body.Platform, Language: body.Language, Timezone: body.Timezone, }) { return } if h.userClient == nil { writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error") return } resp, err := h.userClient.LoginPassword(request.Context(), &userv1.LoginPasswordRequest{ Meta: authRequestMetaWithLoginContext(request, body.DeviceID, body.Platform, body.Language, body.Timezone), DisplayUserId: account, Password: body.Password, }) if err != nil { writeRPCError(writer, request, err) return } writeOK(writer, request, authData(resp.GetToken(), nil)) } // loginThirdParty 处理三方登录即注册。 func (h *Handler) loginThirdParty(writer http.ResponseWriter, request *http.Request) { var body struct { Provider string `json:"provider"` Credential string `json:"credential"` Username string `json:"username"` Gender string `json:"gender"` Country string `json:"country"` InviteCode string `json:"invite_code"` DeviceID string `json:"device_id"` Device string `json:"device"` OSVersion string `json:"os_version"` Avatar string `json:"avatar"` Birth string `json:"birth"` AppVersion string `json:"app_version"` BuildNumber string `json:"build_number"` Source string `json:"source"` InstallChannel string `json:"install_channel"` Campaign string `json:"campaign"` Platform string `json:"platform"` Language string `json:"language"` Timezone string `json:"timezone"` } if !decode(writer, request, &body) { return } if !h.allowPublicAuthRequest(writer, request, authRateInput{ operation: authOperationThirdParty, deviceID: body.DeviceID, provider: body.Provider, }) { return } if !h.allowLoginRisk(writer, request, loginRiskInput{ Channel: body.Provider, Platform: body.Platform, Language: body.Language, Timezone: body.Timezone, }) { return } if h.userClient == nil { writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error") return } resp, err := h.userClient.LoginThirdParty(request.Context(), &userv1.LoginThirdPartyRequest{ Meta: authRequestMetaWithLoginContext(request, body.DeviceID, body.Platform, body.Language, body.Timezone), Provider: body.Provider, Credential: body.Credential, Username: body.Username, Gender: body.Gender, Country: body.Country, InviteCode: body.InviteCode, DeviceId: body.DeviceID, Device: body.Device, OsVersion: body.OSVersion, Avatar: body.Avatar, Birth: body.Birth, AppVersion: body.AppVersion, BuildNumber: body.BuildNumber, Source: body.Source, InstallChannel: body.InstallChannel, Campaign: body.Campaign, Platform: body.Platform, Language: body.Language, Timezone: body.Timezone, }) if err != nil { writeRPCError(writer, request, err) return } isNewUser := resp.GetIsNewUser() writeOK(writer, request, authData(resp.GetToken(), &isNewUser)) } // setPassword 处理已登录用户首次设置密码。 func (h *Handler) setPassword(writer http.ResponseWriter, request *http.Request) { var body struct { Password string `json:"password"` } if !decode(writer, request, &body) { return } if h.userClient == nil { writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error") return } resp, err := h.userClient.SetPassword(request.Context(), &userv1.SetPasswordRequest{ Meta: authRequestMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), Password: body.Password, }) if err != nil { writeRPCError(writer, request, err) return } writeOK(writer, request, map[string]bool{"password_set": resp.GetPasswordSet()}) } // refreshToken 处理 refresh token 轮换。 func (h *Handler) refreshToken(writer http.ResponseWriter, request *http.Request) { var body struct { RefreshToken string `json:"refresh_token"` DeviceID string `json:"device_id"` } if !decode(writer, request, &body) { return } if !h.allowPublicAuthRequest(writer, request, authRateInput{ operation: authOperationRefresh, deviceID: body.DeviceID, }) { return } if h.userClient == nil { writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error") return } resp, err := h.userClient.RefreshToken(request.Context(), &userv1.RefreshTokenRequest{ Meta: authRequestMeta(request, body.DeviceID), RefreshToken: body.RefreshToken, }) if err != nil { writeRPCError(writer, request, err) return } writeOK(writer, request, authData(resp.GetToken(), nil)) } // logout 处理 refresh session 失效。 func (h *Handler) logout(writer http.ResponseWriter, request *http.Request) { var body struct { RefreshToken string `json:"refresh_token"` SessionID string `json:"session_id"` } if !decode(writer, request, &body) { return } if !h.allowPublicAuthRequest(writer, request, authRateInput{ operation: authOperationLogout, sessionID: body.SessionID, }) { return } if h.userClient == nil { writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error") return } resp, err := h.userClient.Logout(request.Context(), &userv1.LogoutRequest{ Meta: authRequestMeta(request, ""), SessionId: body.SessionID, RefreshToken: body.RefreshToken, }) if err != nil { writeRPCError(writer, request, err) return } writeOK(writer, request, map[string]bool{"revoked": resp.GetRevoked()}) } // authRequestMeta 生成 user-service auth RPC 的统一追踪和审计元信息。 func authRequestMeta(request *http.Request, deviceID string) *userv1.RequestMeta { return authRequestMetaWithLoginContext(request, deviceID, "", "", "") } func authRequestMetaWithLoginContext(request *http.Request, deviceID string, platform string, language string, timezone string) *userv1.RequestMeta { return &userv1.RequestMeta{ RequestId: requestIDFromContext(request.Context()), Caller: "gateway-service", GatewayNodeId: "gateway-local", SentAtMs: time.Now().UnixMilli(), DeviceId: deviceID, ClientIp: clientIP(request), UserAgent: request.UserAgent(), CountryByIp: countryByIP(request), SessionId: auth.SessionIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()), Platform: strings.ToLower(strings.TrimSpace(platform)), Language: strings.TrimSpace(language), Timezone: strings.TrimSpace(timezone), } } // authData 把 user-service 的 token response 转成外部 HTTP 扁平 data。 func authData(token *userv1.AuthToken, isNewUser *bool) authTokenData { if token == nil { return authTokenData{IsNewUser: isNewUser} } return authTokenData{ UserID: userIDString(token.GetUserId()), AppCode: token.GetAppCode(), DisplayUserID: token.GetDisplayUserId(), DefaultDisplayUserID: token.GetDefaultDisplayUserId(), DisplayUserIDKind: token.GetDisplayUserIdKind(), DisplayUserIDExpiresAtMs: token.GetDisplayUserIdExpiresAtMs(), SessionID: token.GetSessionId(), AccessToken: token.GetAccessToken(), RefreshToken: token.GetRefreshToken(), ExpiresInSec: token.GetExpiresInSec(), TokenType: token.GetTokenType(), IsNewUser: isNewUser, ProfileCompleted: token.GetProfileCompleted(), OnboardingStatus: token.GetOnboardingStatus(), } } // clientIP 提取 gateway 看到的客户端地址。 func clientIP(request *http.Request) string { for _, header := range []string{"X-Trusted-Client-IP", "X-Real-IP"} { if ip := normalizeIP(request.Header.Get(header)); ip != "" { return ip } } forwarded := strings.TrimSpace(request.Header.Get("X-Forwarded-For")) if forwarded != "" { parts := strings.Split(forwarded, ",") for _, part := range parts { ip := normalizeIP(part) if ip == "" { continue } if addr, err := netip.ParseAddr(ip); err == nil && isPublicClientAddr(addr) { return ip } } } host, _, err := net.SplitHostPort(request.RemoteAddr) if err != nil { return normalizeIP(request.RemoteAddr) } return host } func normalizeIP(value string) string { value = strings.TrimSpace(value) if value == "" { return "" } if host, _, err := net.SplitHostPort(value); err == nil { value = host } if addr, err := netip.ParseAddr(value); err == nil { return addr.String() } return value } func isPublicClientAddr(addr netip.Addr) bool { return addr.IsValid() && addr.IsGlobalUnicast() && !addr.IsPrivate() && !addr.IsLoopback() && !addr.IsLinkLocalUnicast() && !addr.IsLinkLocalMulticast() && !addr.IsMulticast() && !addr.IsUnspecified() } func countryByIP(request *http.Request) string { // country_by_ip 不接受 JSON body;只采信 gateway/边缘层写入的地域头。 for _, header := range []string{"CF-IPCountry", "CloudFront-Viewer-Country", "X-Vercel-IP-Country", "X-AppEngine-Country", "X-Country-Code"} { country := strings.ToUpper(strings.TrimSpace(request.Header.Get(header))) if country == "" || country == "XX" || country == "UNKNOWN" { continue } if len(country) >= 2 && len(country) <= 3 { return country } } return "" }