[开发日记]Spring Boot + MyBatis-Plus 抽奖系统排障实录:从 JWT 被拦截到雪花 ID 失控,我是怎样一步步修通登录与人员列表的

1. 背景介绍

这两天我在继续完善一个 Spring Boot + MyBatis-Plus + MySQL + 静态 HTML/jQuery 的抽奖系统后台。当前主要在推进三块内容:

  1. 用户注册与密码登录
  2. 后台管理页跳转与登录态维护
  3. 人员列表查询与展示

问题就是在这一串前后端联调里被连续触发出来的:前端看起来"登录成功"了,但页面不跳;人员列表接口明明已经放行,却还是拿不到数据;数据库里的用户主键又莫名其妙变成了超大的雪花 ID。整个过程很典型,也很适合拿来做一次完整复盘。

2. 遭遇的困境(Bug/报错)

最开始出现的是几个连续的异常现象。

现象 1:登录提示成功,但页面不跳转

text 复制代码
登录成功,Token 已保存。正在进入普通用户页面...

页面只停留在这句提示上,没有真正进入后台页面。

现象 2:人员列表接口请求被拦截

后端日志里最早出现的是 JWT 解析失败:

text 复制代码
获取路径: /user/base-user/find-lis
获取token: "eyJhbGciOiJIUzI1NiJ9..."
JWT signature does not match locally computed signature.
token解析失败

这里最醒目的细节是:token 外面带了一层双引号

现象 3:拦截器通过后,人员列表仍然报 500

当我把 token 问题修掉之后,日志继续往下走,新的异常就浮出来了:

java 复制代码
java.lang.NullPointerException: Cannot invoke 
"com.amadeus.lotterysystem.dao.dataobject.Encrypt.getValue()" 
because the return value of 
"com.amadeus.lotterysystem.dao.dataobject.UserDO.getPhoneNumber()" is null
    at com.amadeus.lotterysystem.service.Impl.UserServiceImpl.lambda$findUserInfoList$0(UserServiceImpl.java:81)

也就是说:拦截器这次其实已经放行了,真正炸掉的是人员列表的数据转换逻辑。

现象 4:新注册用户的 ID 仍然是超大的雪花 ID

我后来排查数据库时发现:

sql 复制代码
CREATE TABLE `user` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  ...
) ENGINE=InnoDB AUTO_INCREMENT=2062046654608482309

表面上看 id 已经是 AUTO_INCREMENT,但自增起点居然已经被抬到了一个巨大的值,所以后续插入的新用户仍然会拿到看起来像"雪花算法"的超大 ID。

3. 排查与分析

这轮问题不是单点故障,而是一串非常典型的"前后端联调连锁反应"。

3.1 登录成功但不跳转

我先回看了登录页 blogin.html 的成功分支。最开始的问题很简单:

  • 登录成功后只是执行了 showSuccess(...)
  • 根本没有页面跳转逻辑

后来我补了跳转,但又引入了一个前端小坑:

javascript 复制代码
var redirect = params.get('redirect');

这里使用了 params,但页面里其实没有定义这个变量,所以实际执行到跳转回调时,JS 直接抛错,导致页面仍然不跳。

3.2 JWT 被拦截的真正原因

从日志里能看出,后端拿到的是:

text 复制代码
"eyJhbGciOiJIUzI1NiJ9..."

也就是带双引号的字符串,而不是原始 JWT。JWT 验签是严格按原始字符串来算的,只要多一个字符,签名就对不上。

所以这个问题的根因不是"token 无效",而是:

  • 前端存储或读取 token 时把它变成了带引号的字符串
  • 后端拦截器又没有做清洗
  • 最终导致 SignatureException
3.3 人员列表 500 的底层原因

当拦截器问题修掉后,真正的数据问题才露出来。

人员列表查询走的是:

  • UserMapper.selectUserListByIdentity(...)
  • UserServiceImpl.findUserInfoList(...)

findUserInfoList 里有这么一段:

java 复制代码
userDTO.setPhoneNumber(userDO.getPhoneNumber().getValue());

但查询人员列表时,phoneNumber 没有被正确映射回来,导致 userDO.getPhoneNumber()null

本质原因有两个:

  1. 列表查询的 SQL 用的是 select * from user
  2. 这条查询没有复用前面已经写好的 @Results / ResultMap,于是 EncryptTypeHandler 没有稳定生效

于是最终形成链路:

  • Mapper 没把 phone_number 正确映射为 Encrypt
  • Service 层又默认它一定不为空
  • 一调用 .getValue() 就直接 NPE
3.4 用户主键为什么还会继续变大

这是最容易误判的一类问题。

我最开始修的是实体主键策略:

  • BaseDO.id 增加了 @TableId(type = IdType.AUTO)
  • 并补了 MyBatis-Plus 全局配置 id-type=auto

但数据库里还是继续插出超大 ID。重新检查后发现,问题不在"当前插入策略",而在 数据库自增计数器已经被历史雪花 ID 顶高了

也就是说:

  • 代码现在确实不再主动生成雪花 ID 了
  • 但是 MySQL 自增序列已经来到 2062046654608482309
  • 所以哪怕走 AUTO_INCREMENT,新数据依然会是巨大的数字

4. 终极解决方案

这一轮的解决并不是改一个点,而是把整条链路都补齐。

4.1 登录成功后补齐跳转逻辑

修改前

javascript 复制代码
showSuccess('登录成功,Token 已保存。身份:' + formatIdentity(returnedIdentity));

修改后

javascript 复制代码
var queryParams = new URLSearchParams(window.location.search);

showSuccess('登录成功,Token 已保存。正在进入' + formatIdentity(returnedIdentity) + '页面...');
var redirectPath = getRedirectPath(returnedIdentity);
window.setTimeout(function () {
    window.top.location.href = redirectPath;
}, 300);

function getRedirectPath(identity) {
    var redirect = queryParams.get('redirect');
    if (redirect && redirect.charAt(0) === '/') {
        return redirect;
    }

    if (identity === 'ADMIN') {
        return '/admin.html';
    }

    return '/admin.html';
}

这里顺手把 window.location.href 提升成了 window.top.location.href,这样即使登录页在 iframe 里,也能推动整个页面跳转。

4.2 统一修复 token 存取与解析

前端修改前

javascript 复制代码
localStorage.setItem('token', result.token);

前端修改后

javascript 复制代码
var token = normalizeToken(result.token);
localStorage.setItem('token', token);
localStorage.setItem('user_token', token);

function normalizeToken(token) {
    return token ? String(token).trim().replace(/^"+|"+$/g, '') : '';
}

后端拦截器修改前

java 复制代码
String jwtToken = request.getHeader("user_token");
Claims claims = JWTUtil.parseJWT(jwtToken);
if (claims == null) {
    return false;
}

后端拦截器修改后

java 复制代码
String jwtToken = normalizeToken(request.getHeader("user_token"));
Claims claims = JWTUtil.parseJWT(jwtToken);
if (claims == null) {
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    return false;
}

private String normalizeToken(String token) {
    if (token == null) {
        return null;
    }
    return token.trim().replaceAll("^\"+|\"+$", "");
}

这样做的好处是:前端不再制造脏 token,后端也具备容错能力。

4.3 修复人员列表接口与数据映射

Mapper 修改前

java 复制代码
@Select("<script>" +
        " select * from user" +
        " <if test=\"identity!=null\">" +
        "    where identity = #{identity}" +
        " </if>" +
        " order by id desc" +
        " </script>")
List<UserDO> selectUserListByIdentity(@Param("identity")String identity);

Mapper 修改后

java 复制代码
@Select("<script>" +
        " select id, gmt_create, gmt_modified, user_name, email, phone_number, password, identity from user" +
        " <if test=\"identity!=null\">" +
        "    where identity = #{identity}" +
        " </if>" +
        " order by id desc" +
        " </script>")
@ResultMap("UserDOMap")
List<UserDO> selectUserListByIdentity(@Param("identity")String identity);

Service 修改前

java 复制代码
userDTO.setPhoneNumber(userDO.getPhoneNumber().getValue());

Service 修改后

java 复制代码
userDTO.setPhoneNumber(userDO.getPhoneNumber() == null ? null : userDO.getPhoneNumber().getValue());

同时我还修正了 Controller 的路径和返回字段:

java 复制代码
@RequestMapping("/base-user/find-list")

并把列表里的 identity 从误填的 email 改回真正的身份值。

4.4 修复 MyBatis-Plus 主键策略和数据库自增计数器

实体层修改前

java 复制代码
private Long id;

实体层修改后

java 复制代码
@TableId(value = "id", type = IdType.AUTO)
private Long id;

配置修改后

properties 复制代码
mybatis-plus.global-config.db-config.id-type=auto

但这还不够,因为数据库里已经被历史数据污染过了。

我最后还执行了数据库修复,把已有雪花 ID 用户迁回正常范围,并重置自增值:

sql 复制代码
UPDATE user
SET id = CASE id
    WHEN 2062046654608482306 THEN 1
    WHEN 2062046654608482307 THEN 2
    WHEN 2062046654608482308 THEN 3
    ELSE id
END
WHERE id IN (2062046654608482306, 2062046654608482307, 2062046654608482308);

ALTER TABLE user AUTO_INCREMENT = 4;

这一步做完后,后续新注册用户的 ID 才真正恢复为 4、5、6... 这种正常递增形式。

5. 踩坑心得

5.1 联调时,不要只盯着"第一处报错"

这次非常典型:

  • 先看到的是 JWT 被拦截
  • 修完以后又出现 人员列表 NPE
  • 再往后又挖出 数据库自增计数器异常

如果只修第一层,就会误以为"问题已经解决",但真正的业务链路其实还没跑通。完整走通一次真实请求链路,往往比只看局部代码更重要。

5.2 Token、主键策略、类型处理器,都是"系统级约定"

这几个问题表面分散,实际上都属于同一类:前后端或框架之间的约定没有统一

  • Token 存的时候和取的时候不一致
  • 主键策略在实体、框架、数据库三层没有完全对齐
  • EncryptTypeHandler 在单条查询里生效,但在列表查询里被绕开了

我的收获是:

  1. 认证字段命名要统一 ,比如固定只用 user_tokenAuthorization
  2. 主键策略要同时检查代码和数据库,不能只改其中一边
  3. 自定义类型处理器一旦存在,列表查询最好显式复用统一映射 ,别偷懒写 select *

这次排障虽然来回折腾了几轮,但也把登录、拦截、列表、数据库这几条链都理顺了。回头看,其实每一步都很值,因为系统现在终于不是"某个接口能跑",而是核心流程真正被打通了。

相关推荐
古城小栈1 小时前
Rustix库:Rust 系统编程 的 基石
开发语言·后端·rust
我登哥MVP1 小时前
Spring Boot 从“会用”到“精通”:Rest风格原理
java·spring boot·后端·spring·maven·intellij-idea·mybatis
Je1lyfish1 小时前
CMU15-445 (2025 Fall/2026 Spring) Project#4 - Concurrency Control
开发语言·数据库·c++·笔记·后端·算法·系统架构
我登哥MVP1 小时前
Spring Boot 从“会用”到“精通”:静态资源原理
java·spring boot·后端·spring·tomcat·maven·intellij-idea
奋斗的袍子0071 小时前
springboot集成国密算法SM2
java·spring boot·算法
caibixyy1 小时前
Springboot + flowable6.8.0
spring boot·flowable6.8.0
JAVA面经实录9171 小时前
SpringBoot 全套完整版学习文档(零基础+实战+面试+源码)
java·spring boot·spring·架构
接着奏乐接着舞1 小时前
springcloud xxl-job
后端·spring·spring cloud
我是一颗柠檬2 小时前
【Redis】Cluster集群Day11(2026年)
数据库·redis·后端·缓存