feat: App→网页免登录(一次性换票 SSO)——移动/桌面点链接直达已登录官网

- 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>
This commit is contained in:
wangjia
2026-07-04 21:30:08 +08:00
parent 11413df3db
commit a773a33779
13 changed files with 446 additions and 8 deletions
+4
View File
@@ -70,6 +70,8 @@ func Setup(r *gin.Engine, db *gorm.DB) {
{
auth.POST("/login", loginIP, authH.Login)
auth.POST("/refresh", refreshIP, authH.Refresh)
// App→网页免登录:一次性票据兑换(公开;票据 60s 单次可用,复用 refresh 限流)
auth.POST("/web-ticket/exchange", refreshIP, authH.WebTicketExchange)
}
// 公开接口(无需登录):读接口防爬、写接口防刷
@@ -95,6 +97,8 @@ func Setup(r *gin.Engine, db *gorm.DB) {
{
api.POST("/auth/logout", authH.Logout)
api.POST("/auth/ping", authH.Ping)
// App→网页免登录:签发一次性票据(需登录)
api.POST("/auth/web-ticket", authH.WebTicket)
// 在线会话列表:所有登录用户只读
api.GET("/sessions", sessionH.List)
// 强制下线:仅 admin/superadmin