Spring Boot + Sa-Token 实时聊天系统:用户注册流程源码深度剖析

Spring Boot + Sa-Token 实时聊天系统:用户注册流程源码深度剖析

本文以微狗实时聊天项目为例,从一行注册接口出发,逐层拆解 Controller → Service → Mapper 的完整调用链路,挖掘其中值得学习的 4 个技术亮点。


一、项目背景

微狗是一个基于 Spring Boot + Netty 的实时聊天网站,支持私聊、群聊、文件上传、第三方登录等功能。项目采用经典的三层架构,注册流程虽看似简单,但细节中藏着几个容易忽略的工程实践。


二、请求入口:Controller 层

java 复制代码
// UserController.java
@PostMapping("/register")
@ApiOperation(value = "用户注册")
public BaseResponse<Long> userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
    if (userRegisterRequest == null) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR);
    }
    String userAccount = userRegisterRequest.getUserAccount();
    String userPassword = userRegisterRequest.getUserPassword();
    String checkPassword = userRegisterRequest.getCheckPassword();
    if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
        return null;
    }
    long result = userService.userRegister(userAccount, userPassword, checkPassword);
    return ResultUtils.success(result);
}

Controller 的职责很清晰:接收请求 → 提取参数 → 调用 Service → 包装响应。这里只做最基本的空值校验,把真正的业务逻辑全部下放到 Service 层,保持 Controller 的轻薄。

【亮点一:参数校验的双层防御】

Controller 层做了粗粒度的空值检查(防止明显的恶意请求直接打到数据库),Service 层再做细粒度的长度、重复性校验。这种"粗筛 + 精筛"的模式,既避免无意义的数据库查询,又保证业务规则的完整覆盖。

很多项目只在 Controller 做一次校验就完事,一旦有人绕过 Controller(比如内部调用 Service),就会出现数据不一致。


三、核心逻辑:Service 层

java 复制代码
// UserServiceImpl.java
@Override
public long userRegister(String userAccount, String userPassword, String checkPassword) {
    // ① 参数校验
    if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");
    }
    if (userAccount.length() < 4) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户账号过短");
    }
    if (userPassword.length() < 8 || checkPassword.length() < 8) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户密码过短");
    }
    if (!userPassword.equals(checkPassword)) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "两次输入的密码不一致");
    }

    // ② synchronized 锁------防止并发注册同一账号
    synchronized (userAccount.intern()) {
        // ③ 查重
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("userAccount", userAccount);
        long count = this.baseMapper.selectCount(queryWrapper);
        if (count > 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号重复");
        }

        // ④ MD5 + SALT 加密
        String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());

        // ⑤ 保存用户
        User user = new User();
        user.setUserAccount(userAccount);
        user.setUserPassword(encryptPassword);
        user.setUserAvatar(DEFAULT_AVATAR);
        user.setUserName(DEFAULT_NICKNAME + System.currentTimeMillis());
        boolean saveResult = this.save(user);
        if (!saveResult) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "注册失败,数据库错误");
        }

        // ⑥ 新用户自动加入系统群聊
        Long userId = user.getId();
        UserRoomRelate userRoomRelate = new UserRoomRelate();
        userRoomRelate.setRoomId(SYSTEM_ROOM_ID);
        userRoomRelate.setUserId(userId);
        userRoomRelateService.save(userRoomRelate);

        return userId;
    }
}

这段代码只有 50 行左右,但藏着 4 个值得深挖的技术亮点。


四、亮点二:synchronized (userAccount.intern()) --- 用字符串常量池做并发锁

这是整段代码最值得讨论的一行:

java 复制代码
synchronized (userAccount.intern()) {

为什么需要锁?

注册流程的"查重 → 插入"是一个先查后写的非原子操作 。假设两个线程同时用账号 zhangsan 注册:

复制代码
时间线:
  线程A: selectCount → count=0 → 准备insert
  线程B: selectCount → count=0 → 准备insert   ← 此时A还没insert,B查到的也是0
  线程A: insert 成功
  线程B: insert 成功   ← 重复账号被写入了!

加锁后,同一账号的注册请求会串行执行,杜绝并发重复。

为什么用 intern()?

String.intern() 是 JDK 的原生方法,它会将字符串放入 JVM 的字符串常量池,相同内容的字符串返回同一个对象引用。

java 复制代码
"zhangsan".intern() == "zhangsan".intern()  // true,指向常量池同一个对象
"zhangsan1".intern() != "zhangsan2".intern() // 不同账号,不同锁对象

这意味着:

  • 同账号的注册请求 → 锁同一个对象 → 串行执行
  • 不同账号的注册请求 → 锁不同对象 → 并行执行

synchronized(this) 细细得多------后者会让所有注册请求排队,而 intern() 只阻塞同账号的请求。

这个方案的局限

问题 说明
JVM 常量池大小有限 大量账号注册会导致常量池膨胀,在高并发场景下可能触发 GC 问题
单机有效 分布式部署时,两台机器的 JVM 常量池互不共享,锁失效
JDK 7+ 常量池移到堆 不再是 PermGen 溢出,但仍然占用堆内存

改进建议 :生产环境应使用 分布式锁 (Redis SETNX 或 Redisson),本项目作为学习项目,单机锁够用且足够直观。


五、亮点三:先查重再插入 --- 防止脏数据的经典模式

java 复制代码
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userAccount);
long count = this.baseMapper.selectCount(queryWrapper);
if (count > 0) {
    throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号重复");
}

配合 synchronized 锁,这段查重逻辑确保了:

  1. 单线程内:查到 count > 0 说明账号已存在,直接拒绝
  2. 多线程内:锁保证了同一账号不会被两个线程同时查到 count=0

更优方案 :在数据库层面给 user_account 字段加 唯一索引,即使代码层没防住,数据库也会拒绝重复插入。这是"代码防御 + 数据库兜底"的双保险策略。


六、亮点四:MD5 + 固定盐值加密 --- 密码存储的基本功

java 复制代码
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());

其中 SALT 定义在 SystemConstants 中:

java 复制代码
public interface SystemConstants {
    String SALT = "cong";
}

加密流程

复制代码
原始密码: 12345678
加盐拼接:  cong12345678
MD5结果:   e10adc3949ba59abbe56e057f20f883e...(实际值)

加盐的目的:防止彩虹表攻击。如果直接 MD5 存密码,攻击者拿常用密码的 MD5 对照表就能反推。加了盐之后,即使两个用户密码相同,存储的密文也不同。

当前方案的不足

问题 说明
固定盐值 所有用户用同一个盐,同一密码的加密结果还是一样的
MD5 已不够安全 MD5 已被证明存在碰撞漏洞,不应用于密码加密

生产建议 :使用 BCryptSCrypt ,它们自带随机盐、可调强度,Spring Security 的 BCryptPasswordEncoder 就很好用:

java 复制代码
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String encryptPassword = encoder.encode(userPassword);
// 验证时
encoder.matches(rawPassword, storedPassword)

七、亮点五:注册即入群 --- 用业务联动提升用户体验

java 复制代码
// ⑥ 新用户自动加入系统群聊
Long userId = user.getId();
UserRoomRelate userRoomRelate = new UserRoomRelate();
userRoomRelate.setRoomId(SYSTEM_ROOM_ID);  // 系统群聊ID=1
userRoomRelate.setUserId(userId);
userRoomRelateService.save(userRoomRelate);

这个细节很多人不会注意到:注册不只是创建用户记录,还自动把用户拉入系统群聊

SYSTEM_ROOM_ID = 1 是一个全局群聊房间,所有注册用户都会被加入。这样的设计有几个好处:

  1. 新用户不空:注册后立刻能看到系统群聊的消息,不会面对一个空白的聊天界面
  2. 降低流失率:用户第一天就能互动,留存率比"空房间"高得多
  3. 系统公告渠道:运营可以通过系统群聊发通知、活动等

实现方式

UserRoomRelate 是用户和房间的关联表:

java 复制代码
// UserRoomRelate.java
@TableName(value = "user_room_relate")
public class UserRoomRelate {
    private Long id;
    private Long roomId;   // 房间ID
    private Long userId;   // 用户ID
    private Date createTime;
    private Date updateTime;
}

注册成功后,往这张表插入一条记录,用户就自动拥有了系统群聊的成员身份。


八、完整调用链路图

复制代码
前端 POST /api/user/register
        │
        ↓
UserRegisterRequest (DTO)
  ├── userAccount
  ├── userPassword
  └── checkPassword
        │
        ↓
UserController.userRegister()
  ├── 粗粒度空值校验
  └── 调用 userService.userRegister()
        │
        ↓
UserServiceImpl.userRegister()
  ├── ① 细粒度参数校验(长度、一致性)
  ├── ② synchronized(userAccount.intern()) 加锁
  ├── ③ UserMapper.selectCount() 查重
  ├── ④ MD5+SALT 加密密码
  ├── ⑤ UserMapper.save() 保存用户
  │     └── UserConstant.DEFAULT_AVATAR     默认头像
  │     └── UserConstant.DEFAULT_NICKNAME   默认昵称+时间戳
  ├── ⑥ UserRoomRelateService.save() 加入系统群聊
  │     └── SystemConstants.SYSTEM_ROOM_ID  系统群聊ID
  └── 返回 userId
        │
        ↓
BaseResponse<Long> → 前端收到用户ID

九、涉及的所有类

类名 包路径 作用
UserRegisterRequest model/dto/user/ 注册请求体,接收前端参数
UserController controller/ REST 接口入口,路由 /user/register
UserService service/ 用户服务接口,定义业务方法签名
UserServiceImpl service/impl/ 用户服务实现,注册的核心逻辑全在这里
UserMapper mapper/ MyBatis-Plus Mapper,操作 user
User model/entity/ 用户实体类,映射 user 表字段
UserRoomRelate model/entity/ 用户-房间关联实体
UserRoomRelateService service/ 用户-房间关联服务
SystemConstants constant/ SALT 盐值、SYSTEM_ROOM_ID
UserConstant constant/ 默认头像、默认昵称、角色常量
ErrorCode common/ 错误码枚举(PARAMS_ERROR、SYSTEM_ERROR 等)
BusinessException exception/ 自定义业务异常,统一错误处理
BaseResponse common/ 统一响应包装类
ResultUtils common/ 响应工具类,快速构建 success/error

十、总结

一个注册接口,50 行核心代码,4 个技术亮点:

亮点 做法 学习价值 生产改进方向
并发防重 synchronized(userAccount.intern()) 字符串常量池锁,精细粒度 → Redis 分布式锁
查重防脏 先 selectCount 再 insert 先查后写+锁的组合 → 数据库唯一索引兜底
密码加密 MD5 + 固定盐值 加盐是基本功 → BCrypt 随机盐
注册入群 save + UserRoomRelate 业务联动,用户体验优先 → 事件驱动解耦

注册流程是 IM 系统的第一个用户触点,写得扎实,后面的登录、聊天才有基础。希望本文对你在做类似项目时有参考价值。


项目地址https://github.com/lhccong/we-go(后端)

技术栈:Spring Boot 2.7.2 + MyBatis-Plus + Sa-Token + Netty + Redis

相关推荐
Yingjun Mo6 小时前
3. Meta-Harness:模型基座外壳的端到端优化
人工智能·算法
Agent产品评测局6 小时前
标准化产品vs定制开发,制造业自动化方案选型横评:2026工业智能体落地深度指南
运维·人工智能·ai·chatgpt·自动化
放下华子我只抽RuiKe56 小时前
React 从入门到生产(一):JSX 与组件思维
前端·javascript·人工智能·pytorch·深度学习·react.js·前端框架
QYR_Jodie6 小时前
全电动注塑机械市场深度研判:36.13亿美元赛道,节能化转型如何驱动精密制造升级?
人工智能·市场报告
RSTJ_16256 小时前
PYTHON+AI LLM DAY FIFITY
人工智能·深度学习
逻辑君6 小时前
物理生物学研究报告【20260007】
人工智能·算法
weixin_446260856 小时前
终极工程指南:llama.cpp 本地AI部署手册 (2026)
人工智能·llama
2401_860319526 小时前
我把游戏策划桌搬进了 AI Agent:一次用 JiuwenSwarm 做创意协作的实验
人工智能·游戏策划
qqqweiweiqq6 小时前
Jetson Orin nx 无法train pi0
人工智能·python·深度学习