|
|
@@ -28,10 +28,26 @@ type (
|
|
|
FindListByProductMembers(ctx context.Context, productCode string, page, pageSize int64) ([]*SysUser, map[int64]string, int64, error)
|
|
|
FindByIds(ctx context.Context, ids []int64) ([]*SysUser, error)
|
|
|
FindIdsByDeptId(ctx context.Context, deptId int64) ([]int64, error)
|
|
|
+ // UpdateProfile 更新用户资料字段(昵称 / 邮箱 / 手机 / 备注 / 部门 / 状态),username 仅用于
|
|
|
+ // 构造旧缓存键 `cacheSysUserUsernamePrefix` 做失效,**不会**被写入 SET 子句。若未来确实需要
|
|
|
+ // 修改 username,请独立实现 `UpdateUsernameTx`:
|
|
|
+ // ① 同事务内做 old/new 两份 UsernameKey 的失效;
|
|
|
+ // ② 捕获 1062 (UNIQUE 冲突) → response.ErrConflict,不得混进本方法的签名(审计 L-R11-3)。
|
|
|
UpdateProfile(ctx context.Context, id int64, username string, nickname, email, phone, remark string, deptId, newStatus int64, statusChanged bool, expectedUpdateTime int64) error
|
|
|
- UpdatePassword(ctx context.Context, id int64, password string, mustChangePassword int64) error
|
|
|
- UpdateStatus(ctx context.Context, id int64, status int64, expectedUpdateTime int64) error
|
|
|
- IncrementTokenVersion(ctx context.Context, id int64) (int64, error)
|
|
|
+ // UpdateProfileWithTx 与 UpdateProfile 行为等价,但 UPDATE 执行在调用方传入的事务里;
|
|
|
+ // 用于"改 deptId → 同事务内先 FOR SHARE sys_dept 目标行"修复 DeleteDept vs UpdateUser
|
|
|
+ // 的 write skew(审计 M-R11-3)。缓存失效仍由 m.ExecCtx 按 (idKey, usernameKey) 兜底。
|
|
|
+ UpdateProfileWithTx(ctx context.Context, session sqlx.Session, id int64, username string, nickname, email, phone, remark string, deptId, newStatus int64, statusChanged bool, expectedUpdateTime int64) error
|
|
|
+ // UpdatePassword 审计 H-R11-1:expectedUpdateTime 必须由调用方用**外层校验旧密码时拿到的
|
|
|
+ // 那一份 updateTime** 显式透传;禁止函数内部再 FindOne 自对齐乐观锁,否则内层 CAS 等于退化
|
|
|
+ // 为 last-write-wins,被"旧会话 + 知道旧密码"的攻击者可以把管理员紧急改过的新密码盖回。
|
|
|
+ // username 仅用于构造 `cacheSysUserUsernamePrefix` 缓存键失效(sysUser.username 唯一,本函数
|
|
|
+ // 不会修改它)。
|
|
|
+ UpdatePassword(ctx context.Context, id int64, username string, password string, mustChangePassword, expectedUpdateTime int64) error
|
|
|
+ // UpdateStatus 审计 M-R11-2:username 由调用方透传,避免仅为构造缓存键而多打一次 FindOne。
|
|
|
+ UpdateStatus(ctx context.Context, id int64, username string, status int64, expectedUpdateTime int64) error
|
|
|
+ // IncrementTokenVersion 审计 M-R11-2:username 由调用方透传,避免仅为构造缓存键而多打一次 FindOne。
|
|
|
+ IncrementTokenVersion(ctx context.Context, id int64, username string) (int64, error)
|
|
|
IncrementTokenVersionIfMatch(ctx context.Context, id int64, username string, expected int64) (int64, error)
|
|
|
}
|
|
|
|
|
|
@@ -125,17 +141,41 @@ func (m *customSysUserModel) UpdateProfile(ctx context.Context, id int64, userna
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
-func (m *customSysUserModel) UpdatePassword(ctx context.Context, id int64, password string, mustChangePassword int64) error {
|
|
|
- data, err := m.FindOne(ctx, id)
|
|
|
+// UpdateProfileWithTx 见接口注释(审计 M-R11-3)。实现上复用 m.ExecCtx 负责的 cache 失效语义,
|
|
|
+// 但 UPDATE 语句在调用方传入的 session 事务里执行。session==nil 时 panic,阻止误用——
|
|
|
+// 非事务场景必须走 UpdateProfile 而不是本方法。
|
|
|
+func (m *customSysUserModel) UpdateProfileWithTx(ctx context.Context, session sqlx.Session, id int64, username string, nickname, email, phone, remark string, deptId, newStatus int64, statusChanged bool, expectedUpdateTime int64) error {
|
|
|
+ if session == nil {
|
|
|
+ return errors.New("UpdateProfileWithTx requires a non-nil session")
|
|
|
+ }
|
|
|
+ sysUserIdKey := fmt.Sprintf("%s%v", cacheSysUserIdPrefix, id)
|
|
|
+ sysUserUsernameKey := fmt.Sprintf("%s%v", cacheSysUserUsernamePrefix, username)
|
|
|
+ now := time.Now().Unix()
|
|
|
+
|
|
|
+ res, err := m.ExecCtx(ctx, func(ctx context.Context, _ sqlx.SqlConn) (sql.Result, error) {
|
|
|
+ if statusChanged {
|
|
|
+ query := fmt.Sprintf("UPDATE %s SET `nickname`=?, `email`=?, `phone`=?, `remark`=?, `deptId`=?, `status`=?, `tokenVersion`=`tokenVersion`+1, `updateTime`=? WHERE `id`=? AND `updateTime`=?", m.table)
|
|
|
+ return session.ExecCtx(ctx, query, nickname, email, phone, remark, deptId, newStatus, now, id, expectedUpdateTime)
|
|
|
+ }
|
|
|
+ query := fmt.Sprintf("UPDATE %s SET `nickname`=?, `email`=?, `phone`=?, `remark`=?, `deptId`=?, `updateTime`=? WHERE `id`=? AND `updateTime`=?", m.table)
|
|
|
+ return session.ExecCtx(ctx, query, nickname, email, phone, remark, deptId, now, id, expectedUpdateTime)
|
|
|
+ }, sysUserIdKey, sysUserUsernameKey)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
+ affected, _ := res.RowsAffected()
|
|
|
+ if affected == 0 {
|
|
|
+ return ErrUpdateConflict
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
|
|
|
+func (m *customSysUserModel) UpdatePassword(ctx context.Context, id int64, username string, password string, mustChangePassword, expectedUpdateTime int64) error {
|
|
|
sysUserIdKey := fmt.Sprintf("%s%v", cacheSysUserIdPrefix, id)
|
|
|
- sysUserUsernameKey := fmt.Sprintf("%s%v", cacheSysUserUsernamePrefix, data.Username)
|
|
|
- // 乐观锁:WHERE 叠加 updateTime 与 FindOne 拿到的一致。避免 FindOne → Exec 之间并发改密把
|
|
|
- // 本次写盖成"最后一写赢"、或目标行被删除后仍返回成功造成语义欺骗(见审计 M-2)。
|
|
|
- expectedUpdateTime := data.UpdateTime
|
|
|
+ sysUserUsernameKey := fmt.Sprintf("%s%v", cacheSysUserUsernamePrefix, username)
|
|
|
+ // 审计 H-R11-1:expectedUpdateTime 必须来自**外层校验旧密码时读到的**快照。不要再内部 FindOne
|
|
|
+ // 自取 data.UpdateTime —— 那会让 CAS 在本函数内自我对齐,退化为 last-write-wins,让"会话持有 +
|
|
|
+ // 知道旧密码"的攻击者可以把 admin 紧急改过的新密码盖回到自己手里那一份。
|
|
|
res, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (sql.Result, error) {
|
|
|
query := fmt.Sprintf("UPDATE %s SET `password` = ?, `mustChangePassword` = ?, `tokenVersion` = `tokenVersion` + 1, `updateTime` = ? WHERE `id` = ? AND `updateTime` = ?", m.table)
|
|
|
return conn.ExecCtx(ctx, query, password, mustChangePassword, time.Now().Unix(), id, expectedUpdateTime)
|
|
|
@@ -144,8 +184,7 @@ func (m *customSysUserModel) UpdatePassword(ctx context.Context, id int64, passw
|
|
|
return err
|
|
|
}
|
|
|
if affected, _ := res.RowsAffected(); affected == 0 {
|
|
|
- // 行被删除或被并发改过:对外统一回 ErrUpdateConflict,避免对已删除用户返回 nil 让上层
|
|
|
- // 误判为"改密成功"(审计 M-2)。
|
|
|
+ // 行被删除或被并发改过(任何字段,包括 status/password/profile):统一回 ErrUpdateConflict。
|
|
|
return ErrUpdateConflict
|
|
|
}
|
|
|
return nil
|
|
|
@@ -158,14 +197,11 @@ func (m *customSysUserModel) UpdatePassword(ctx context.Context, id int64, passw
|
|
|
// - expectedUpdateTime 不匹配 → ErrUpdateConflict;上层统一回 409 "数据已被其他操作修改"。
|
|
|
// - 避免并发冻结/解冻请求走"last-write-wins",出现两个 admin 同时点"冻结"/"解冻"
|
|
|
// 时后到者覆盖先到者、tokenVersion 被连续加两次把刚刚解冻的用户再次踢下线的诡异现象。
|
|
|
-func (m *customSysUserModel) UpdateStatus(ctx context.Context, id int64, status int64, expectedUpdateTime int64) error {
|
|
|
- data, err := m.FindOne(ctx, id)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
-
|
|
|
+func (m *customSysUserModel) UpdateStatus(ctx context.Context, id int64, username string, status int64, expectedUpdateTime int64) error {
|
|
|
+ // 审计 M-R11-2:username 由调用方(ValidateStatusChange 返回的目标用户对象)显式透传,
|
|
|
+ // 不再内部 FindOne。真实并发安全继续靠 `WHERE updateTime = expectedUpdateTime` 乐观锁兜底。
|
|
|
sysUserIdKey := fmt.Sprintf("%s%v", cacheSysUserIdPrefix, id)
|
|
|
- sysUserUsernameKey := fmt.Sprintf("%s%v", cacheSysUserUsernamePrefix, data.Username)
|
|
|
+ sysUserUsernameKey := fmt.Sprintf("%s%v", cacheSysUserUsernamePrefix, username)
|
|
|
res, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (sql.Result, error) {
|
|
|
query := fmt.Sprintf("UPDATE %s SET `status` = ?, `tokenVersion` = `tokenVersion` + 1, `updateTime` = ? WHERE `id` = ? AND `updateTime` = ?", m.table)
|
|
|
return conn.ExecCtx(ctx, query, status, time.Now().Unix(), id, expectedUpdateTime)
|
|
|
@@ -187,17 +223,14 @@ func (m *customSysUserModel) UpdateStatus(ctx context.Context, id int64, status
|
|
|
// 必须调用 IncrementTokenVersionIfMatch 走 CAS 语义,否则会回到 R5 以前的并发 rotate 窗口,
|
|
|
// 两次并发 refresh 都能换到新令牌,等同于会话劫持(见审计 L-2)。
|
|
|
// 调用前请先走 TokenOpLimiter 等限流,避免被反复触发把合法用户 kick 出登录。
|
|
|
-func (m *customSysUserModel) IncrementTokenVersion(ctx context.Context, id int64) (int64, error) {
|
|
|
- data, err := m.FindOne(ctx, id)
|
|
|
- if err != nil {
|
|
|
- return 0, err
|
|
|
- }
|
|
|
-
|
|
|
+func (m *customSysUserModel) IncrementTokenVersion(ctx context.Context, id int64, username string) (int64, error) {
|
|
|
+ // 审计 M-R11-2:username 由调用方显式透传,不再内部 FindOne 仅为构造缓存键。
|
|
|
+ // 调用方(Logout 唯一入口)在 UserDetailsLoader.Load 之后天然已经持有 ud.Username。
|
|
|
sysUserIdKey := fmt.Sprintf("%s%v", cacheSysUserIdPrefix, id)
|
|
|
- sysUserUsernameKey := fmt.Sprintf("%s%v", cacheSysUserUsernamePrefix, data.Username)
|
|
|
+ sysUserUsernameKey := fmt.Sprintf("%s%v", cacheSysUserUsernamePrefix, username)
|
|
|
|
|
|
var newVersion int64
|
|
|
- err = m.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error {
|
|
|
+ err := m.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error {
|
|
|
query := fmt.Sprintf("UPDATE %s SET `tokenVersion` = LAST_INSERT_ID(`tokenVersion` + 1), `updateTime` = ? WHERE `id` = ?", m.table)
|
|
|
res, err := session.ExecCtx(ctx, query, time.Now().Unix(), id)
|
|
|
if err != nil {
|