相比传统账号密码登录,短信登录具有无需记忆密码、操作便捷、安全性较高等优点。本文将以黑马点评(hm-dianping)项目为背景,详细讲解如何基于 Session 实现一套完整的短信验证码登录功能。内容包括业务流程设计、核心代码实现、登录状态拦截以及 Session 方案的局限性分析。
一、业务流程与设计思路
在动手编码之前,我们先理清基于 Session 的短信登录整体流程。如下图所示:
+--------+ +-------------+ +-----------------+
| 前端 | 请求 | 后端接口 | 操作 | Session |
+--------+ <--> +-------------+ <--> +-----------------+
| | |
| 1. 提交手机号 | 2. 生成验证码 | 3. 存储验证码
| 4. 提交手机号+验证码 | 5. 校验验证码、查询/创建用户 | 6. 读取验证码,存储用户信息
| 7. 后续请求携带Cookie | 8. 拦截器校验登录状态 | 9. 获取用户信息
详细步骤分解如下:
-
发送验证码:用户输入手机号,点击"获取验证码"。后端校验手机号格式后,生成随机验证码,并将验证码保存到当前会话(Session)中,最后通过短信通道发送给用户(开发阶段通常直接打印到控制台)。
-
登录/注册:用户输入手机号和收到的验证码,点击登录。后端从 Session 中取出之前保存的验证码进行比对。如果一致,则根据手机号查询用户;若用户不存在,则自动创建一个新用户(即注册)。最后将用户信息存入 Session,标记用户已登录。
-
登录状态校验 :对于需要登录才能访问的接口(如个人中心、下单等),通过拦截器 拦截请求,从 Session 中获取用户信息。若存在,则将用户信息存入
ThreadLocal供后续业务使用,并放行请求;否则直接返回未授权状态。
整个流程的核心是依赖 Tomcat 的 Session 机制 ,即服务器为每个浏览器会话创建一个唯一的 JSESSIONID,并通过 Cookie 在客户端和服务端之间传递,从而实现会话的保持。
二、环境准备与工具类
黑马点评项目基于 Spring Boot,我们首先确保引入了必要的依赖。这里使用 hutool 工具包简化验证码生成、Bean 复制等操作。
XML
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
此外,项目中通常包含一个 RegexUtils 工具类,用于校验手机号格式(例如使用正则 ^1[3-9]\\d{9}$)。
三、核心功能实现
3.1 发送短信验证码
Controller 层:接收手机号,调用 Service 处理。
java
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private IUserService userService;
@PostMapping("/code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
return userService.sendCode(phone, session);
}
}
Service 层实现:
java
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public Result sendCode(String phone, HttpSession session) {
// 1. 校验手机号格式
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误!");
}
// 2. 生成6位随机验证码
String code = RandomUtil.randomNumbers(6);
// 3. 保存验证码到 Session(可设置过期时间,但 Session 本身可配置超时)
session.setAttribute("code", code);
// 4. 模拟发送短信(实际中调用短信服务商 API)
log.debug("短信验证码:{}", code);
return Result.ok();
}
}
说明 :验证码存入 Session 的 key 为 "code",后续登录时会从中取出比对。注意 Session 的默认过期时间为 30 分钟,一般验证码有效期较短(如 5 分钟),但基于 Session 管理过期时间较为复杂,通常需要结合定时任务或额外字段处理。这也是 Session 方案的一个小缺陷。
3.2 短信验证码登录/注册
登录表单 DTO:
java
@Data
public class LoginFormDTO {
private String phone;
private String code;
}
Controller 层:
java
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session) {
return userService.login(loginForm, session);
}
Service 层实现:
java
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
String code = loginForm.getCode();
// 1. 校验手机号格式
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误!");
}
// 2. 从 Session 获取验证码并校验
String cacheCode = (String) session.getAttribute("code");
if (cacheCode == null || !cacheCode.equals(code)) {
return Result.fail("验证码错误!");
}
// 3. 根据手机号查询用户
User user = query().eq("phone", phone).one();
// 4. 用户不存在,自动注册
if (user == null) {
user = createUserWithPhone(phone);
}
// 5. 保存用户信息到 Session(使用 UserDTO 隐藏敏感字段)
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
session.setAttribute("user", userDTO);
// 可删除验证码,防止重复使用
session.removeAttribute("code");
return Result.ok(userDTO);
}
// 辅助方法:创建新用户
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
// 生成默认昵称,如 "user_随机字符串"
user.setNickName("user_" + RandomUtil.randomString(10));
save(user);
return user;
}
关键点:
-
使用
UserDTO代替完整的User实体存入 Session,避免敏感信息(如密码、手机号)暴露。UserDTO通常只包含id、nickName、icon等字段。 -
登录成功后,移除 Session 中的验证码,防止同一验证码被重复使用。
-
返回
UserDTO给前端,前端可用于展示用户信息。
3.3 登录状态校验拦截器
为了统一处理需要登录的接口,我们使用 拦截器 + ThreadLocal 实现。首先定义一个 UserHolder 工具类,用于在当前线程中保存和获取用户信息。
java
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO user) {
tl.set(user);
}
public static UserDTO getUser() {
return tl.get();
}
public static void removeUser() {
tl.remove();
}
}
然后编写拦截器 LoginInterceptor:
java
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 从 Session 中获取用户
HttpSession session = request.getSession(false); // 不创建新 Session
if (session == null) {
response.setStatus(401);
return false;
}
UserDTO user = (UserDTO) session.getAttribute("user");
// 2. 用户不存在,未登录,返回 401
if (user == null) {
response.setStatus(401);
return false;
}
// 3. 用户存在,保存到 ThreadLocal
UserHolder.saveUser(user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 请求结束后移除用户,防止内存泄漏
UserHolder.removeUser();
}
}
说明:
-
request.getSession(false)表示如果当前请求没有关联 Session,则返回 null,避免创建无用 Session。 -
未登录时直接设置 HTTP 状态码 401(未授权),前端可据此跳转登录页。
-
将用户信息存入
ThreadLocal,后续业务代码(如 Controller、Service)可以直接通过UserHolder.getUser()获取当前登录用户,无需再操作 Session。
最后,在配置类中注册拦截器,并设置放行路径:
java
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/code",
"/user/login",
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot"
);
}
}
3.4 在业务中使用当前登录用户
以查询当前用户信息为例:
java
@GetMapping("/me")
public Result me() {
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
这样既简洁又安全,业务代码无需关心 Session 的获取细节。
四、基于 Session 方案的局限性
以上实现完整演示了基于 Session 的短信登录。但在实际生产环境,尤其是分布式集群部署时,该方案会遇到以下问题:
4.1 Session 共享问题
当后端服务部署在多台 Tomcat 上,且前端通过 Nginx 进行负载均衡时,用户的第一次请求可能落在 Server1,Session 创建在 Server1;第二次请求如果落在 Server2,Server2 无法获取 Server1 的 Session,就会误认为用户未登录,导致登录状态丢失。
4.2 传统解决方案的不足
早期常用的解决方案是 Session 黏着(Sticky Session) 或 Session 复制:
-
Session 黏着:通过 Nginx 配置,将同一用户的请求始终转发到同一台服务器。但一旦该服务器宕机,会话即丢失,且负载均衡能力下降。
-
Session 复制:服务器之间同步 Session 数据(如 Tomcat 的 DeltaManager)。但这种方式会带来大量的网络开销和数据冗余,集群规模大时性能急剧下降。
4.3 现代解决方案:引入 Redis
为了解决上述问题,业界普遍采用 外部集中式存储 的方式,即使用 Redis 统一管理登录状态。具体思路:
-
发送验证码时,将验证码存入 Redis,并设置过期时间(如 2 分钟)。
-
登录成功后,生成一个随机的 Token(如 UUID),将用户信息存入 Redis,并将 Token 返回给前端。前端后续请求在请求头中携带 Token。
-
拦截器从请求头获取 Token,查询 Redis 获取用户信息,并实现 Token 续期。
这种方式不仅解决了 Session 共享问题,还能更好地控制过期时间、支持分布式环境。黑马点评项目后续也正是采用 Redis 重构登录功能。
五、总结
本文以黑马点评项目为基础,详细讲解了基于 Session 的短信验证码登录实现。从业务流程出发,逐步完成了验证码发送、登录注册、拦截器校验等核心代码,并介绍了如何使用 ThreadLocal 优化用户信息传递。
虽然 Session 方案简单易懂,适合单体应用或学习阶段,但在分布式环境下存在明显的共享问题。通过分析其局限性,我们引出了基于 Redis 的改进方向,为后续学习打下基础。
参考资料:
-
黑马点评项目源码
-
Hutool 官方文档
-
Spring 拦截器与 ThreadLocal 使用指南