说实话,看到“点评day01,Session实现登录”这个标题,很多刚学JavaWeb的朋友可能会觉得:不就是个登录吗,有什么好写的?但恰恰是这个“简单”的登录功能,把HTTP无状态、Cookie/Session机制、拦截器、线程隔离这些后端基础概念全串起来了。我当年做这个项目的时候,光是一个Session失效的问题就折腾了大半天,后来才发现是对Cookie的Path理解不到位。
这篇就把我做完整个Day01登录模块的完整思路、踩过的坑、排查问题的方法全部摊开讲,代码和配置都是可以直接抄走的级别。看完你不仅能把这个登录功能写得明明白白,以后面试被问到Session原理,也能聊出点别人聊不出的深度。
1. 登录方案选型:为什么一个登录要分这么多天做
1.1 点评项目的登录到底在解决什么问题
先说清楚项目背景。这个“点评”是一个仿大众点评的实战项目,核心业务是商户查询、好友关注、Feed流推送这些,但无论哪个业务,用户得先登录才能玩。Day01要做的就是一个基于Session的短信验证码登录,覆盖三个核心场景:
- 用户输入手机号,点击获取验证码,后端生成验证码并下发(开发环境直接打印到日志或返回给前端)。
- 用户输入手机号+验证码,后端校验通过后创建登录态。
- 用户后续访问需要登录的接口(比如查看自己的订单、点赞等),后端能识别出“这个人是谁”。
看似简单的三个流程,背后涉及的知识点却不少:验证码的生成与校验、Session的存储与读取、以及登录态的校验方式。很多初学者容易忽略的是,HTTP协议本身是无状态的,也就是说服务器天然记不住“你是你”。Session机制就是为了解决这个问题而生的。
1.2 为什么这个项目选择Session而不是Token或JWT
我刚学的时候也疑惑过:现在很多企业项目不都用JWT吗?为什么还要学Session?其实黑马点评这套课程的设计是有讲究的。在单体应用、服务端渲染或者简单的前后端分离场景下,Session依然是非常主流的方案,尤其是企业内部系统、管理后台这类对并发要求不高的项目,Session方案简单直接、代码可读性强、后端可以随时强制注销某个用户,这些特点让它至今没有被淘汰。
要是直接上JWT,你就要额外处理令牌刷新、黑名单、密钥管理这些问题,对新手来说知识跨度太大。而且面试时面试官特别喜欢横向对比“Cookie和Session的区别”“Session和JWT各自的优缺点”,如果只懂其中一个,就很难把这个问题聊透。所以先用Session把登录的基础逻辑跑通,后续再演进到Redis方案或者JWT方案,知识体系才能循序渐进地建立起来。
搭好项目骨架之后,我们要开始写具体的实现代码了。但在动手之前,一定要先把Session的底层机制搞清楚,否则写出来的代码即使能跑,出了问题也很难定位。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Session机制拆解:不是只会调API就完了
2.1 Session和Cookie这对搭档到底怎么配合
很多人把Session和Cookie混为一谈,其实它们是两码事。Cookie是存储在客户端(浏览器)的一小段文本数据,由服务器通过Set-Cookie响应头下发给浏览器,浏览器会在后续请求中自动携带。而Session是存储在服务器端的一段数据,用于记录用户的会话状态,它本身并没有“自动跟随请求”的能力。
这两者是怎么配合的呢?当用户第一次请求服务器时,服务器会创建一个Session对象,并分配一个唯一的Session ID。这个Session ID会通过Cookie下发给浏览器。以后浏览器每次请求都会自动带上这个Cookie,服务器根据Cookie里的Session ID,就能在内存中找到对应的Session数据。
打个比方:Session就像是你在健身房办的会员卡档案,保存在健身房的电脑里,记录着你的剩余次数、私教课进度等信息。而Cookie就好比那张实体会员卡,卡上只有一个卡号。每次你去健身房,掏卡给前台刷一下,前台根据卡号调出你的档案。如果哪天你卡丢了(Cookie失效),前台就不知道你是谁了,但档案本身还留在库里。
2.2 一个完整请求从发出到响应的完整链路
为了让你更直观地理解,我用一个短信验证码登录的请求来演示Session的完整工作流程:
- 前端输入手机号138xxxx8888,点击“获取验证码”,发送请求到后端。
- 后端接口接收手机号,生成一个6位随机验证码,比如
482913,然后把验证码和手机号作为键值对存入Session,同时把验证码返回给前端(实际生产环境是通过短信服务商下发)。 - 服务器在首次使用Session时,会创建一个Session对象,并把Session ID通过
Set-Cookie: JSESSIONID=xxx响应头返回给浏览器。浏览器收到后,把这个Cookie存在本地。 - 用户输入验证码
482913,点击“登录”,前端携带手机号和验证码发起登录请求。同时,浏览器自动在请求头里加上Cookie: JSESSIONID=xxx。 - 后端从请求中获取到JSESSIONID,去内存中找到对应的Session,取出之前存的验证码,和用户提交的验证码比对。
- 比对一致,登录成功。后端在Session中存入当前用户的信息(比如用户ID、手机号),前端跳转到主页。
- 后续用户访问其他需要登录的接口时,浏览器同样自动携带JSESSIONID的Cookie,后端从Session中取出用户信息,判断用户是否已登录。
理解这7步很关键,因为后面所有代码写起来会反反复复用到底层的这个逻辑。如果你对整个过程还比较模糊,建议先在本子上面把这7步画一遍,再开始写代码。
2.3 关于Session的三个认知误区
在带过不少新手之后,我发现大家对Session常见的三个误区:
第一个误区是“Session是存在浏览器的”。这个说法完全错了。Session数据永远在服务器端,浏览器只保存Session ID。你可以把Session理解为服务器内存里的一张表,Session ID就是查表的主键。
第二个误区是“关闭浏览器Session就销毁了”。其实Session是惰性销毁的,它有默认的超时时间,比如Tomcat默认是30分钟,超过这个时间没有访问才会被销毁。关闭浏览器销毁的只是JSESSIONID这个Cookie,等下次再打开浏览器,因为没有Cookie,服务器会认为你是一个新用户,所以看起来像“登录失效”了,但服务器上那个Session对象可能还在内存里躺着,直到超时或被主动清除。
第三个误区是“Session是绝对安全的”。 Session ID如果被黑客截获,黑客就可以冒充你的身份访问系统,这就是常说的“会话劫持”。此外,如果攻击者能在登录前把一个已知的Session ID塞给受害者,等受害者登录成功后,攻击者再用这个ID访问,就能直接冒用受害者的登录态,这叫“Session固定攻击”。后面写代码的时候,我们虽然不会深入做安全防护,但一定要知道这些风险的存在。
理解了Session的工作机制,写代码就有底气多了。下面直接进入实操环节,看看这个短信验证码登录到底怎么写。
3. 短信验证码登录:从接口设计到代码实敲
3.1 前端调用流程和后端接口设计
先明确我们要实现哪些接口。整个登录模块涉及两个核心接口:
- 发送验证码接口:
POST /user/code?phone=xxx,参数是手机号,返回验证码(开发模式直接返回,方便调试)。 - 登录接口:
POST /user/login,参数是手机号和验证码,登录成功会把用户信息保存到Session。
另外还有两个辅助接口:查询当前登录用户GET /user/me,用于登录后回显用户信息;以及登出接口POST /user/logout,用于退出登录,逻辑很简单,直接移除Session中的用户信息就行。
整个前端流程是这样的:用户在登录页输入手机号,点击获取验证码,后端把验证码发到用户手机(开发模式直接打印在后端控制台)。用户把收到的验证码填进去,点击登录,后端校验通过后,前端跳转到首页,同时通过GET /user/me接口拉取用户信息展示在页面上。
3.2 发送验证码接口的完整实现
发送验证码的核心逻辑是:校验手机号格式合法,生成6位随机验证码,把验证码和手机号保存到Session中,然后返回验证码。
具体代码实现如下:
java复制@PostMapping("/code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 1. 校验手机号格式
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误");
}
// 2. 生成6位随机验证码
String code = String.format("%06d", new Random().nextInt(1000000));
// 3. 保存验证码到Session
session.setAttribute("code", code);
session.setAttribute("phone", phone);
// 4. 开发环境直接返回验证码
log.debug("验证码发送成功:{}", code);
return Result.ok(code);
}
这里有几个细节值得注意。第一,验证码的生成我用的是String.format("%06d", new Random().nextInt(1000000)),这样能保证哪怕随机数只有1,也会被格式化成000001,避免用户看到一个只有几位数的验证码导致无法对齐位数的问题。第二,我同时把手机号也存进了Session,这其实是为登录接口准备的——登录时不仅要校验验证码,还要校验当前请求的手机号和当初获取验证码的手机号是否一致,防止张三拿到了自己手机的验证码,却拿李四的手机号去登录。
还有个小细节:理论上生产环境的验证码应该有有效期,比如5分钟内有效,过期作废。Session方案里可以给验证码加一个时间戳来实现,或者定期清理。黑马点评这套课程里因为选择了Session方案,没有刻意做有效期,但面试官问到“验证码过期怎么处理”的时候,你要能答上来。
3.3 登录接口的实现与Session保存用户信息
登录接口的逻辑稍微复杂一点,分四步:
java复制@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session) {
// 1. 校验手机号和验证码是否为空
String phone = loginForm.getPhone();
String code = loginForm.getCode();
if (StrUtil.isBlank(phone) || StrUtil.isBlank(code)) {
return Result.fail("手机号或验证码不能为空");
}
// 2. 从Session获取之前发送的验证码进行比对
Object cacheCode = session.getAttribute("code");
String cachePhone = (String) session.getAttribute("phone");
if (cacheCode == null || !cacheCode.toString().equals(code)) {
return Result.fail("验证码错误");
}
if (!phone.equals(cachePhone)) {
return Result.fail("手机号与获取验证码的手机号不一致");
}
// 3. 根据手机号查询用户,不存在则创建新用户,实现自动注册
User user = userService.query()
.eq("phone", phone)
.one();
if (user == null) {
user = createUserWithPhone(phone);
}
// 4. 保存用户信息到Session,注意这里保存的是UserDTO而不是完整User
session.setAttribute("user", UserDTO.builder()
.id(user.getId())
.icon(user.getIcon())
.nickName(user.getNickName())
.build());
return Result.ok();
}
为什么这里要把完整User对象转成UserDTO再存Session?这是一个安全考量。User对象里可能有密码、手机号等敏感字段,就算Session在服务器端相对安全,也最好不要把不必要的字段暴露出去。更关键的是,后面的业务代码中,我们只需要用到用户的ID、昵称、头像这几个字段,存一个精简的DTO,既能减少内存占用,也避免了后续不小心把整个用户对象返回给前端的风险。
另外注意createUserWithPhone这个方法实现了“手机号+验证码登录,没注册过就自动注册”的逻辑。这种注册登录一体化的设计在现代移动端产品中很常见,用户不需要单独走一遍注册流程,体验上非常友好。具体实现就是创建一个User对象,填充手机号和默认昵称,然后插入数据库。
到这里,登录功能本身已经能跑通了。但项目做到这里只完成了一半——因为目前的后端接口,用户不登录也照样能访问,完全没有起到拦截作用。下一步我们要做的,就是给接口“装上门禁”,让未登录的用户无法访问需要登录才能看的接口。
4. 登录校验拦截器:让后端真正记住“你是谁”
4.1 拦截器校验用户登录态的思路
校验用户是否登录,最简单的思路就是写一个拦截器,在请求到达Controller之前,先检查Session里有没有用户信息。如果有,说明是已登录用户,放行;如果没有,说明未登录,直接返回401状态码和错误信息,让前端跳转到登录页。
这里我要多说一句:很多教程会教你用HandlerInterceptor实现登录校验,但它的一个隐藏大坑是——默认情况下,拦截器只拦截Controller的请求,而放行静态资源。如果你在登录页里引用了CSS、JS、图片等静态资源,并且没有在拦截器配置里显式放行它们,页面样式就会崩掉,因为拦截器会把静态资源的请求也拦下来,返回401。
看下我实际用的拦截器代码:
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);
if (session != null) {
UserDTO user = (UserDTO) session.getAttribute("user");
if (user != null) {
// 2. 用户已登录,保存到ThreadLocal,放行
UserHolder.saveUser(user);
return true;
}
}
// 3. 未登录,返回401状态码
response.setStatus(401);
return false;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 4. 请求处理完毕,删除ThreadLocal中的数据,防止内存泄漏
UserHolder.removeUser();
}
}
这里有一个很容易被忽略但很重要的细节:我在获取Session时用了request.getSession(false),而不是request.getSession()。这两个方法有什么区别?request.getSession()如果当前没有Session,会创建一个新的;而getSession(false)如果没有Session,直接返回null。在登录校验场景下,我们明明是要判断用户是否登录,如果用getSession(),它反而会“好心”地帮未登录用户创建一个新Session,导致永远拦截不到未登录请求。
4.2 ThreadLocal保存用户:这个知识点面试官很爱问
看到这里你可能有个疑问:拦截器把用户信息从Session取出来了,然后放到了ThreadLocal里,为什么不直接放在请求对象或Session里,业务代码要用的时候再去取呢?这不是多此一举吗?
其实这是为了代码的简洁性。想象一下,如果你在Controller里每个方法都写UserDTO user = (UserDTO) request.getSession().getAttribute("user"),代码会非常冗余且不优雅。而ThreadLocal提供了线程级别的变量隔离,同一个请求从进入Controller到返回响应,全程都在同一个线程里执行,所以我们可以在这个线程的任何地方拿到用户信息。
用一句话概括ThreadLocal:它是一个能在“当前线程”内共享数据的容器,这个线程处理完请求后,我们可以安全地清理掉数据。实现的工具类也很简单:
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();
}
}
然后业务代码里就可以直接用UserHolder.getUser()拿到当前登录用户,非常清爽。我在实际项目里,只要涉及当前登录用户的操作,都是直接调用这个工具类,不需要在方法签名里层层传递用户参数。
这里提醒一个经典的坑:使用ThreadLocal一定要在请求结束后清理数据。因为Tomcat的工作线程是复用的,这次请求结束后线程不会销毁,如果你不清理ThreadLocal,下次请求复用这个线程时,ThreadLocal里还残留着上一个用户的数据,就会导致严重的串号问题。所以我在afterCompletion里调用了removeUser(),确保每个请求结束后都干干净净。
4.3 拦截器注册与路径配置:静态资源放行是关键
写好了拦截器,接下来要在配置类里注册它,让Spring知道哪些路径要走拦截器、哪些路径要放行。这里就是静态资源陷阱的高发区。
java复制@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/code", // 放行发送验证码接口
"/user/login", // 放行登录接口
"/shop/**", // 放行商户查询接口
"/shop-type/**", // 放行商户类型接口
"/blog/hot", // 放行热门博客接口
"/upload/**", // 放行文件上传接口
"/**/*.html", // 放行静态页面
"/**/*.css", // 放行样式文件
"/**/*.js" // 放行脚本文件
);
}
}
这个配置的逻辑是:发送验证码和登录接口本身就是为了让用户从“未登录”变成“已登录”,必须放行;商户查询、热门博客这些是游客也能看的,放行;剩下的接口比如用户中心、点赞、关注等,全部要求登录。
静态资源的放行方式有两种,一种是像我这样把常见的静态资源后缀写进去,另一种是把静态资源统一放到某个路径下(比如/static/**),然后直接放行这个路径。我更推荐后者,因为更省事,而且不会出现漏配某个后缀导致页面异常的情况。但黑马点评的前端页面是放在/nginx-1.18.0/html/hmdp/目录下由Nginx托管的,后端返回JSON数据,所以静态资源问题在这个项目里不算突出,但如果你自己写完整的前后端不分离项目,这一点要特别注意。
另外还有一个小细节:拦截器是通过request.getSession(false)拿Session的,而Session ID是放在Cookie里的。如果你在前端调试时关闭了Cookie,或者后端设置了HttpOnly之外的其他属性导致Cookie无法正常写入,那每次请求都拿不到Session,就会一直401。这也是非常经典的“登录成功后立刻401”的排查方向之一。
到这里,一个完整的Session登录功能已经全部实现并能够拦截未登录请求了。但是代码能跑通和代码真正稳定运行是两码事,下面我要分享一下在实际写代码、调试、测试过程中遇到的典型问题和排查思路,这部分内容往往才是真正让你少走弯路的干货。
5. 常见报错与排查技巧实录:这些坑我替你踩过了
5.1 登录相关的经典报错:从现象定位到根因
我在实际开发以及带着学生做这个项目时,遇到过非常多登录相关的报错,其中有一些特别典型,我把它们整理成了一张表格,方便你快速对照排查:
| 报错现象 | 可能原因 | 排查思路 |
|---|---|---|
| 登录成功后访问接口还是401 | Session未保存成功,或拦截器取Session方式不对 | 确认登录接口是否执行了session.setAttribute("user", ...);确认拦截器用的是getSession(false) |
| 验证码明明正确却提示验证码错误 | Session中的验证码被覆盖,或前端提交字段名和后端不一致 | 在登录接口打断点,查看Session中实际存的值;确认前端传的是code字段 |
| 获取验证码成功后,等一会儿再登录就说验证码已失效 | Session超时,或Session被意外重建 | 检查Tomcat默认Session超时时间(默认30分钟);检查是否有调用session.invalidate()或request.getSession(true) |
| 重启后端服务后,所有用户登录态全部失效 | Session默认存在应用内存中,重启即清空 | 这是正常现象。生产环境如需持久化,可考虑Session共享方案(如Redis),或换JWT方案 |
| 同一个浏览器登录了A账号,再切B账号发现还是A的登录态 | Cookie中的JSESSIONID没变,Session中的数据没被覆盖 | 登出时清理验证码和用户信息后再重新登录,确认切号逻辑 |
其中“验证码正确却提示错误”这个坑我遇到的频率最高,而且大多数时候不是验证码真的错了,而是字段名对不上。前端提交的是{phone: "138xxx", code: "482913"},但后端接收参数的类字段名写的是{phone: "138xxx", verificationCode: "482913"},导致loginForm.getCode()一直是null,值明明有却取不到。这种问题在Spring MVC里往往不会有任何报错提示,只是功能不通,非常隐蔽。
排查思路我一般这样做:在LoginFormDTO上临时加一行System.out.println(loginForm),打印出实际收到的参数,一眼就能看出字段名是否匹配。或者更直接一点,在登录接口第一行打个断点,调出loginForm对象,看里面哪些字段是null。记住一个原则:先确认“接口到底收到了什么”,再谈下一步逻辑。
5.2 几个非代码层面的诡异问题:环境与配置的锅
除了代码本身的bug,这个项目里我还遇到过几类“非代码因素”导致的登录异常,这些坑如果没人提醒,自己排查可能浪费一整天。
第一个是端口占用问题。Spring Boot默认端口是8080,但很多开发机的8080端口可能已经被其他服务占用了。如果启动日志显示端口被占用,服务起不来,你就登录不了。解决办法是换一个端口,在application.yml里配置server.port=8081,同时修改前端代码里所有指向后端地址的配置,保持一致。我遇到过最离谱的一次是电脑上某软件的调试服务占了8080,项目一直起不来,我还以为是代码写错了,排查了很久。
第二个是Linux服务器上SSH连接数打满导致登录失败的问题。我记得有次在服务器上部署项目,发现su - root切换用户时直接报错:pam_systemd(su-l:session): failed to create session: No buffer space available。这个报错的意思是系统创建Session时内存缓冲区不足,通常是/run/user/0目录下的session数量超限,或者systemd的PID文件空间满了。对于做JavaWeb项目的同学来说,这个场景虽然不太常见,但如果你用SSH工具连服务器部署项目,多多少少会遇到类似问题。解决方法是清理/tmp下无用的systemd文件,或者重启一下系统服务释放连接。
第三个是Windows上的Local Session Manager服务CPU占用过高。这个和JavaWeb无关,但我猜很多人在开发时也遇到过系统突然卡顿,打开任务管理器发现“本地会话管理器”占用CPU高得吓人。这通常是系统服务异常或系统文件损坏导致,和你的项目代码没关系,不用慌,重启电脑一般能解决。但如果频繁出现,就要考虑是不是系统补丁更新冲突,或者后台有异常软件在搞鬼。
5.3 容易被忽略的边界场景测试清单
项目写完只是第一步,真正决定你这个模块“能不能上线”的,是你有没有把边界场景测全。我在写完登录模块后,会按照下面这张清单过一遍,建议你也这么做:
- 手机号为空、格式非法:应该返回“手机号格式错误”。
- 验证码为空、验证码错误:应该返回“验证码错误”。
- 先获取验证码,然后用另一个手机号登录:应该返回“手机号与获取验证码的手机号不一致”。
- 不携带任何Cookie直接访问需要登录的接口:应该返回401。
- 携带伪造的JSESSIONID访问需要登录的接口:后端找不到对应Session,应该返回401,而不是报500。
- 登录成功后,Session过期(可以通过设置短超时时间来模拟),再访问需要登录的接口:应该返回401。
- 同一个浏览器窗口,先登录A账号再登出,再登录B账号:B账号信息应该正确显示。
这些用例看着不起眼,但每一个都能暴露一类真实问题。比如“携带伪造的JSESSIONID”这个场景,我一开始没测,后来发现如果Session不存在,代码里session.getAttribute("user")返回null,倒是不会报错,但如果你在代码里用了session.getAttribute("user").getId()这种链式调用,就会抛NullPointerException,直接导致接口500。所以以后写从Session取数据的代码,第一件事永远是判空。
登录模块还有一个容易被忽略的点:验证码的时效性。基于Session的实现里,如果用户反复点击“获取验证码”,后一次会直接覆盖前一次的验证码,但Session中的手机号也会同步更新。所以后获取的验证码一定会覆盖先前的,这也是合理的业务逻辑。但要注意,如果一个手机号在短时间内频繁获取验证码,可能会被恶意刷接口,白白消耗短信费用。黑马点评的视频里没有特别处理这个问题,但如果你做的是商业项目,一定要加“60秒内不能重复发送”的频控逻辑,简单一点可以往Session里存一个发送时间戳,复杂一点可以用Redis做计数器。这个小细节在面试的时候提一下,是能加分的。
还有一点是关于登出逻辑的。很多同学会把登出写成“调用session.invalidate()把整个Session销毁”。这样做虽然简单粗暴,但要注意:如果你在同一个Session里还存了其他业务数据(比如购物车、浏览记录),这些数据也会一起被清掉。更合理的做法是先判断当前用户是否已登录,已登录才执行登出逻辑。如果你用Redis方案做会话管理,登出就变成删除Redis里对应的key,逻辑上更清晰。
5.4 为什么我推荐你把这段代码写完后再做一遍“代码走读”
写完代码、跑通功能、测完边界用例之后,我强烈建议你不要急着进入明天的课程内容,而是花半小时把这段登录代码再逐行走一遍。这不是浪费时间,而是帮你建立“代码审查”技能的最好方式。
走读的时候重点看这几件事:第一,Session的Key是否写成了魔法值。代码里写了三次session.setAttribute("user", ...),如果有一次写成"userInfo",后面取的时候就会一直取不到,而且这种问题非常难肉眼发现。建议把Key定义成常量,比如public static final String SESSION_USER = "user";。第二,所有从Session取数据的地方是否都做了判空。第三,Controller层的参数校验是否足够健壮,比如手机号格式、验证码长度,不能完全依赖前端做好校验,后端必须再校验一遍,因为前端传过来的数据是可以被伪造的。
我每次写完这类基础模块都会做一遍走读,每次都能发现一些细小的疏漏,这也是我从“能跑就行”到“稳如老狗”转变的关键方法。对你来说,这个习惯建立得越早,后面写那些动辄几万行代码的商业项目时,犯错的概率就越低。
做完这些工作,Session实现登录这个Day01其实才算真正“吃透”了。我发现很多同学学项目是“看完视频、跑通代码就觉得自己会了”,但我更建议你合上视频,照着需求文档,自己从零写一遍。第一遍参照着写,第二遍独立写,第三遍尝试优化(比如把验证码有效期加上、把Session改造成Redis共享)。三遍写下来,这个模块才真正变成你自己的东西。
这个项目的后续内容里,登录部分还会升级到Redis方案,核心逻辑没变,只是把Session换成了Redis保存登录态。到时候你会知道,登录功能远不止“调一个接口”那么简单——它承载了用户身份认证、会话管理、接口权限校验这些后端最核心的能力。把Session这一版彻底搞懂,后面不管换什么存储、什么框架,你都会有底气。
