feat: 站点媒体库与移动端底部导航,服务测试补强
- 新增媒体库:media_library/media_thumbs 服务(WebP 缩略图)、管理端 media 页面 - 移动端底部导航 MobileTabBar 替换 MobilePostBar - 旧数据导入增强与测试、路由与登录会话/板块侧边栏测试补强 - site-doc 组件精简(移除 Breadcrumb),文档新增迁移公告说明
This commit is contained in:
@@ -73,6 +73,7 @@ type AuthService struct {
|
||||
db *gorm.DB
|
||||
jwtSecret []byte
|
||||
encKey []byte // refresh token 落库密文的 AES-GCM 密钥(由 JWT 密钥派生)
|
||||
devMode bool
|
||||
}
|
||||
|
||||
func NewAuthService(db *gorm.DB, jwtSecret string) *AuthService {
|
||||
@@ -81,6 +82,21 @@ func NewAuthService(db *gorm.DB, jwtSecret string) *AuthService {
|
||||
return &AuthService{db: db, jwtSecret: []byte(jwtSecret), encKey: keySum[:]}
|
||||
}
|
||||
|
||||
// WithDevMode dev 模式放宽轮转宽限期:开发中 go run / next dev 重启频繁,
|
||||
// 轮换响应丢失后的重放往往远超 60s 才回来,同链重放不应升级为盗用判定
|
||||
func (s *AuthService) WithDevMode(dev bool) *AuthService {
|
||||
s.devMode = dev
|
||||
return s
|
||||
}
|
||||
|
||||
// rotationGrace 轮转宽限期:dev 放宽(见 RefreshRotationGrace 注释)
|
||||
func (s *AuthService) rotationGrace() time.Duration {
|
||||
if s.devMode {
|
||||
return 15 * time.Minute
|
||||
}
|
||||
return RefreshRotationGrace
|
||||
}
|
||||
|
||||
// dummyPasswordHash 用户不存在时也执行一次 bcrypt 比较,避免通过响应耗时枚举用户名
|
||||
var dummyPasswordHash, _ = bcrypt.GenerateFromPassword(
|
||||
[]byte("j13-dummy-password-for-constant-timing"), bcrypt.DefaultCost)
|
||||
@@ -133,7 +149,8 @@ func (s *AuthService) Register(username, email, password string) (*model.User, e
|
||||
}
|
||||
|
||||
// Login 用户登录,返回 access token + refresh token + user;
|
||||
// kicked 为本次登录顶掉的旧会话数(真·单会话:任何新登录踢掉所有旧设备;>0 供上层广播告知)
|
||||
// kicked 为本次登录因超出会话上限被淘汰的最旧会话数(多会话并存:
|
||||
// 新登录不踢旧设备,仅超过 maxActiveSessions 时裁剪;>0 供上层广播告知)
|
||||
func (s *AuthService) Login(username, password, ip, ua string) (string, string, *model.User, int, error) {
|
||||
var user model.User
|
||||
if err := s.db.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
@@ -317,7 +334,8 @@ func (s *AuthService) decryptToken(encoded string) (string, error) {
|
||||
}
|
||||
|
||||
// CreateRefreshToken 创建并存储 refresh token(存哈希 + 密文 + 设备信息)。
|
||||
// 返回值 kicked 为被顶掉的旧会话行数(真·单会话:踢掉所有旧设备)。
|
||||
// 多会话并存:各设备登录互不影响,新会话构成独立的轮转家族(family_id);
|
||||
// 返回值 kicked 为因超出会话上限被淘汰的最旧会话行数。
|
||||
func (s *AuthService) CreateRefreshToken(userID uint, ip, ua string) (string, uint, int, error) {
|
||||
token := generateRandomToken()
|
||||
cipherText, err := s.encryptToken(token)
|
||||
@@ -342,7 +360,7 @@ func (s *AuthService) CreateRefreshToken(userID uint, ip, ua string) (string, ui
|
||||
return "", 0, 0, err
|
||||
}
|
||||
rt.FamilyID = rt.ID
|
||||
kicked := s.revokeAllSessionsExcept(userID, rt.FamilyID)
|
||||
kicked := s.enforceSessionCap(userID)
|
||||
return token, rt.FamilyID, kicked, nil
|
||||
}
|
||||
|
||||
@@ -410,7 +428,7 @@ func (s *AuthService) RotateRefreshToken(oldToken, ip, ua string) (string, strin
|
||||
|
||||
if rt.Revoked {
|
||||
// 宽限期内、且轮转链指向的新 token 仍健康:并发重放,返回同一对
|
||||
if rt.RotatedTo != 0 && rt.RotatedAt != nil && now.Sub(*rt.RotatedAt) <= RefreshRotationGrace {
|
||||
if rt.RotatedTo != 0 && rt.RotatedAt != nil && now.Sub(*rt.RotatedAt) <= s.rotationGrace() {
|
||||
var next model.RefreshToken
|
||||
if e := tx.First(&next, rt.RotatedTo).Error; e == nil &&
|
||||
next.UserID == rt.UserID && !next.Revoked && now.Before(next.ExpiresAt) &&
|
||||
@@ -423,13 +441,16 @@ func (s *AuthService) RotateRefreshToken(oldToken, ip, ua string) (string, strin
|
||||
}
|
||||
}
|
||||
}
|
||||
// 其他吊销后重放:判定为盗用,撤销该用户整个 refresh token 家族
|
||||
// 其他吊销后重放:判定为盗用,仅撤销该轮转链所在家族(OAuth BCP:
|
||||
// 被盗的是这台设备的链,不牵连用户其余登录设备——否则开发期
|
||||
// middleware/浏览器丢轮换的竞态会把所有设备一齐踢下线)
|
||||
fam := sessionFamilyID(rt)
|
||||
if err := tx.Model(&model.RefreshToken{}).
|
||||
Where("user_id = ? AND revoked = ?", rt.UserID, false).
|
||||
Where("(family_id = ? OR id = ?) AND revoked = ?", fam, fam, false).
|
||||
Updates(map[string]any{"revoked": true, "token_cipher": "", "updated_at": now}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("[auth] 检测到 refresh token 吊销后重放,已撤销 user=%d 的全部 refresh token", rt.UserID)
|
||||
log.Printf("[auth] 检测到 refresh token 吊销后重放,已撤销 family=%d 的 refresh token", fam)
|
||||
outErr = ErrRefreshReused
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user