第三期 · 账号体系与双 Token 鉴权:让用户「无感登录,有感安全」

关键词:Spring Boot 3 · Spring Security 无状态 · JWT HS256 · Refresh Token 轮换 + 宽限期 · 微信 code2session · 静默登录 目标读者:后端 / 运维 / 前端


一、业务目标

二期做到了「单机可用」,三期的命题是:让用户能够在换手机、重装小程序后拿回数据,同时不牺牲一期的「免登录可用」体验。

三个硬性业务约束:

约束 含义 实现
登录不能是门槛 未登录必须完整可用 游客模式 → LocalAdapter
登录不能打断 用户不应看到「请先登录」弹窗 wx.login 静默登录,失败静默降级
登录不能丢数据 游客期记录必须无损上云 登录后自动触发合并(第四期详解)

对应的三个技术指标:

  • 静默登录成功率 ≥ 95%(失败自动降级,用户无感);
  • access token 有效期 120min,refresh 30 天并支持吊销;
  • 全站无 Session,水平扩展无状态。

二、核心功能范围

功能 说明
微信授权登录 双端 wx.login → code → 服务端 code2session → openid → 建/查用户 → 签发 Token
账号密码注册/登录 双端 BCrypt(10),注册即登录
双 Token 签发与轮换 双端 access 120min / refresh 30 天,轮换 + 60s 宽限期
401 自动续期 前端 单飞刷新 + 一次重放
登出 / 全设备登出 双端 单设备删除 / 按 userId 全删
注销账号 双端 级联删除全部数据
用户资料 双端 头像昵称读写
会话复用 前端 refresh 剩余 >7 天才复用,避免 token 膨胀
部署体系 运维 install.sh / rollout.sh / verify.sh / systemd / Nginx / Certbot

三、涉及的技术模块

bash 复制代码
mood-backend/
├── security/  JwtTokenProvider(HS256)· JwtAuthFilter(OncePerRequest)· CurrentUser(ThreadLocal)
├── config/    SecurityConfig(无状态链 + 白名单 + BCrypt)· OpenApiConfig · props/{Jwt,Wechat}Properties
├── service/   AuthService(★ 轮换核心)· WechatService(code2session)
├── document/  User(uk_openid / uk_username)· RefreshToken(uk_user_device + TTL 7 天)
├── common/    Result · ErrorCode · BizException · GlobalExceptionHandler · TraceIdFilter · HashUtil
├── task/      TokenCleanupTask(每日 03:30 兜底清理 + 容量观测)
└── deploy/    install.sh · rollout.sh · verify.sh · nginx-*.conf · mood-backend.service · .env

emotion-monster/
├── app.js        silentLogin / onLoginSuccess / canReuseSession / logout
├── utils/http.js ★ 401 单飞刷新 + 重放
└── utils/storage.js  session 域同步直写

四、关键技术选型与实现要点

4.1 鉴权方案对比

方案 有状态 吊销 泄漏影响 结论
Session + Cookie 即时 单会话 ❌ 小程序 Cookie 别扭,难扩展
单 JWT(长有效) 无法 长期失控 ❌ 不可接受
Access + Refresh(静态 refresh) refresh 长期有效 ⚠️ 可用但弱
Access + Refresh 轮换 + 宽限期(本项目) 可检测重放 ✅ 已选
Opaque Token + Redis 是(Redis) 即时 可控 引入 Redis 时再评估

为什么选「轮换 + 宽限期」而不是简单的静态 refresh?

因为网络是不可靠的:客户端发出 refresh 请求、服务端已轮换、但响应在回程丢失 ------ 客户端手里只剩旧 token,而旧 token 已失效,用户被强制登出。宽限期 让旧 token 在 60 秒内仍可用一次,解决这个 race;同时若旧 token 在宽限期 被使用,说明它可能被盗(重放),此时吊销该用户该设备的全部会话

4.2 JWT 结构

java 复制代码
// security/JwtTokenProvider.java:27-42
Key key = Keys.hmacShaKeyFor(secret.getBytes(UTF_8));   // 要求 secret ≥ 32 字节

// accessToken claims
sub       = userId
typ       = "access"
username  = ""(缺省)
deviceId  = ""(缺省)
iat / exp = now / now + accessTtlMinutes

解析异常映射:

java 复制代码
// L44-52
ExpiredJwtException           → ErrorCode.TOKEN_EXPIRED  (20001)
JwtException / IllegalArgument → ErrorCode.TOKEN_INVALID  (20002)

4.3 过滤器链与白名单

java 复制代码
// config/SecurityConfig.java:38-54
http
  .csrf(disable).formLogin(disable).httpBasic(disable)
  .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
  .authorizeHttpRequests(a -> a
      .requestMatchers("/api/v1/auth/**", "/api/v1/health",
                       "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html",
                       "/actuator/health").permitAll()
      .anyRequest().authenticated())
  .exceptionHandling(e -> e.authenticationEntryPoint(...))   // 401 输出 Result JSON
  .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
java 复制代码
// security/JwtAuthFilter.java:37-59
// ① 读 Authorization: Bearer <token>
// ② 解析成功 → CurrentUser.set(userId, deviceId) + SecurityContextHolder
// ③ 解析失败 → 直接写 401 JSON,不继续过滤器链
// ④ finally 必定 CurrentUser.clear() + SecurityContextHolder.clearContext()

④ 是必须的:Tomcat 线程复用,不清理 ThreadLocal 会造成严重的用户数据串号。

java 复制代码
// security/CurrentUser.java:22-28
public static String userId() {
  String v = USER_ID.get();
  if (v == null || v.isBlank()) throw new BizException(ErrorCode.TOKEN_INVALID);
  return v;
}

这是全业务层取当前用户的唯一入口 ,所有查询强制按 CurrentUser.userId() 隔离。第五期做管理端时要在此基础上新增 userType 校验。

4.4 Refresh Token 轮换:本期最复杂的一段代码

java 复制代码
// service/AuthService.java:155-164
public TokenResp refresh(RefreshReq req) {
  String oldHash = HashUtil.sha256(req.refreshToken());
  for (int attempt = 0; attempt < 2; attempt++) {     // 最多两轮
    TokenResp resp = tryRotate(req, oldHash);
    if (resp != null) return resp;
  }
  throw new BizException(ErrorCode.TOKEN_INVALID);     // CAS 连续失败
}

tryRotate(L171-229)的完整判定链:

kotlin 复制代码
① findByTokenHash(oldHash)
   未命中 → findByPreviousTokenHash(oldHash)   (宽限期内复用)
② 命中 previous 但超出宽限期(60s)
   → deleteByUserIdAndDeviceId(吊销该用户该设备全部会话)
   → throw TOKEN_INVALID        ★ 判定为重放攻击
③ revoked || expiresAt < now
   → delete(token) + throw TOKEN_EXPIRED
④ checkStatus(user)             ★ 封禁账号不能靠有效 refresh 无限续期
⑤ CAS 原子轮换:
   UpdateResult r = mongoTemplate.updateFirst(
       Query(_id = token.id, <tokenHash|previousTokenHash> = oldHash),
       Update.set("tokenHash", sha256(newRaw))
             .set("previousTokenHash", token.getTokenHash())   ← 旧 hash 降级为宽限凭证
             .set("rotatedAt", now)
             .set("expiresAt", now + refreshTtlDays)           ← 滑动过期
             .set("createdAt", now).set("revoked", false));
   if (r.getMatchedCount() == 0) return null;    ← 并发未命中,触发第二轮
⑥ 签发新 accessToken 返回

三个容易忽略的细节:

  1. 轮换时不更新 deviceId (L208-210 注释明示)------否则会撞 uk_user_device 唯一索引,把用户踢下线;
  2. 滑动过期 :每次轮换把 expiresAt 推到 now + 30 天,活跃用户永不过期;
  3. 只存 hash :库里只有 sha256(raw),明文仅签发时回传一次,库被拖走也无法冒用。

4.5 微信登录与 openid 处理

java 复制代码
// service/AuthService.java:114-147
WechatService.SessionResult session = wechatService.code2Session(req.code().trim());
User user = userRepository.findByOpenid(session.openid()).orElse(null);
boolean isNewUser = (user == null);
// 新用户:写入 openid / unionid / 默认昵称「微信用户」/ status=1
// 老用户:checkStatus + 按需覆盖昵称头像
// 新用户才 ensureSetting()
java 复制代码
// service/WechatService.java:45-102
// 开发模式:未配 appid/secret 且 dev-mode=true → 伪 openid "dev-" + sha256(code).substring(0,24)
// 非 dev 且缺凭据 → SYSTEM_ERROR「服务端未配置微信 appid/secret」
// RestTemplate 连接 3s / 读 5s;按 String 接收后手工解析(微信返回 text/plain)
// errcode != "0" → 报错;40226 特判 ACCOUNT_LOCKED(账号存在风险,暂无法登录)

安全红线:

  • session_key 不落库SessionResult 只作临时传递);
  • openid / unionid 不下发toUserInfo(L343-346)只返回 id/username/nickname/avatarUrl/createdAt
  • 密码强度 ^(?=.*[A-Za-z])(?=.*\d).{8,64}$,BCrypt(10) 存储,绝不明文或可逆加密

4.6 前端:静默登录与会话复用

js 复制代码
// app.js:89-103
silentLogin() {
  wx.login({
    success: (r) => {
      if (!r.code) return;
      http.post('/auth/wx-login', { code: r.code, deviceId: this.globalData.deviceId }, { auth: false })
        .then(data => this.onLoginSuccess(data))
        .catch(() => { /* 静默降级:保持游客态,本地数据不受影响 */ });
    },
    fail: () => {}
  });
}
js 复制代码
// app.js:13 + 78-84
const REUSE_MIN_REMAIN = 7 * 24 * 3600 * 1000;
canReuseSession() {
  const s = storage.getSession();
  if (!s || !s.refreshToken || !s.refreshExpiresAt) return false;
  const exp = Date.parse(s.refreshExpiresAt);
  if (isNaN(exp)) return false;
  return exp - Date.now() > REUSE_MIN_REMAIN;    // 剩余 <7 天才重新登录续期
}

为什么要 REUSE_MIN_REMAIN 如果每次冷启动都 wx.login 换发新 refreshToken,refresh_tokens 集合会单调膨胀(每个用户每天一条)。详见 docs/refresh_tokens-数据增长诊断报告-v1.0.md。设 7 天阈值后,活跃用户的 token 文档数从 O(启动次数) 降为 O(月)。

4.7 前端:401 单飞刷新 + 重放

js 复制代码
// utils/http.js:5, 20-58
let refreshTask = null;
function refreshAccessToken() {
  if (refreshTask) return refreshTask;                 // ★ 并发 401 只刷新一次
  const session = storage.getSession();
  if (!session || !session.refreshToken) return Promise.resolve(false);
  refreshTask = new Promise(resolve => {
    wx.request({
      url: BASE_URL + '/auth/refresh', method: 'POST',
      data: { refreshToken: session.refreshToken, deviceId: storage.getDeviceId() },
      success: (res) => {
        const body = res.data || {};
        if (res.statusCode === 200 && body.code === 0 && body.data) {
          storage.setSession({ ...session,
            accessToken: body.data.accessToken,
            refreshToken: body.data.refreshToken,
            refreshExpiresAt: body.data.refreshExpiresAt,
            expiresAt: new Date(Date.now() + body.data.expiresIn * 1000).toISOString() });
          resolve(true);
        } else { storage.clearSession(); resolve(false); }   // 续期失败 → 强制登出
      },
      fail: () => resolve(false)
    });
  }).then(ok => { refreshTask = null; return ok; }, () => { refreshTask = null; return false; });
  return refreshTask;
}
js 复制代码
// utils/http.js:84-95
if ((code === 20001 || code === 20002) && needAuth) {   // 注意:是业务码,不是 HTTP 401
  refreshAccessToken().then(ok => {
    if (!ok) { reject(new Error('登录已失效,请重新登录')); return; }
    doRequest(opts).then(resolve, reject);              // 只重放一次
  });
  return;
}

关键认知 :后端鉴权失败返回的是 HTTP 200 + code=20001GlobalExceptionHandlerBizException 的 TOKEN_* 映射为 401,但业务异常默认 200)。前端必须按业务码判断,不能只看 HTTP 状态码。这是全项目最容易踩的坑,第五期管理端方案把它列为风险 #1。


五、数据流转与接口设计

5.1 登录时序

scss 复制代码
小程序                          后端                        微信
  │ wx.login()                                              │
  │────────────────────────────────────────────────────────▶│
  │◀─────────── code(5 分钟有效,一次性)───────────────────│
  │ POST /auth/wx-login {code, deviceId}                     │
  │──────────────────▶│                                      │
  │                   │ GET sns/jscode2session               │
  │                   │─────────────────────────────────────▶│
  │                   │◀───── {openid, session_key} ─────────│
  │                   │ findByOpenid → 无则建 user + setting  │
  │                   │ newRefreshToken(): sha256 落库(upsert by userId+deviceId)
  │                   │ JwtTokenProvider 签 access           │
  │◀──────────────────│                                      │
  │ {userId, accessToken, refreshToken, expiresIn,
  │  refreshExpiresAt, isNewUser, user}                      │
  │                                                          │
  │ storage.setSession(...) → Api.init(true) → 合并游客数据 → 拉云端设置

5.2 接口清单

方法 路径 说明 鉴权
POST /api/v1/auth/register 账号密码注册(注册即登录),服务端强制 userType=USER
POST /api/v1/auth/login 账号密码登录
POST /api/v1/auth/wx-login {code, deviceId} → openid → Token
POST /api/v1/auth/refresh {refreshToken, deviceId} 轮换续期
POST /api/v1/auth/logout {refreshToken} 单设备
POST /api/v1/auth/logout-all 全设备登出
GET / PUT /api/v1/users/me 资料读写
DELETE /api/v1/users/me 注销,级联删除
GET /api/v1/health 健康检查

5.3 错误码

复制代码
SUCCESS 0           PARAM_ERROR 10001       PARAM_OUT_OF_RANGE 10002
TOKEN_EXPIRED 20001 TOKEN_INVALID 20002     ACCOUNT_LOCKED 20003   FORBIDDEN 20004
NOT_FOUND 30001     DUPLICATE 30003         INVALID_CREDENTIAL 30005
DATA_INVALID 40001  SYNC_CONFLICT 50001
SYSTEM_ERROR 90001  TOO_MANY_REQUESTS 90002

映射规则(GlobalExceptionHandler):

异常 HTTP
BizException(TOKEN_*) 401
BizException(FORBIDDEN / ACCOUNT_LOCKED) 403
BizException(TOO_MANY_REQUESTS) 429
其它 BizException 200(必须判 code)
MethodArgumentNotValidException / HttpMessageNotReadable / IllegalArgument 400
DuplicateKeyException 200 + DUPLICATE
其它 Exception 500,仅记日志不泄漏堆栈

5.4 文档模型与索引

java 复制代码
// document/RefreshToken.java
@CompoundIndex(name="uk_user_device", def="{'userId':1,'deviceId':1}", unique=true)  // L19
@Indexed  userId                       // L25
@Indexed(unique=true) tokenHash        // L28
@Indexed  previousTokenHash            // L37
@Indexed(expireAfterSeconds = 604800) expiresAt   // L50  TTL 7 天
// 字段:tokenHash(sha256) · previousTokenHash(宽限) · rotatedAt · deviceId · expiresAt · revoked
java 复制代码
// document/User.java
@CompoundIndex(name="uk_openid", def="{'openid':1}", unique=true, sparse=true)  // L17
@Indexed(unique=true, sparse=true) username                                     // L24
// 字段:username · passwordHash(BCrypt) · nickname · avatarUrl · openid · unionid · status(1正常/0冻结/2注销中)

application.yml:15 显式 spring.data.mongodb.auto-index-creation: true ------ 生产务必开启,否则唯一约束不生效,幂等与会话唯一性都无从谈起。

5.5 令牌清理

java 复制代码
// task/TokenCleanupTask.java
@Scheduled(cron = "${app.task.token-cleanup-cron:0 30 3 * * *}")   // 默认每日 03:30
deleteByExpiresAtBefore(now - 7 days);      // 与 TTL 索引对齐,兜底 TTL 线程异常/历史脏数据
reportCapacity();                            // total / valid / validRate% / active24h / storageMB / indexMB

设计定位:主力回收是 TTL 索引,本任务只兜底;多实例并发执行幂等,仅日志重复。


六、技术难点分析

难点 1:refresh_tokens 集合增长失控

现象:每次冷启动都签发新 refreshToken → 集合单调膨胀。

三层治理(已落地)

  1. upsert 而非 insert :按 (userId, deviceId) upsert,同设备复用同一文档;
  2. 前端会话复用REUSE_MIN_REMAIN = 7 天,剩余时间足够就不重新登录;
  3. TTL + 定时任务expiresAt TTL 7 天 + 每日 03:30 兜底清理 + 容量观测日志。

详见 docs/refresh_tokens-数据增长诊断报告-v1.0.md

难点 2:并发 401 引发刷新风暴

问题:页面并发发出 7 个请求,access 恰好过期 → 7 次 refresh → 前 6 次成功后旧 refresh 已失效,后续全部失败 → 用户被登出。

解法 :模块级 refreshTask 单飞句柄(http.js:5)。并发请求共享同一个 Promise,只发一次刷新请求,其余 await 同一结果。

边界 :refresh 失败则 clearSession(),所有等待者统一收到失败 → 提示重新登录(而非部分成功部分失败的混乱状态)。

难点 3:轮换中的网络竞态

问题:refresh 请求成功但响应丢失,客户端旧 token 已失效。

解法previousTokenHash + 60s 宽限期(JwtProperties: refreshGraceSeconds=60)。宽限期内旧 token 可用一次;超出宽限期使用旧 token → 判定重放 → 吊销该用户该设备全部会话。

难点 4:dev 模式的正确开关

app.wechat.dev-mode 默认 true(application.yml:37),未配凭据时用 dev-sha256(code) 伪造 openid,方便本地开发。

风险:生产忘记关 → 任何人用任意 code 都能登录成「dev-xxx」用户,且可能撞到他人账号。

必须application-local.ymldeploy/mood-backend.env 中显式 WECHAT_DEV_MODE=false(当前 env 已配置)。上线 checklist 必须核验。

难点 5:注销与事务

AuthService.deleteAccount(L269)标注 @Transactional,但工程未配置 MongoTransactionManager,该注解实际不生效。

影响:注销过程中若中断,可能残留部分数据。

应对:MongoDB 单机无副本集不支持多文档事务。短期内按「先软删 user(status=2)→ 异步清理」改造;或部署副本集后启用事务。第五期处理。


七、部署方案与要点

7.1 交付的脚本

文件 职责
deploy/install.sh(238 行) 环境初始化:识别 apt/dnf → 装 nginx+certbot → JDK17 (CentOS7 源无 JDK17,改下 Temurin)→ 建 mood 系统用户与目录 → 装 env(0600)→ systemd → logrotate(14 天)→ Mongo 检测 → 证书签发 → 防火墙
deploy/rollout.sh(62 行) 备份旧 jar(保留最近 5 个)→ 替换 → restart → 轮询 health 最多 120s → 超时自动回滚 + 打印日志
deploy/verify.sh(84 行) 8 项验收:systemd active → 8200 监听 → health 含 UP → 80 跳转 → HTTPS health → 证书剩余 >7 天 → 支持 TLS1.2 → Swagger 可访问;--api 追加注册→me→pull→moods→settings→无 Token 期望 401→注销
deploy/mood-backend.service User=moodEnvironmentFileJAVA_OPTS、SIGTERM + TimeoutStopSec=30Restart=on-failureStartLimitBurst=5、加固四项
deploy/nginx-mood.conf 80→443 跳转、ACME 直通、TLSv1.2/1.3(小程序要求)、HSTS、limit_req 10r/s burst=20、keepalive 32、四 Forwarded 头、client_max_body_size 10m

7.2 配置与密钥管理

优先级:环境变量 > application-local.yml > application.yml

变量 说明
MONGODB_URI 密码含 @ 必须编码为 %40authSource=admin
JWT_SECRET ≥32 字节,务必更换并定期轮换
WECHAT_APP_ID / WECHAT_APP_SECRET 小程序凭据
WECHAT_DEV_MODE 生产必须 false
SERVER_ADDRESS=127.0.0.1 / SERVER_PORT=8200 仅回环监听

⚠️ 现存风险deploy/mood-backend.env 含真实 Mongo 密码、JWT secret、微信 AppSecret 且已提交进仓库 (文件头注释自陈「请勿提交」)。处置 :立即轮换全部密钥 → 从 git 历史中清理(git filter-repo)→ 改为部署时由密钥管理服务/Ansible 下发。

7.3 灰度与回滚

rollout.sh 的回滚是文件级 + 健康检查驱动的:

bash 复制代码
备份 jar → 替换 → systemctl restart → 轮询 /api/v1/health(≤120s)
   └─ 超时/失败 → stop → 还原 jar → start → 打印 journalctl -n 100 → exit 1

建议补充:①启动前 curl 一次旧版本 health 作为基线;②保留最近 5 个 jar(已做);③关键发版先灰度单实例。


八、性能与安全考量

性能

现状 建议
JWT 校验 纯 HMAC 计算,无 IO ✅ 已最优
登录 QPS 每次需访问微信接口(3s/5s 超时) 微信侧限流时注意降级;可缓存 code → openid 结果(code 一次性,缓存窗口极短)
refresh 2 次 Mongo 查询 + 1 次 updateFirst 索引齐全,uk_user_device / tokenHash 唯一索引
线程池 RestTemplate 连接 3s / 读 5s ✅ 防止微信侧抖动拖垮线程池

安全

措施 状态
密码 BCrypt(10),不明文/可逆
refreshToken 只存 sha256
openid / session_key 不下发、不落库
全站 HTTPS,TLS ≥ 1.2,HSTS ✅(Nginx)
后端仅监听回环,MongoDB 仅回环
systemd 加固(NoNewPrivileges 等)
Nginx 限流 10r/s burst=20
账号级限流 / 登录失败锁定 ❌ 待补
密钥从仓库移除并轮换 ❌ 待办(高优先级)
注销事务语义 ❌ 待补
全链路 traceId(端 → 服务端) ❌ 小程序端未生成/透传 X-Trace-Id

九、风险与应对

风险 等级 应对
密钥入库泄露 轮换全部密钥 + 清理 git 历史 + 改为部署注入
生产 WECHAT_DEV_MODE=true 上线 checklist 核验;启动日志打印 dev-mode 告警
微信小程序 request 合法域名未配 生产必须已备案 HTTPS 域名并配置;verify.sh 已覆盖
401 风暴 前端单飞刷新(已实现)
重放攻击 宽限期 + 超期吊销整设备会话(已实现)
弱口令 / 撞库 密码强度正则;建议补失败次数限制与验证码
@Transactional 不生效 改异步清理或启用副本集事务
时区不一致 前后端统一 Asia/ShanghaiDateUtil.ZONE 固定
无 traceId 串联 小程序端生成 X-Trace-Id 并透传(第五期)

十、验收标准与交付物

验收标准

  • verify.sh 8 项全绿,--api 冒烟 7 步通过(含无 Token 期望 401);
  • 微信登录:新用户自动建号 + 建 setting,老用户不覆盖已有昵称;
  • refresh 轮换:连续两次 refresh 均成功;第二次用第一次的旧 token 在 60s 内仍成功一次;
  • 重放检测:超过 60s 后用旧 token → 返回 20002 且该设备全部会话被吊销;
  • 并发 401:同时发 10 个请求,只触发 1 次 /auth/refresh(抓包验证);
  • 全设备登出后,其它设备的 access/refresh 立即失效;
  • 注销账号后,mood/period/setting/refreshToken/user 全部清除;
  • 响应体中不含 openid / unionid / passwordHash / session_key
  • 生产环境 WECHAT_DEV_MODE=falseJWT_SECRET 非默认值且 ≥32 字节;
  • rollout.sh 模拟失败(替换成坏 jar)能自动回滚并恢复服务。

交付物

类型 内容
代码 security/ config/ service/AuthService service/WechatService task/TokenCleanupTask
前端 app.js 静默登录/会话复用、utils/http.js 单飞刷新
部署 deploy/ 9 个文件(install / rollout / verify / systemd / nginx×3 / env)
文档 详细设计文档 v6.0(登录与同步版)、docs/refresh_tokens-数据增长诊断报告-v1.0.md
接口 Swagger http://localhost:8200/swagger-ui.html

十一、后续优化方向

  1. 密钥治理:接入密钥管理(KMS / Vault / 云厂商 Secret Manager),禁止入库;
  2. 登录风控:IP + 账号维度失败次数限制、图形验证码、异地登录提醒;
  3. 端云 traceId 打通 :小程序端生成 X-Trace-Id 并在错误上报中携带,彻底解决「用户报错无法定位」;
  4. 会话管理页 :「我的设备」列表 + 单设备踢下线(复用 logout-all 的按 deviceId 删除能力);
  5. 注销异步化status=2 软删 + 定时任务清理,规避无事务问题;
  6. 账号体系扩展 :为第五期管理端预留 userTypeUSER / ADMIN)与 JWT claim。

相关推荐
耀耀切克闹灬19 小时前
Skyline 渲染问题记录以及总结
微信小程序
小程序开发X20 小时前
2026 深圳 APP 开发甄选:技术架构搭建、全流程落地、综合选型解读
微信小程序·小程序·app开发
EatFan1 天前
一个二维码背后的系统设计:批次生成、绑定、扫码与数据统计怎么做?
java·后端·微信小程序·二维码·qrcode
微笑的曙光2 天前
第一期 · 本地优先 MVP:不写一个后端,如何做出完整体验
微信小程序
EatFan2 天前
Java接入微信支付保姆式教程(三):SpringBoot 接入微信支付并完成统一下单
微信小程序·微信支付·springboot
QQ_21696290962 天前
基于SpringBoot+Vue的小生活平台的设计与实现
java·数据库·vue.js·spring boot·spring·微信小程序·生活
飞梦工作室2 天前
H5页面能否直接播放视频号直播?实战方案与踩坑总结
微信小程序·小程序
StevenLdh3 天前
情绪小恐龙:一个微信小程序从架构设计到部署上线的全记录
微信小程序·小程序·notepad++
Bs_MoneyMagnet3 天前
基于springboot+vue的旅游行程分享与推荐小程序的设计与实现 源码+文档
java·vue.js·spring boot·后端·微信小程序·毕业设计·计算机毕业设计