fix(game): scope yomi launch session lookup
This commit is contained in:
parent
709fd00ab2
commit
59a609a45d
@ -35,6 +35,7 @@ type Repository interface {
|
||||
GetPlatform(ctx context.Context, appCode string, platformCode string) (gamedomain.Platform, error)
|
||||
CreateLaunchSession(ctx context.Context, session gamedomain.LaunchSession) error
|
||||
GetLaunchSessionByToken(ctx context.Context, appCode string, token string) (gamedomain.LaunchSession, error)
|
||||
GetLaunchSessionByTokenScope(ctx context.Context, appCode string, token string, platformCode string, providerGameID string) (gamedomain.LaunchSession, error)
|
||||
InsertCallbackLog(ctx context.Context, log gamedomain.CallbackLog) error
|
||||
CreateGameOrder(ctx context.Context, order gamedomain.GameOrder) (gamedomain.GameOrder, bool, error)
|
||||
MarkOrderSucceeded(ctx context.Context, appCode string, orderID string, walletTransactionID string, balanceAfter int64, nowMs int64) error
|
||||
|
||||
@ -876,6 +876,79 @@ func TestHandleYomiGetTokenAndUserInfo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleYomiGetTokenScopesSharedAccessTokenByPlatformAndGame(t *testing.T) {
|
||||
secret := "12345678901234567890123456789012"
|
||||
token := "shared_app_access_token"
|
||||
repo := &fakeRepository{
|
||||
platform: gamedomain.Platform{
|
||||
PlatformCode: "yomi",
|
||||
AdapterType: gamedomain.AdapterYomiV4,
|
||||
CallbackSecretCiphertext: secret,
|
||||
AdapterConfigJSON: `{"uid_mode":"display_user_id"}`,
|
||||
},
|
||||
sessions: []gamedomain.LaunchSession{
|
||||
{
|
||||
AppCode: "lalu",
|
||||
SessionID: "game_sess_yomi_463",
|
||||
UserID: 42,
|
||||
DisplayUserID: "420001",
|
||||
RoomID: "room_463",
|
||||
PlatformCode: "yomi",
|
||||
GameID: "yomi_463",
|
||||
ProviderGameID: "463",
|
||||
LaunchTokenHash: stableHash(token),
|
||||
Status: gamedomain.SessionActive,
|
||||
ExpiresAtMS: 1700086400000,
|
||||
CreatedAtMS: 1700000000000,
|
||||
},
|
||||
{
|
||||
AppCode: "lalu",
|
||||
SessionID: "game_sess_yomi_464",
|
||||
UserID: 42,
|
||||
DisplayUserID: "420001",
|
||||
RoomID: "room_464",
|
||||
PlatformCode: "yomi",
|
||||
GameID: "yomi_464",
|
||||
ProviderGameID: "464",
|
||||
LaunchTokenHash: stableHash(token),
|
||||
Status: gamedomain.SessionActive,
|
||||
ExpiresAtMS: 1700086400000,
|
||||
CreatedAtMS: 1700000001000,
|
||||
},
|
||||
{
|
||||
AppCode: "lalu",
|
||||
SessionID: "game_sess_reyou_1",
|
||||
UserID: 42,
|
||||
DisplayUserID: "420001",
|
||||
RoomID: "room_reyou",
|
||||
PlatformCode: "reyou",
|
||||
GameID: "reyou_1",
|
||||
ProviderGameID: "1",
|
||||
LaunchTokenHash: stableHash(token),
|
||||
Status: gamedomain.SessionActive,
|
||||
ExpiresAtMS: 1700086400000,
|
||||
CreatedAtMS: 1700000002000,
|
||||
},
|
||||
},
|
||||
}
|
||||
svc := New(Config{}, repo, &fakeWallet{balanceAfter: 1880}, &fakeUser{})
|
||||
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
||||
|
||||
raw, _, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
||||
Meta: &gamev1.RequestMeta{RequestId: "req-yomi-scoped-gettoken", AppCode: "lalu"},
|
||||
PlatformCode: "yomi",
|
||||
Operation: "gettoken",
|
||||
RawBody: encryptYomiTestBody(t, secret, `{"gameUid":463,"lang":"en","platAuthCode":"shared_app_access_token","platUserId":"420001","platPayload":"","platRoomId":"room_463"}`),
|
||||
RemoteAddr: "3.1.174.194:443",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("HandleCallback failed: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(raw), `"code":200`) || !strings.Contains(string(raw), `"platToken":"shared_app_access_token"`) {
|
||||
t.Fatalf("yomi scoped gettoken response mismatch: %s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleYomiChangeBalanceDecryptsAndAppliesWallet(t *testing.T) {
|
||||
secret := "12345678901234567890123456789012"
|
||||
token := "gt_test_token"
|
||||
@ -1581,6 +1654,7 @@ type fakeRepository struct {
|
||||
launchable gamedomain.LaunchableGame
|
||||
platform gamedomain.Platform
|
||||
session gamedomain.LaunchSession
|
||||
sessions []gamedomain.LaunchSession
|
||||
order gamedomain.GameOrder
|
||||
orders []gamedomain.GameOrder
|
||||
repair gamedomain.RepairOrder
|
||||
@ -1613,14 +1687,39 @@ func (f *fakeRepository) GetPlatform(context.Context, string, string) (gamedomai
|
||||
}
|
||||
func (f *fakeRepository) CreateLaunchSession(_ context.Context, session gamedomain.LaunchSession) error {
|
||||
f.session = session
|
||||
f.sessions = append(f.sessions, session)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeRepository) GetLaunchSessionByToken(_ context.Context, _ string, token string) (gamedomain.LaunchSession, error) {
|
||||
for index := len(f.sessions) - 1; index >= 0; index-- {
|
||||
if f.sessions[index].LaunchTokenHash == stableHash(token) {
|
||||
return f.sessions[index], nil
|
||||
}
|
||||
}
|
||||
if f.session.LaunchTokenHash == stableHash(token) {
|
||||
return f.session, nil
|
||||
}
|
||||
return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "session token invalid")
|
||||
}
|
||||
func (f *fakeRepository) GetLaunchSessionByTokenScope(_ context.Context, _ string, token string, platformCode string, providerGameID string) (gamedomain.LaunchSession, error) {
|
||||
tokenHash := stableHash(token)
|
||||
for index := len(f.sessions) - 1; index >= 0; index-- {
|
||||
session := f.sessions[index]
|
||||
if session.LaunchTokenHash != tokenHash || session.PlatformCode != strings.TrimSpace(platformCode) {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(providerGameID) != "" && session.ProviderGameID != strings.TrimSpace(providerGameID) {
|
||||
continue
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
if f.session.LaunchTokenHash == tokenHash && f.session.PlatformCode == strings.TrimSpace(platformCode) {
|
||||
if strings.TrimSpace(providerGameID) == "" || f.session.ProviderGameID == strings.TrimSpace(providerGameID) {
|
||||
return f.session, nil
|
||||
}
|
||||
}
|
||||
return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "session token invalid")
|
||||
}
|
||||
func (f *fakeRepository) InsertCallbackLog(context.Context, gamedomain.CallbackLog) error { return nil }
|
||||
func (f *fakeRepository) CreateGameOrder(_ context.Context, order gamedomain.GameOrder) (gamedomain.GameOrder, bool, error) {
|
||||
for _, existing := range f.orders {
|
||||
|
||||
@ -153,7 +153,7 @@ func (s *Service) handleYomiGetToken(ctx context.Context, app string, platform g
|
||||
return yomiAdapterError(yomiCodeSystemError, "invalid payload", true, "")
|
||||
}
|
||||
config := yomiConfigFromPlatform(platform)
|
||||
session, err := s.validYomiSession(ctx, app, body.PlatAuthCode)
|
||||
session, err := s.validScopedYomiSession(ctx, app, platform.PlatformCode, body.PlatAuthCode, body.GameUID.String())
|
||||
// 同时校验 token、用户和游戏,避免 A 游戏拿到的 token 被挪到 B 游戏或别的用户。
|
||||
if err != nil || !yomiSessionMatches(session, config, body.PlatUserID, body.GameUID.String()) {
|
||||
return yomiAdapterError(yomiCodeTokenInvalid, "token invalid", true, "")
|
||||
@ -188,7 +188,7 @@ func (s *Service) handleYomiUserInfo(ctx context.Context, app string, platform g
|
||||
return yomiAdapterError(yomiCodeSystemError, "invalid payload", true, "")
|
||||
}
|
||||
config := yomiConfigFromPlatform(platform)
|
||||
session, err := s.validYomiSession(ctx, app, body.PlatToken)
|
||||
session, err := s.validScopedYomiSession(ctx, app, platform.PlatformCode, body.PlatToken, "")
|
||||
// userinfo 没有 gameUid 时只校验用户;change_balance 会额外校验游戏 ID。
|
||||
if err != nil || !yomiSessionMatches(session, config, body.PlatUserID, "") {
|
||||
return yomiAdapterError(yomiCodeTokenInvalid, "token invalid", true, "")
|
||||
@ -229,7 +229,7 @@ func (s *Service) handleYomiChangeBalance(ctx context.Context, app string, platf
|
||||
}
|
||||
providerOrderID := strings.TrimSpace(body.RequestID.String())
|
||||
config := yomiConfigFromPlatform(platform)
|
||||
session, err := s.validYomiSession(ctx, app, body.PlatToken)
|
||||
session, err := s.validScopedYomiSession(ctx, app, platform.PlatformCode, body.PlatToken, body.GameUID.String())
|
||||
// 改账必须绑定 token、uid、gameUid 三个维度,防止跨用户、跨游戏复用合法 token。
|
||||
if err != nil || !yomiSessionMatches(session, config, body.PlatUserID, body.GameUID.String()) {
|
||||
return yomiAdapterError(yomiCodeTokenInvalid, "token invalid", true, providerOrderID)
|
||||
@ -432,6 +432,21 @@ func (s *Service) validYomiSession(ctx context.Context, app string, token string
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (s *Service) validScopedYomiSession(ctx context.Context, app string, platformCode string, token string, providerGameID string) (gamedomain.LaunchSession, error) {
|
||||
// 同一个 App token 可能同时启动多个厂商游戏,所以 Yomi 回调必须带平台和可选 gameUid 查询,不能只按 token 命中任意旧 session。
|
||||
if strings.TrimSpace(token) == "" {
|
||||
return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "token invalid")
|
||||
}
|
||||
session, err := s.repository.GetLaunchSessionByTokenScope(ctx, app, token, platformCode, providerGameID)
|
||||
if err != nil {
|
||||
return gamedomain.LaunchSession{}, err
|
||||
}
|
||||
if session.Status != gamedomain.SessionActive || session.ExpiresAtMS <= s.now().UnixMilli() {
|
||||
return gamedomain.LaunchSession{}, xerr.New(xerr.SessionExpired, "token expired")
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func yomiSessionMatches(session gamedomain.LaunchSession, config yomiAdapterConfig, platUserID string, gameUID string) bool {
|
||||
// gameUID 允许为空是为了兼容 userinfo;只要厂商传了就必须和启动会话里的 provider_game_id 一致。
|
||||
if strings.TrimSpace(gameUID) != "" && strings.TrimSpace(gameUID) != strings.TrimSpace(session.ProviderGameID) {
|
||||
|
||||
@ -544,6 +544,31 @@ func (r *Repository) GetLaunchSessionByToken(ctx context.Context, appCode string
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetLaunchSessionByTokenScope(ctx context.Context, appCode string, token string, platformCode string, providerGameID string) (gamedomain.LaunchSession, error) {
|
||||
// 同一个 App access token 会被多个厂商和多个游戏复用,回调校验必须把 token、平台和可选游戏 ID 一起收窄。
|
||||
// 这里按 created_at_ms 倒序取最新一条,避免同用户重复打开同一游戏时拿到旧房间或旧 session。
|
||||
row := r.db.QueryRowContext(ctx,
|
||||
`SELECT app_code, session_id, user_id, display_user_id, room_id, region_id, scene, platform_code, game_id,
|
||||
provider_game_id, launch_token_hash, status, expires_at_ms, created_at_ms, updated_at_ms
|
||||
FROM game_launch_sessions
|
||||
WHERE app_code = ?
|
||||
AND launch_token_hash = ?
|
||||
AND platform_code = ?
|
||||
AND (? = '' OR provider_game_id = ?)
|
||||
ORDER BY created_at_ms DESC
|
||||
LIMIT 1`,
|
||||
appcode.Normalize(appCode), stableHash(strings.TrimSpace(token)), strings.TrimSpace(platformCode), strings.TrimSpace(providerGameID), strings.TrimSpace(providerGameID),
|
||||
)
|
||||
var item gamedomain.LaunchSession
|
||||
if err := row.Scan(&item.AppCode, &item.SessionID, &item.UserID, &item.DisplayUserID, &item.RoomID, &item.RegionID, &item.Scene, &item.PlatformCode, &item.GameID, &item.ProviderGameID, &item.LaunchTokenHash, &item.Status, &item.ExpiresAtMS, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "game session token invalid")
|
||||
}
|
||||
return gamedomain.LaunchSession{}, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (r *Repository) InsertCallbackLog(ctx context.Context, log gamedomain.CallbackLog) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`INSERT INTO game_callback_logs (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user