day30 - 黑马点评短信登录与Session实现
一、项目导入
1.1 黑马点评项目结构
黑马点评(hm-dianping)是一个基于 Spring Boot 的点评类项目,本次学习从导入项目开始。项目采用分层架构,核心包结构如下:
1 | com.hmdp |
1.2 短信登录的整体流程
短信登录分为三步:
- 发送短信验证码:用户提交手机号,后台校验格式后生成 6 位验证码,存入 Session。
- 验证码登录:用户提交手机号和验证码,后台从 Session 取出验证码比对,一致则根据手机号查询用户,不存在则自动创建,最后将用户信息存入 Session。
- 校验登录状态:后续请求携带 Cookie 中的
JSESSIONID,后台通过拦截器从 Session 取出用户信息,未登录则拦截。
1 | 发送验证码 → 验证码登录(查用户/创建用户)→ 拦截器校验登录状态 |
二、基于 Session 实现短信登录
2.1 对 Session 的理解
每个 Tomcat 服务器维护一个 Session 池。当浏览器发起请求并调用 session.setAttribute(key, value) 时:
- 服务器为该浏览器生成一个 SessionID;
- 创建一个 Session 对象,将字段名与字段值存入其中;
- 将 SessionID 通过 Cookie 返回给浏览器;
- 下次请求时浏览器携带该 SessionID,服务器据此找到对应的 Session 对象。
Session 的核心作用是在无状态的 HTTP 协议下保存用户的登录状态。
2.2 发送短信验证码
接口:POST /user/code,参数为手机号。实现位于 UserServiceImpl.sendCode:
1 |
|
关键点:
RegexUtils.isPhoneInvalid校验手机号格式,不合法直接返回错误;RandomUtil.randomNumbers(6)来自 Hutool,生成 6 位随机数字;验证码以
"code"为 key 存入 Session,供登录时比对。
2.3 验证码登录
接口:POST /user/login,参数为 LoginFormDTO(包含 phone、code、password)。实现位于 UserServiceImpl.login:
1 |
|
登录逻辑:校验手机号 → 校验验证码 → 查询用户 → 不存在则创建 → 存入 Session。
2.4 自动创建用户
createUserWithPhone 方法在用户不存在时自动创建账号:
1 | private User createUserWithPhone(String phone) { |
昵称随机生成,格式为 user_ + 10 位随机字符串。
2.5 MyBatis-Plus 快捷操作
本项目使用 MyBatis-Plus,无需编写 Mapper 层的 SQL,直接通过 Service 层的链式方法操作数据库:
| 方法 | 作用 |
|---|---|
query().eq("phone", phone).one() |
条件查询单条记录 |
save(user) |
插入一条记录 |
UserServiceImpl 继承 ServiceImpl<UserMapper, User>,从而获得 query()、save() 等方法。
2.6 @TableId 主键自增
User 实体的主键使用 @TableId 注解,声明主键字段和自增策略:
1 |
|
IdType.AUTO 表示使用数据库自增主键。调用 save(user) 后,数据库生成的主键 id 会回填到 user 对象中,后续可以直接通过 user.getId() 获取。
@TableId的type属性还有ASSIGN_ID(雪花算法)、INPUT(手动输入)等取值,本项目使用数据库自增。
三、登录校验:拦截器与 ThreadLocal
3.1 拦截器实现登录校验
LoginInterceptor 实现 HandlerInterceptor,在请求进入 Controller 前拦截:
1 |
|
拦截逻辑:从 Session 取 user → 为空返回 401 拦截 → 不为空则存入 ThreadLocal 并放行。
3.2 ThreadLocal 保存用户信息
UserHolder 封装了基于 ThreadLocal 的用户存取:
1 | public class UserHolder { |
为什么用 ThreadLocal:Tomcat 从线程池中取一个线程处理每个请求,请求结束后线程归还线程池。ThreadLocal 以当前线程为 key 存储数据,保证每个线程只能访问自己的那份用户信息,实现线程隔离。Controller 中通过 UserHolder.getUser() 获取当前登录用户。
注意:必须在
afterCompletion中调用removeUser()清除 ThreadLocal,否则线程复用会导致数据错乱和内存泄漏。
3.3 注册拦截器
MvcConfig 中注册拦截器,并放行不需要登录的路径:
1 |
|
/user/code 和 /user/login 必须放行,否则用户无法获取验证码和登录。
四、UserDTO 隐藏敏感信息
4.1 为什么用 UserDTO
User 实体包含 password 等敏感字段,直接存入 Session 并返回前端存在安全风险。UserDTO 只保留必要字段:
1 |
|
使用 UserDTO 的好处:
避免敏感信息返回前端:不暴露 password 等字段;
节省内存:只传递业务需要的字段。
4.2 User 转 UserDTO
项目中有两种对象拷贝方式,注意区分:
| 方式 | 来源 | 写法 |
|---|---|---|
BeanUtil.copyProperties |
Hutool | BeanUtil.copyProperties(user, UserDTO.class) |
BeanUtils.copyProperties |
Spring | BeanUtils.copyProperties(user, userDTO) |
本项目拦截器中使用 Hutool 的 BeanUtil:
1 | UserHolder.saveUser(BeanUtil.copyProperties((User) user, UserDTO.class)); |
注意
BeanUtil(Hutool)和BeanUtils(Spring)名字相似,前者的第二个参数可以直接传目标类的Class,后者需要传入目标对象实例。
五、Session 共享问题
5.1 问题描述
在集群部署时,每台 Tomcat 服务器的 Session 是独立的:
1 | 用户请求 → 负载均衡 → Tomcat1(有 session) |
用户第一次请求落到 Tomcat1 并在其 Session 中保存了登录信息,第二次请求可能被负载均衡分发到 Tomcat2,而 Tomcat2 上没有该用户的 Session,导致登录校验失败。
5.2 传统方案:Session 拷贝
早期通过 Session 拷贝实现共享:任意一台服务器的 Session 修改时,同步给其他 Tomcat。但存在两个问题:
- 每台服务器都保存完整的 Session 数据,服务器压力过大;
- Session 拷贝存在延迟,可能导致短时间内数据不一致。
5.3 解决方案:用 Redis 代替 Session
Redis 数据本身是共享存储的,多台 Tomcat 都访问同一个 Redis,天然解决 Session 共享问题。Redis 实现登录的具体代码明日再学。
今日总结
| 模块 | 核心要点 |
|---|---|
| 项目结构 | 黑马点评采用 Controller → Service → Mapper 分层,集成 MyBatis-Plus |
| 短信登录 | 发送验证码(存 Session)→ 验证码登录(查/建用户,存 Session)→ 拦截器校验 |
| Session 原理 | 每台 Tomcat 有 Session 池,通过 SessionID(Cookie)关联浏览器与 Session 对象 |
| MyBatis-Plus | 继承 ServiceImpl 获得 query()、save() 等方法,无需写 Mapper SQL |
@TableId |
IdType.AUTO 声明自增主键,save 后主键回填到实体 |
| 登录校验 | LoginInterceptor 从 Session 取用户,未登录返回 401;已登录存入 ThreadLocal |
| ThreadLocal | 以当前线程为 key 存储数据,实现线程隔离;请求结束必须 remove 防泄漏 |
| UserDTO | 只保留 id、nickName、icon,隐藏 password 等敏感信息,节省内存 |
| 对象拷贝 | Hutool BeanUtil.copyProperties(user, UserDTO.class) 与 Spring BeanUtils 区分 |
| Session 共享 | 集群下每台 Tomcat Session 独立;传统拷贝有压力和延迟问题;用 Redis 代替可解决 |