a773a33779
- backend:POST /auth/web-ticket(需登录,签 60s 单次票据,每用户限频)+ POST /auth/web-ticket/exchange(公开,原子即焚兑换正常登录态,响应同 /auth/login 另带 shop_code);web_tickets 新表 + user_sessions.kind 列 (sso 会话:web 平台、refresh 12h 硬到期、不占设备并发配额、设备管理可见 可踢);清理任务顺带清过期票据;schema.sql/AutoMigrate/testutil 同步 - web:/sso 落地页——兑票写入与 Web 版共享的 localStorage 后跳 redirect; redirect 白名单只收本站相对路径(防 open redirect),票据即时从地址栏抹除 - client:launchAuthedWebPath(ref, path) 封装(签票→拼 /sso URL→launchUrl, 失败降级未登录直开;桌面/移动同一套);授权面板「查看套餐价格」接入 test(backend): web_ticket_test 三用例(兑换+续期/单次即焚/过期拒绝/ sso 不占配额双向验证) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
98 lines
3.3 KiB
Go
98 lines
3.3 KiB
Go
package service
|
|
|
|
// App→网页免登录一次性换票的回归测试:签票/兑票/单次即焚/过期/sso 不占配额。
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/wangjia/jiu/backend/config"
|
|
"github.com/wangjia/jiu/backend/internal/model"
|
|
"github.com/wangjia/jiu/backend/testutil"
|
|
)
|
|
|
|
func TestWebTicket_ExchangeOnce(t *testing.T) {
|
|
db := testutil.SetupTestDB()
|
|
shop := testutil.CreateTestShop(db, "SSO001")
|
|
user := testutil.CreateTestUser(db, shop.ID, "admin", "password123", "admin")
|
|
svc := NewAuthService(db)
|
|
|
|
ticket, ttl, err := svc.IssueWebTicket(shop.ID, user.ID)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, ticket)
|
|
assert.Equal(t, 60, ttl)
|
|
|
|
dev := DeviceInfo{Platform: "web", IP: "1.2.3.4", UserAgent: "test"}
|
|
pair, u, sh, err := svc.LoginWithWebTicket(ticket, dev)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, pair.AccessToken)
|
|
assert.Equal(t, user.ID, u.ID)
|
|
assert.Equal(t, "SSO001", sh.Code)
|
|
|
|
// 兑出的 refresh token 可正常续期(会话有效)
|
|
_, err = svc.RefreshTokens(pair.RefreshToken)
|
|
assert.NoError(t, err)
|
|
|
|
// 单次即焚:二次兑换必失败
|
|
_, _, _, err = svc.LoginWithWebTicket(ticket, dev)
|
|
assert.ErrorIs(t, err, ErrTicketInvalid)
|
|
}
|
|
|
|
func TestWebTicket_Expired(t *testing.T) {
|
|
db := testutil.SetupTestDB()
|
|
shop := testutil.CreateTestShop(db, "SSO002")
|
|
user := testutil.CreateTestUser(db, shop.ID, "admin", "password123", "admin")
|
|
svc := NewAuthService(db)
|
|
|
|
ticket, _, err := svc.IssueWebTicket(shop.ID, user.ID)
|
|
require.NoError(t, err)
|
|
// 手动把票据改成已过期
|
|
require.NoError(t, db.Model(&model.WebTicket{}).
|
|
Where("ticket = ?", ticket).
|
|
Update("expires_at", time.Now().Add(-time.Minute)).Error)
|
|
|
|
_, _, _, err = svc.LoginWithWebTicket(ticket, DeviceInfo{Platform: "web"})
|
|
assert.ErrorIs(t, err, ErrTicketInvalid)
|
|
}
|
|
|
|
func TestWebTicket_SsoSessionNotCountedInQuota(t *testing.T) {
|
|
db := testutil.SetupTestDB()
|
|
old := config.C.Session.LimitTotal
|
|
config.C.Session.LimitTotal = 2
|
|
defer func() { config.C.Session.LimitTotal = old }()
|
|
|
|
shop := testutil.CreateTestShop(db, "SSO003")
|
|
user := testutil.CreateTestUser(db, shop.ID, "admin", "password123", "admin")
|
|
svc := NewAuthService(db)
|
|
|
|
// 占满 2 个正常配额
|
|
_, _, err := svc.Login("SSO003", "admin", "password123",
|
|
DeviceInfo{DeviceID: "d1", Platform: "macos"})
|
|
require.NoError(t, err)
|
|
_, _, err = svc.Login("SSO003", "admin", "password123",
|
|
DeviceInfo{DeviceID: "d2", Platform: "ios"})
|
|
require.NoError(t, err)
|
|
|
|
// 配额已满仍可换票登录(sso 不受配额限制)
|
|
ticket, _, err := svc.IssueWebTicket(shop.ID, user.ID)
|
|
require.NoError(t, err)
|
|
_, _, _, err = svc.LoginWithWebTicket(ticket, DeviceInfo{Platform: "web"})
|
|
require.NoError(t, err, "sso 会话不受设备配额限制")
|
|
|
|
// sso 会话短时效 + 打标记
|
|
var sess model.UserSession
|
|
require.NoError(t, db.Where("shop_id = ? AND kind = 'sso'", shop.ID).
|
|
First(&sess).Error)
|
|
assert.Equal(t, "web", sess.Platform)
|
|
assert.WithinDuration(t, time.Now().Add(12*time.Hour), sess.RefreshExpAt,
|
|
time.Minute)
|
|
|
|
// 反向:sso 会话在场时,正常登录的配额统计不把它算进去
|
|
_, _, err = svc.Login("SSO003", "admin", "password123",
|
|
DeviceInfo{DeviceID: "d1", Platform: "macos"}) // 同设备重登复用坑位
|
|
assert.NoError(t, err, "sso 会话不占配额,正常登录不受影响")
|
|
}
|