黑马点评短信登录01_session_sms_login

黑马点评短信登录一:浏览器到底是怎么找到服务器 Session 的?

本文来自我学习黑马点评 Redis 实战篇第 1 章「短信登录」时的整理。

这一章表面上是在写短信验证码登录,实际是在学习一个后端项目最基础、也最容易混乱的问题:用户登录后,服务端到底靠什么记住"你是谁"?

本篇先讲第一阶段:基于 Session 的短信登录。重点不是死记硬背代码,而是把 CookieJSESSIONIDSession、登录拦截器、ThreadLocal 这些概念串起来。


1. 为什么短信登录这一章并不简单

刚开始看短信登录时,很容易觉得它只是几个普通接口:

text 复制代码
1. 用户输入手机号
2. 后端发送验证码
3. 用户输入验证码
4. 后端校验验证码
5. 登录成功

如果只看业务页面,好像确实不难。

但真正写后端代码时,会遇到一串问题:

text 复制代码
验证码生成后,要存在哪里?
用户下一次登录请求过来时,后端怎么拿到之前那个验证码?
登录成功后,后端怎么记住这个用户已经登录?
后续请求访问 /user/me 时,后端怎么知道当前用户是谁?
拦截器为什么要把用户存到 ThreadLocal?
ThreadLocal 是不是登录态本身?

我一开始最卡的地方是这个:

服务器里可能有很多个 Session。用户第一次请求发送验证码时,验证码存到了某个 Session 里。那用户第二次请求登录时,服务器怎么知道应该去找刚才那个 Session?

这个问题如果不想明白,后面学 Redis token、登录拦截器、双拦截器刷新登录态时都会很乱。

所以这篇文章先围绕 Session 版短信登录,把这条链路讲清楚。


2. 本篇文章解决什么问题

本文主要讲清楚 6 件事:

text 复制代码
1. 短信验证码登录的基本流程是什么。
2. Session 版发送验证码怎么实现。
3. Session 版登录怎么实现。
4. 浏览器为什么能找到服务端之前创建的那个 Session。
5. 登录拦截器为什么要配合 ThreadLocal。
6. Session 方案为什么会引出后面的 Redis token 方案。

先给出一句话总结:

Session 版登录的本质是:服务端把验证码和用户信息存在自己的 Session 里,浏览器通过 Cookie 携带 JSESSIONID,让服务端在下一次请求时找到同一个 Session。

这句话很重要,后面所有代码都围绕它展开。


3. 黑马点评第一章的登录接口有哪些

在项目里,短信登录相关接口主要在 UserController 中。

最终版项目里的 Controller 大概是这样:

java 复制代码
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private IUserService userService;

    @PostMapping("/code")
    public Result sendCode(@RequestParam("phone") String phone) {
        return userService.sendCode(phone);
    }

    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm) {
        return userService.login(loginForm);
    }

    @GetMapping("/me")
    public Result me() {
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }
}

这里有三个重点接口:

text 复制代码
POST /user/code
发送短信验证码。

POST /user/login
提交手机号和验证码,完成登录。

GET /user/me
获取当前登录用户信息。

不过要注意:当前源码已经演进到了 Redis token 版本,方法签名里已经没有 HttpSession 参数。

而本篇先讲讲义前半段的 Session 版本。因为学习顺序上,只有先明白 Session 版,后面才知道为什么要换成 Redis。


4. 短信登录的完整业务流程

短信登录可以拆成两个阶段。

第一阶段:发送验证码。

text 复制代码
用户输入手机号
    ↓
后端校验手机号格式
    ↓
生成 6 位验证码
    ↓
保存验证码
    ↓
发送验证码

第二阶段:提交验证码登录。

text 复制代码
用户输入手机号和验证码
    ↓
后端校验手机号格式
    ↓
后端取出之前保存的验证码
    ↓
比较用户提交的验证码是否一致
    ↓
一致则根据手机号查询用户
    ↓
用户不存在则自动注册
    ↓
保存登录用户信息
    ↓
登录成功

这里的关键点是:

验证码不是只生成出来就结束了,它必须先被服务端保存起来。否则用户下一次提交验证码时,后端根本不知道该和谁比较。

在 Session 版实现中,验证码先存在 Session 里。


5. 什么是 Session

先用最朴素的话说:

Session 是服务端为某个客户端保存的一份会话数据。

比如用户 A 来访问网站,Tomcat 可以给用户 A 创建一个 Session:

text 复制代码
sessionA:
  code = 123456
  user = 用户A

用户 B 来访问网站,Tomcat 可以给用户 B 创建另一个 Session:

text 复制代码
sessionB:
  code = 888888
  user = 用户B

这些 Session 数据默认保存在服务端,也就是 Tomcat 的内存中。

它不是浏览器里的东西。

浏览器真正拿到的是一个 Session 的编号,最常见就是:

text 复制代码
JSESSIONID

这就像去餐厅存包:

text 复制代码
服务员把你的包放在柜子里。
你手里拿到的是一个号码牌。
下一次你拿号码牌回来,服务员根据号码牌找到对应柜子。

在登录场景里:

text 复制代码
Session 数据:存在服务端
JSESSIONID:发给浏览器
Cookie:浏览器保存 JSESSIONID 的地方

Cookie 是浏览器保存的一小段数据。

服务端可以通过响应头告诉浏览器:

text 复制代码
请你保存一个 Cookie:
JSESSIONID=abc123

浏览器保存后,后续访问同一个网站时,会自动在请求头里带上:

text 复制代码
Cookie: JSESSIONID=abc123

这样服务端就知道:

text 复制代码
这次请求带来的 JSESSIONID 是 abc123。
那我就去找 id 为 abc123 的 Session。

所以 Cookie 和 Session 的关系可以这样理解:

text 复制代码
Cookie 是浏览器保存凭证的地方。
Session 是服务端保存会话数据的地方。
JSESSIONID 是连接 Cookie 和 Session 的那个编号。

Cookie、JSESSIONID、Session 的关系图

浏览器第一次访问服务端
Tomcat 创建 Session
SessionId = abc123
响应头 Set-Cookie: JSESSIONID=abc123
浏览器保存 Cookie
下次请求自动携带 Cookie
服务端读取 JSESSIONID=abc123
找到对应 Session

这张图就是解开 Session 疑惑的关键。


7. 疑惑:服务器有很多 Session,怎么知道该用哪一个?

这就是我学习时最想问的问题。

假设用户先请求:

text 复制代码
POST /user/code?phone=13800000000

后端生成验证码:

text 复制代码
123456

然后执行:

java 复制代码
session.setAttribute("code", code);

验证码被放进了当前 Session。

问题来了:用户几秒后再请求:

text 复制代码
POST /user/login

后端执行:

java 复制代码
Object cacheCode = session.getAttribute("code");

这个 session 为什么还是刚才那个?

答案是:

因为第一次请求时,Tomcat 会把这个 Session 对应的 JSESSIONID 通过 Cookie 发给浏览器。第二次请求时,浏览器会自动带上这个 Cookie。Tomcat 根据 JSESSIONID 找回同一个 Session。

也就是说,并不是浏览器把验证码传回来了。

浏览器传回来的只是:

text 复制代码
JSESSIONID

服务端根据这个编号找到之前的 Session,然后从 Session 里取出验证码。

两次请求的完整链路

Session存储 Tomcat/后端 浏览器 Session存储 Tomcat/后端 浏览器 POST /user/code?phone=手机号 校验手机号并生成验证码 创建/获取 Session,保存 code Set-Cookie: JSESSIONID=abc123 POST /user/login,Cookie: JSESSIONID=abc123 根据 abc123 找到同一个 Session 返回 Session 中的 code 比较用户输入验证码和 Session 中验证码 登录结果

这也是为什么同一个浏览器窗口里,先发验证码再登录可以对上。


8. Session 版发送验证码代码

讲义中的 Session 版发送验证码代码大致如下:

java 复制代码
@Override
public Result sendCode(String phone, HttpSession session) {
    // 1.校验手机号
    if (RegexUtils.isPhoneInvalid(phone)) {
        return Result.fail("手机号格式错误!");
    }

    // 2.生成验证码
    String code = RandomUtil.randomNumbers(6);

    // 3.保存验证码到 session
    session.setAttribute("code", code);

    // 4.发送验证码
    log.debug("发送短信验证码成功,验证码:{}", code);

    return Result.ok();
}

这段代码的核心不是"生成随机数",而是这句:

java 复制代码
session.setAttribute("code", code);

它表示:

text 复制代码
把当前用户这次会话里的验证码保存起来。

逐行解释

先校验手机号:

java 复制代码
if (RegexUtils.isPhoneInvalid(phone)) {
    return Result.fail("手机号格式错误!");
}

这一步是为了避免明显非法的手机号继续往后走。

然后生成 6 位验证码:

java 复制代码
String code = RandomUtil.randomNumbers(6);

这里用的是 Hutool 的工具方法,意思是生成一个长度为 6 的数字字符串,例如:

text 复制代码
482913

然后保存到 Session:

java 复制代码
session.setAttribute("code", code);

setAttribute 可以理解成往 Session 这个小 Map 里放数据:

text 复制代码
key   = code
value = 482913

最后日志打印:

java 复制代码
log.debug("发送短信验证码成功,验证码:{}", code);

真实生产环境应该调用短信服务商发送验证码。学习项目里为了方便测试,就把验证码打印到日志里。

发送验证码流程图



请求 /user/code
获取手机号
手机号格式是否正确?
返回手机号格式错误
生成 6 位验证码
保存到 Session: code
日志打印验证码
返回成功


9. Session 版登录代码

讲义中的 Session 版登录代码大致如下:

java 复制代码
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1.校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        return Result.fail("手机号格式错误!");
    }

    // 2.校验验证码
    Object cacheCode = session.getAttribute("code");
    String code = loginForm.getCode();
    if (cacheCode == null || !cacheCode.toString().equals(code)) {
        return Result.fail("验证码错误");
    }

    // 3.根据手机号查询用户
    User user = query().eq("phone", phone).one();

    // 4.用户不存在则创建
    if (user == null) {
        user = createUserWithPhone(phone);
    }

    // 5.保存用户信息到 session 中
    session.setAttribute("user", user);

    return Result.ok();
}

这段代码的主线是:

text 复制代码
校验手机号
    ↓
从 Session 取出验证码
    ↓
比较验证码
    ↓
查用户
    ↓
用户不存在就注册
    ↓
把用户保存到 Session

为什么登录时能从 Session 取到验证码

代码里是这样取的:

java 复制代码
Object cacheCode = session.getAttribute("code");

这个 session 是 Tomcat 根据请求里的 JSESSIONID 找到的。

所以本质链路是:

text 复制代码
浏览器请求头 Cookie 里带 JSESSIONID
    ↓
Tomcat 根据 JSESSIONID 找到 Session
    ↓
Controller / Service 方法拿到这个 Session
    ↓
代码通过 session.getAttribute("code") 取验证码

不是后端凭空知道用户是谁,而是请求里带了一个能找到 Session 的编号。

为什么登录成功后还要把 user 放进 Session

验证码只是用来证明:

text 复制代码
这个人能拿到这个手机号收到的验证码。

登录成功后,后续接口还需要知道:

text 复制代码
当前登录用户是谁?

所以代码要执行:

java 复制代码
session.setAttribute("user", user);

这样后续请求就可以通过同一个 Session 拿到用户信息:

java 复制代码
Object user = session.getAttribute("user");

如果能拿到,就说明这个用户之前已经登录过。


10. 自动注册:为什么用户不存在时要创建用户

短信验证码登录通常不要求用户先单独注册账号。

用户第一次用手机号登录时,如果数据库里没有这个手机号对应的用户,就直接创建一个新用户。

代码是:

java 复制代码
User user = query().eq("phone", phone).one();

if (user == null) {
    user = createUserWithPhone(phone);
}

创建用户的方法类似这样:

java 复制代码
private User createUserWithPhone(String phone) {
    User user = new User();
    user.setPhone(phone);
    user.setNickName("user_" + RandomUtil.randomString(10));
    save(user);
    return user;
}

这个方法做了三件事:

text 复制代码
1. 创建 User 对象。
2. 设置手机号和随机昵称。
3. 保存到数据库。

所以短信登录的用户体验是:

text 复制代码
老用户:验证码正确 -> 直接登录
新用户:验证码正确 -> 自动注册 -> 登录

11. Session 版登录完整流程图

把发送验证码和登录合起来,可以画成这样:




用户输入手机号
请求 /user/code
校验手机号格式
生成验证码
验证码保存到 Session
返回成功并通过 Cookie 维护 JSESSIONID
用户输入手机号和验证码
请求 /user/login
浏览器携带 JSESSIONID
Tomcat 找到同一个 Session
从 Session 取出 code
验证码是否一致?
返回验证码错误
根据手机号查询用户
用户是否存在?
创建新用户
得到用户信息
保存 user 到 Session
返回登录成功

这张图里最关键的是:

text 复制代码
JSESSIONID 负责让两次请求找到同一个 Session。

12. 登录成功后,为什么还需要登录拦截器

登录成功只是第一步。

真正的项目里,不是所有接口都允许未登录用户访问。

比如:

text 复制代码
GET /user/me
POST /blog
POST /user/sign

这些接口都需要知道当前用户是谁。

如果用户没有登录,就应该拦截。

所以项目里需要一个登录拦截器:

text 复制代码
请求进入 Controller 之前,先判断用户是否已经登录。

如果没登录:

text 复制代码
返回 401,阻止继续访问。

如果已登录:

text 复制代码
把用户信息保存起来,放行请求。

13. Session 版登录拦截器代码

讲义中最开始的登录拦截器类似这样:

java 复制代码
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler
    ) throws Exception {
        // 1.获取 session
        HttpSession session = request.getSession();

        // 2.获取 session 中的用户
        Object user = session.getAttribute("user");

        // 3.判断用户是否存在
        if (user == null) {
            response.setStatus(401);
            return false;
        }

        // 4.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser((User) user);

        // 5.放行
        return true;
    }
}

拦截器的关键判断是:

java 复制代码
Object user = session.getAttribute("user");

如果 Session 里有 user,说明之前登录成功过。

如果没有,说明当前请求没有登录态。

preHandle 返回值是什么意思

preHandle 是 Spring MVC 拦截器在 Controller 方法执行前调用的方法。

它返回 true

text 复制代码
继续执行后面的拦截器和 Controller。

它返回 false

text 复制代码
请求到此结束,不再进入 Controller。

所以未登录时:

java 复制代码
response.setStatus(401);
return false;

意思是:

text 复制代码
告诉前端当前请求未认证,并阻止继续访问业务接口。

14. 为什么要把用户保存到 ThreadLocal

这是另一个非常容易混的点。

拦截器已经从 Session 里拿到了用户:

java 复制代码
Object user = session.getAttribute("user");

那为什么还要执行:

java 复制代码
UserHolder.saveUser((User) user);

原因是:

后面的 Controller 或 Service 可能需要知道当前用户是谁。把用户放到 ThreadLocal 后,同一个请求线程里的后续代码就可以随时通过 UserHolder.getUser() 获取当前用户。

比如后面签到接口会写:

java 复制代码
Long userId = UserHolder.getUser().getId();

点赞、关注、秒杀下单也经常需要:

java 复制代码
当前登录用户 id

如果每个方法都传 HttpSession,代码会很别扭。

所以项目封装了一个 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();
    }
}

ThreadLocal 是什么

简单理解:

ThreadLocal 是线程内部的一份变量副本。不同线程之间互不影响。

Tomcat 处理请求时,通常会从线程池里拿一个线程来执行当前请求。

请求 A 在线程 1 里执行:

text 复制代码
ThreadLocal[线程1] = 用户A

请求 B 在线程 2 里执行:

text 复制代码
ThreadLocal[线程2] = 用户B

这样两个请求互不干扰。

ThreadLocal 不是登录态本身

这一点必须说清楚。

ThreadLocal 不是用来跨请求保存登录状态的。

它只是在一次请求处理过程中,临时保存当前用户。

真正能跨请求识别用户的,在 Session 版里是:

text 复制代码
Cookie 中的 JSESSIONID + 服务端 Session

ThreadLocal 只是请求内部的"当前用户快捷入口"。

ThreadLocal 在请求链路中的位置



浏览器携带 JSESSIONID 请求后端
Tomcat 找到 Session
LoginInterceptor 从 Session 取 user
user 是否存在?
返回 401
UserHolder.saveUser(user)
Controller 执行业务
Service 中通过 UserHolder.getUser() 获取当前用户


15. 为什么请求结束后要 remove

讲义后面会逐步完善 UserHolder.removeUser()

原因是 Tomcat 使用线程池。

线程并不是每次请求结束就销毁,而是会被复用。

比如:

text 复制代码
第一次请求:线程1 处理用户A
第二次请求:线程1 又被拿去处理用户B

如果用户 A 的信息还留在线程 1 的 ThreadLocal 里,后面就可能造成脏数据。

所以请求结束后要清理:

java 复制代码
UserHolder.removeUser();

在最终版项目中,这个清理动作放在 afterCompletion

java 复制代码
@Override
public void afterCompletion(
        HttpServletRequest request,
        HttpServletResponse response,
        Object handler,
        Exception ex
) throws Exception {
    UserHolder.removeUser();
}

可以理解成:

text 复制代码
请求开始时,把当前用户放进 ThreadLocal。
请求结束时,把当前用户从 ThreadLocal 清掉。

这样线程被复用时才不会带着上一个用户的数据继续跑。


16. 为什么不能直接把 User 返回给前端

Session 版刚开始可能会直接把 User 对象保存起来。

但数据库里的 User 实体可能包含一些不应该暴露给前端的字段,比如:

text 复制代码
手机号
密码
创建时间
更新时间
其他敏感字段

所以项目引入了 UserDTO

java 复制代码
@Data
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}

DTO 可以理解成:

专门用于传输给前端或放入登录上下文的安全版用户对象。

它只保留前端需要的信息:

text 复制代码
id
nickName
icon

不把完整 User 暴露出去。

所以后续代码会从:

java 复制代码
session.setAttribute("user", user);

演进为:

java 复制代码
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));

或者在最终 Redis 版本中:

java 复制代码
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);

这个改动看起来小,但它体现的是一个后端开发很重要的习惯:

数据库实体对象不要随便原样暴露给前端。


17. 让登录拦截器生效

写完拦截器后,还要把它注册到 Spring MVC 中。

配置类通常实现 WebMvcConfigurer

java 复制代码
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}

这段配置的意思是:

text 复制代码
默认拦截请求。
但排除一些不需要登录也能访问的路径。

比如:

text 复制代码
/user/code
/user/login

这两个接口一定不能被登录拦截器拦住。

因为用户还没登录时,本来就需要先访问:

text 复制代码
发送验证码
登录

如果把它们也拦住,就会变成:

text 复制代码
想登录 -> 被要求先登录 -> 永远无法登录

这就是典型的拦截器配置错误。


18. Session 版登录的整体请求链路

把所有内容合在一起,Session 版登录链路可以这样理解:




发送验证码请求 /user/code
后端生成验证码
验证码保存到 Session
浏览器保存 JSESSIONID
登录请求 /user/login
浏览器携带 JSESSIONID
Tomcat 找到同一个 Session
从 Session 取验证码
验证码正确?
返回验证码错误
查询或创建用户
用户信息保存到 Session
访问需要登录的接口
浏览器继续携带 JSESSIONID
LoginInterceptor 找到 Session 中 user
user 是否存在?
返回 401
保存到 ThreadLocal
Controller/Service 使用 UserHolder 获取当前用户

这个流程里有三个存储位置:

text 复制代码
浏览器 Cookie:
保存 JSESSIONID。

服务端 Session:
保存验证码 code 和登录用户 user。

ThreadLocal:
一次请求内部临时保存当前用户,方便业务代码获取。

千万不要把这三个混成一个东西。


19. 本篇最容易混淆的几个概念

不是。

Cookie 在浏览器端。

Session 在服务端。

浏览器通过 Cookie 携带 JSESSIONID,服务端通过 JSESSIONID 找到对应 Session。

2. JSESSIONID 是用户信息吗

不是。

JSESSIONID 只是 Session 的编号。

真正的验证码和用户信息存在服务端 Session 里。

3. 验证码是浏览器带回来的吗

用户提交的验证码当然是浏览器传给后端的。

但后端用来对比的那个"正确验证码",不是浏览器带回来的,而是服务端之前保存在 Session 里的。

对比逻辑是:

text 复制代码
用户输入的验证码 vs Session 中保存的验证码

4. ThreadLocal 是登录态吗

不是。

ThreadLocal 只是当前请求线程里的临时变量。

Session 版真正保存登录态的是:

text 复制代码
服务端 Session

ThreadLocal 的作用是让本次请求后续代码更方便地获取当前用户。

5. 为什么不能把 /user/code 和 /user/login 拦截

因为这两个接口本来就是给未登录用户使用的。

如果它们也需要登录后才能访问,用户就无法完成登录。

6. 为什么要用 UserDTO

因为数据库实体 User 可能包含敏感字段。

UserDTO 只保留前端和业务上下文需要的字段,可以避免敏感信息泄露。


20. Session 版方案有什么问题

到这里,Session 版短信登录在单机环境下已经能跑通。

但它有一个非常重要的问题:

Session 默认存在当前 Tomcat 内存里。如果部署多台 Tomcat,就会出现 Session 共享问题。

举个例子。

第一次请求发送验证码:

text 复制代码
浏览器 -> Nginx -> Tomcat A

验证码存到了 Tomcat A 的 Session。

第二次请求登录:

text 复制代码
浏览器 -> Nginx -> Tomcat B

Tomcat B 自己的内存里没有 Tomcat A 的 Session 数据。

于是登录时可能取不到验证码。

这就是 Session 共享问题。

集群下 Session 失效示意图

浏览器发送验证码请求
Nginx
Tomcat A
验证码保存到 Tomcat A 的 Session
浏览器发送登录请求
Nginx
Tomcat B
Tomcat B 找不到 Tomcat A 的 Session 数据
验证码校验失败或登录态丢失

早期可以通过 Session 复制解决:

text 复制代码
Tomcat A 修改 Session 后,同步给 Tomcat B、Tomcat C。

但这个方案有明显缺点:

text 复制代码
1. 每台 Tomcat 都要保存完整 Session 数据,内存压力大。
2. Session 同步可能有延迟。
3. 服务器数量越多,同步成本越高。

所以后面会引出 Redis:

既然多台 Tomcat 都需要共享登录数据,那就把登录数据放到一个所有 Tomcat 都能访问的地方,也就是 Redis。

这就是下一篇要讲的内容:Redis 代替 Session。


21. 面试怎么回答

如果面试官问:短信验证码登录的流程是什么?

可以这样回答:

用户先提交手机号,后端校验手机号格式,通过后生成验证码,并把验证码保存起来。用户再提交手机号和验证码,后端取出之前保存的验证码进行比较。如果验证码正确,就根据手机号查询用户;用户不存在则自动注册;最后保存登录态,返回登录成功。

如果面试官问:Session 版登录里,浏览器怎么找到服务端 Session?

可以这样回答:

Session 数据保存在服务端。浏览器第一次访问时,Tomcat 会创建 Session,并通过响应头把对应的 JSESSIONID 写入浏览器 Cookie。后续请求浏览器会自动携带这个 Cookie,服务端根据 JSESSIONID 找到对应的 Session,因此能取到之前保存的验证码或用户信息。

如果面试官问:ThreadLocal 在登录拦截器中有什么作用?

可以这样回答:

登录拦截器从 Session 中取到当前用户后,会把用户保存到 ThreadLocal。这样在同一次请求的 Controller 或 Service 中,就可以通过 UserHolder.getUser() 获取当前用户,避免层层传递 Session。ThreadLocal 只是当前请求线程内的临时存储,不是跨请求的登录态,请求结束后需要清理。

如果面试官问:为什么要用 UserDTO?

可以这样回答:

数据库中的 User 实体可能包含手机号、密码等敏感字段,不适合直接暴露给前端,也不适合完整放入登录上下文。UserDTO 只保留 id、昵称、头像等必要字段,可以降低敏感信息泄露风险。

如果面试官问:Session 登录方案为什么不适合集群?

可以这样回答:

默认情况下 Session 存在单台 Tomcat 内存中。集群部署时,用户第一次请求可能落到 Tomcat A,第二次请求可能落到 Tomcat B。如果登录态只存在 Tomcat A,Tomcat B 就无法识别用户。Session 复制虽然能解决一部分问题,但会带来内存压力和同步延迟,所以更常见的方案是把登录态放到 Redis 这类共享存储中。


22. 总结

这一篇只讲 Session 版短信登录,但它其实已经把登录态的核心问题讲出来了。

短信登录不是简单地比较验证码,而是要解决两件事:

text 复制代码
1. 验证码生成后,后端下次怎么取出来校验。
2. 用户登录成功后,后端下次怎么知道他是谁。

Session 版的答案是:

text 复制代码
验证码和用户信息存在服务端 Session。
浏览器通过 Cookie 携带 JSESSIONID。
服务端根据 JSESSIONID 找到对应 Session。
拦截器从 Session 中取 user。
ThreadLocal 在一次请求内临时保存当前用户。

最重要的是记住这句话:

Cookie 里不是用户信息,Cookie 里通常只是 JSESSIONID;真正的用户状态存在服务端 Session 中。

不过 Session 方案只适合单机环境。到了多台 Tomcat 集群部署时,Session 共享会成为问题。

所以下一篇就该继续往下走:

text 复制代码
为什么要用 Redis 代替 Session?
token 和 JSESSIONID 到底有什么相似和不同?
Redis 里到底应该存验证码、token,还是用户信息?

这些问题,就是黑马点评短信登录第二阶段的核心。

相关推荐
少司府1 小时前
Tools相关:深入浅出学Git
大数据·c++·git·gitee·github·仓库·分支
Advancer-1 小时前
黑马点评plus --异步秒杀重构升级
java·spring boot·重构·intellij-idea
Dicky-_-zhang1 小时前
服务网格实战:Istio与Linkerd对比选型与落地实践
java·jvm
云烟成雨TD1 小时前
Spring AI Alibaba 1.x 系列【56】SAA Admin 平台功能介绍
java·人工智能·spring
Gauss松鼠会1 小时前
GaussDB(DWS) 资源监控Topsql
java·网络·数据库·算法·oracle·性能优化·gaussdb
夏日听雨眠1 小时前
数据结构(快速排序)
java·数据结构·算法
小碗羊肉1 小时前
【Redis | 第二篇】Jedis&SpringDataRedis
数据库·redis·缓存
字节高级特工1 小时前
C++11(一) 革新:右值引用与移动语义
java·开发语言·c++·人工智能·后端
郝学胜-神的一滴1 小时前
系统设计 012:从用户系统出发,吃透缓存、数据库与高并发设计
java·数据库·python·缓存·php·软件构建