国庆节10月1日
--


第 12 章(登录认证)的开篇——先把「怎么样才算登录成功」和「登录的本质就是一次查询」想清楚,再按三层架构把登录接口落地(控制层收用户名密码、业务层查员工并生成令牌组装返回、数据访问层一条 select),最后照接口文档联调,实测登录成功与密码错误两种响应,并暴露「未登录也能直接访问所有接口」这个问题

80 篇把第 11 章的实战收尾了——班级管理、学员管理、数据统计三大块做完并和前端联调通过,员工、部门那两块更早就是能用的状态。到这里,Tlias 的侧边栏点哪儿都有内容,可整个系统还是”裸奔”的:任何人打开浏览器敲一下 http://localhost:8080/emps,员工数据就全出来了。
本篇开始是第 12 章:后端 Web 实战(登录认证),给系统补上”门禁”。它分成两块:登录功能(本篇,PPT 第 1~9 页)和登录校验(82 篇、83 篇、84 篇)。先看这一章的全貌。
PPT 第 1 页是章节大标题(Web 后端开发),第 4 页是本章章节页——“Web 后端开发 / 登录认证”。这是后端 Web 开发的最后一块:前面的章节把 CRUD、分页、文件上传、事务这些都讲完了,最后要解决的是”谁有资格用这个系统”。
PPT 第 2~3 页是”现在的 Tlias”:部门管理、员工管理、班级管理、学员管理、员工信息统计都已经做出来了(页面上列表、查询、新增、修改、删除一应俱全),但页面里还躺着一张登录页——它背后的接口一个都还没写。

PPT 第 5 页把本章拆成两块:
登录功能(01) → 登录校验(02)
第 6 页打出”01 登录功能”,正片开始。本章四篇笔记的分工如下:
| 编号 | 标题 | PPT 页 | 干什么 |
|---|---|---|---|
| 81(本篇) | 登录功能 | 1~9 | 按接口文档把 POST /login 做出来:用户名密码查库、成功返回登录信息 + 令牌 |
| 82 | 会话技术与JWT令牌 | 10~28 | 登录校验的”登录标记”这一半:会话技术三种方案对比 + JWT 令牌的生成与解析 |
| 83 | 过滤器Filter | 29~42 | 用过滤器把令牌校验统一拦在所有请求前面 |
| 84 | 拦截器Interceptor | 43~53 | 同样的事用 Spring 的拦截器再做一遍,并对比两者 |
PPT 第 7 页一口气问了两件事,答案都很短:
| PPT 的问题 | PPT 的答案 |
|---|---|
| 怎么样才算登录成功了呢? | 用户名和密码都输入正确,登录成功;否则,登录失败 |
| 登录功能的本质是什么? | 查询——根据用户名和密码查询员工信息 |
把这两句翻译成数据库的语言:用户在前端填的 username、password 就是两个查询条件,把它俩拿到 emp 表里”对一对”——
所以登录接口从头到尾不需要新增、修改任何数据,它就是一次带条件的查询加一次判断。想清楚这一点,三层代码怎么写心里就有数了。

/login 的约定(PPT 第 7 页 + 资料/02. 接口文档)#Tlias 是接口文档驱动(57 篇和实战章反复强调过),登录接口也早就定好了。它在接口文档”6. 其他接口 → 6.1 登录”里:
| 项目 | 约定 |
|---|---|
| 请求路径 | /login |
| 请求方式 | POST |
| 接口描述 | 该接口用于员工登录 Tlias 智能学习辅助系统,登录完毕后,系统下发 JWT 令牌 |
请求参数(application/json):
| 名称 | 类型 | 是否必须 | 备注 |
|---|---|---|---|
username | string | 必须 | 用户名 |
password | string | 必须 | 密码 |
请求数据样例:
1{2 "username": "jinyong",3 "password": "123456"4}响应 data 里的四个字段:
| 名称 | 类型 | 是否必须 | 备注 |
|---|---|---|---|
code | number | 必须 | 响应码,1 成功;0 失败 |
msg | string | 非必须 | 提示信息 |
data | object | 必须 | 返回的数据 |
data.id | number | 必须 | 员工 ID |
data.username | string | 必须 | 用户名 |
data.name | string | 必须 | 姓名 |
data.token | string | 必须 | 令牌 |
(接口文档里后四行是缩进写的”|- + 字段名”,表示它们是 data 对象内部的字段;这里写成 data.id 这样更好认。下面响应样例里的 data 结构就是这四行。)
响应数据样例:
1{2 "code": 1,3 "msg": "success",4 "data": {5 "id": 2,6 "username": "songjiang",7 "name": "宋江",8 "token": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJzb25namlhbmciLCJleHAiOjE2OTg3MDE3NjJ9.w06EkRXTep6SrvMns3w5RKe79nxauDe7fdMhBLK-MKY"9 }10}接口文档在”备注说明”里还提前交代了 token 的用法,这一条对后面三篇都关键:
用户登录成功后,系统会自动下发 JWT 令牌,然后在后续的每次请求中,都需要在请求头 header 中携带到服务端,请求头的名称为
token,值为登录时下发的 JWT 令牌。 如果检测到用户未登录,则直接响应 401 状态码。
换句话说:登录接口负责发令牌,后面每个接口都要收令牌。发令牌就是本篇要做的(data.token 那个字段),收令牌、验令牌是82 篇和 83、84 篇的事。

/login、请求体是 username 与 password 两个字段、响应 data 里有 id、username、name、token 四项。开发时对着这张表写,写完对着它联调PPT 第 8 页把登录的思路画成了三层分工图,正好对应37 篇的三层架构:
| 层 | 干什么(PPT 原文) |
|---|---|
| Controller | 接收请求参数(用户名、密码);调用 Service 方法;响应结果 |
| Service | 根据用户名和密码查询员工信息;判定,组装数据并返回 |
| Mapper | SQL:select * from emp where username = ? and password = ? |
LoginInfo:装登录成功的结果#接口文档规定响应 data 里有 id、username、name、token 四项,工程里就为它建一个封装类:
1/**2 * 封装登录结果3 */4@Data5@NoArgsConstructor6@AllArgsConstructor7public class LoginInfo {8 private Integer id; // 员工ID9 private String username; // 用户名10 private String name; // 姓名11 private String token; // 令牌12}四个字段和接口文档 data 下的四项一一对应。Lombok 三件套的用法和第 68~80 篇一样:@Data 生成 getter/setter(Spring 转 JSON 时要用),@NoArgsConstructor + @AllArgsConstructor 让 new LoginInfo(...) 能一行写完四参构造。
1/**2 * 登录Controller3 */4@Slf4j5@RestController6public class LoginController {7
8 @Autowired9 private EmpService empService;10
11 /**12 * 登录13 */14 @PostMapping("/login")15 public Result login(@RequestBody Emp emp){16 log.info("登录: {}", emp);17 LoginInfo info = empService.login(emp);18 if (info != null){19 return Result.success(info); // 登录成功:返回登录信息(含令牌)20 }21 return Result.error("用户名或密码错误"); // 登录失败22 }23}几点说明:
@PostMapping("/login")、请求体是 JSON(32 篇讲过的数据格式),所以用 @RequestBody 把请求体里的 {"username": ..., "password": ...} 直接封装进 Emp 对象——Emp 是项目里现成的实体类,它本来就有 username、password 两个属性,登录这里不需要为了两个字段再单独造一个参数类;@RequestBody 收到 Emp)、调(empService.login(emp))、包(用 Result 统一响应格式,35 篇);null 就是”没查到”,回一句 Result.error("用户名或密码错误")(code = 0);成功则 Result.success(info)(code = 1)。@Slf4j + log.info("登录: {}", emp) 是跟着课程走的日志习惯(63 篇):谁在什么时候试图登录,控制台留一条记录。业务接口 EmpService 里加一个方法:
1 /**2 * 员工登录3 */4 LoginInfo login(Emp emp);实现(EmpServiceImpl):
1 @Override2 public LoginInfo login(Emp emp) {3 //1. 调用mapper接口, 根据用户名和密码查询员工信息4 Emp e = empMapper.selectByUsernameAndPassword(emp);5
6 //2. 判断: 判断是否存在这个员工, 如果存在, 组装登录成功信息7 if(e != null){8 log.info("登录成功, 员工信息: {}", e);9 //生成JWT令牌10 Map<String, Object> claims = new HashMap<>();11 claims.put("id", e.getId());12 claims.put("username", e.getUsername());13 String jwt = JwtUtils.generateToken(claims);14
15 return new LoginInfo(e.getId(), e.getUsername(), e.getName(), jwt);16 }17
18 //3. 不存在, 返回null19 return null;20 }这段代码就是 PPT 第 8 页 Service 那一格的展开,三步对得整整齐齐:
Emp(查不到就是 null);claims(Map),把这次要”装进令牌”的信息放进去:员工的 id 和 username。为什么要放?因为令牌是后续请求唯一的”身份证”——服务器要靠它认出”这次请求是谁发的”,令牌里就得带上能标识这个人的信息;JwtUtils.generateToken(claims) 生成令牌字符串;id、username、name、token 组装成 LoginInfo 返回。
注意 name 是从数据库里查出来的(e.getName()),请求里只有用户名和密码;null,交给 Controller 决定怎么响应。JwtUtils 是这一篇先”当它存在”的工具类:令牌的生成与解析就是82 篇的主题,那里会把它一层层拆开(jjwt 依赖、Jwts.builder()、ExpiredJwtException…)。现在只要知道这一步在”登录成功后生成令牌”即可。
1 /**2 * 根据用户名和密码查询员工3 */4 @Select("select id, username, name from emp where username = #{username} and password = #{password}")5 Emp selectByUsernameAndPassword(Emp emp);对照 PPT 第 8 页的 SQL 看几个细节:
| 细节 | 说明 |
|---|---|
PPT 写的是 select * from emp where ... | 思路层面写 * 没关系;落到工程里只查用得到的三列(id, username, name)——登录只需要这三个字段,没必要把整个员工连密码、头像、薪资全捞回来 |
? 变成 #{username} / #{password} | MyBatis 的预编译参数写法(50、51 篇),#{} 里的名字就是参数的属性名 |
参数是一个 Emp 对象 | Mapper 方法只有一个参数时,#{username}、#{password} 会直接去这个对象的属性里取(54 篇的参数传递方式) |
返回值是单个 Emp | 查到了就返回一个 Emp,查不到返回 null——这个 null 就是 Service 里那个”判定”的依据 |
课程里 emp 表的 password 存的是明文(比如 123456),比较时也是明文比较。真实项目里不会这么干(密码要加盐哈希后存、登录时比对哈希值,数据库被拖走也不能直接看到密码),第 12 章按课程的做法来,心里有这根弦就行。
登录接口写完,按项目惯例要联调:把工程启动起来,用 Apifox 或前端的登录页发一次 POST /login,确认响应和接口文档一致(成功、失败两种情况都要试)。这一步没问题——但 PPT 第 9 页的重点不是”成功”,而是联调之后暴露的问题:
问题:在未登录情况下,我们也可以直接访问部门管理、员工管理等功能。 需求:只有员工登录成功,才可以访问后台系统中的数据。
PPT 这一页画的是浏览器与 Web 服务器之间的那几张流程:Login、Emp、Dept、Report 全都直通服务器,中间少了一道闸门。具体说:
/depts、/emps、/report/*,照样返回 200 和数据——登录这个动作”白做了”;从82 篇开始解决它。登录校验要想清楚两件事:登录标记(登录成功后,后续每次请求都能拿到这个标记——靠会话技术,82 篇)和统一拦截(不用在每个接口里写校验代码,而是在请求进业务之前统一拦一道——靠过滤器 Filter、拦截器 Interceptor,83、84 篇)。
本机实测(工程连的是本机 MySQL 的 tlias 库、用户名 root,课程示例密码 1234——动手时把 password 换成你自己 MySQL 的密码;emp 表里有账号 shinaian / 123456):
1# ① 登录成功 POST /login {"username":"shinaian","password":"123456"}2{"code":1,"msg":"success","data":{3 "id":1,"username":"shinaian","name":"施耐庵",4 "token":"eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJzaGluYWlhbiIsImV4cCI6MTc5MDczNjM0NX0.zHEWXHS6ZSoz8L2klv-8fFZQODftFlp4r_Ps1jGJFic"}}5
6# ② 密码错误(username 不变,password 改成错的)7{"code":0,"msg":"用户名或密码错误","data":null}对着接口文档逐项核对:
返回的 data 就是 LoginInfo:id、username、name 三项来自查到的那个员工,token 是 Service 里 JwtUtils.generateToken(claims) 生成的;
token 是一枚标准的 JWT:三段用 . 分隔,中间那段 Base64 解出来是
1{"id":1,"username":"shinaian","exp":1790736345}这正是代码里 claims.put("id", ...)、claims.put("username", ...) 放进去的内容,exp 是过期时间(课程序言里设的 12 小时后)。令牌为什么长这样、exp 怎么来的,82 篇细讲;
密码错误时走的是 Controller 里的 Result.error(...) 分支:code = 0、msg 是”用户名或密码错误”、data 为 null(Service 返回了 null)。顺带说一句:真实项目里失败提示也故意不区分”用户名不存在”和”密码错误”,免得给攻击者递话。
| 问题 | 答案 |
|---|---|
| 第 12 章要做什么? | 登录功能(发令牌)+ 登录校验(验令牌),给系统装上门禁;本篇做前者(PPT 第 1~9 页) |
| 怎么样才算登录成功? | 用户名和密码都输入正确;否则登录失败 |
| 登录功能的本质? | 查询——根据用户名和密码查询员工信息;不用写任何数据 |
| 登录接口的约定? | POST /login,请求体 application/json(username、password);响应 data 为 {id, username, name, token} |
| Controller 怎么写? | @RestController + @PostMapping("/login") + @RequestBody Emp;调 Service,成功 Result.success(info)、失败 Result.error("用户名或密码错误") |
| Service 怎么写? | 调 Mapper 查员工 → 查到就组装 claims(id、username)生成令牌 → new LoginInfo(id, username, name, token) 返回;查不到返回 null |
| Mapper 怎么写? | @Select("select id, username, name from emp where username = #{username} and password = #{password}"),返回单个 Emp(查不到是 null) |
| 登录结果封装类? | LoginInfo(id、username、name、token),Lombok 的 @Data + @NoArgsConstructor + @AllArgsConstructor |
| 本机实测结果? | 成功:{"code":1,"msg":"success","data":{"id":1,"username":"shinaian","name":"施耐庵","token":"eyJ…"}};密码错:{"code":0,"msg":"用户名或密码错误","data":null} |
| 联调暴露的问题? | 未登录也能直接访问部门/员工/报表等接口 → 需要”只有登录成功才能访问后台数据”的登录校验(82~84 篇) |
POST /login,请求体格式 application/json,参数 username、password;响应 data 是 {id, username, name, token} 四项(code 1 成功 0 失败)select ... from emp where username = ? and password = ?@RestController + @PostMapping("/login"),参数用 @RequestBody 接收 JSON 并封装成 Emp;Service 返回 null 就 Result.error("用户名或密码错误"),否则 Result.success(info)empMapper.selectByUsernameAndPassword(emp))→ 查到就组装 claims(放 id、username)生成令牌 → new LoginInfo(e.getId(), e.getUsername(), e.getName(), jwt) 返回;查不到返回 null(name 来自数据库,不是请求)@Select("select id, username, name from emp where username = #{username} and password = #{password}"),返回单个 Emp;查不到返回 null(这就是 Service 判定的依据);#{} 是预编译参数LoginInfo——id、username、name、token 四个字段,与接口文档 data 一一对应;类上 @Data、@NoArgsConstructor、@AllArgsConstructortlias 库):登录成功 → {"code":1,"msg":"success","data":{"id":1,"username":"shinaian","name":"施耐庵","token":"eyJ…"}};密码错误 → {"code":0,"msg":"用户名或密码错误","data":null};令牌中间那段 Base64 解出来是 {"id":1,"username":"shinaian","exp":1790736345} 2-1 写”按用户名和密码查员工”的这条 SQL
需求:给一对用户名和密码,去员工表里把这个人查出来。只取登录要用的三个字段——员工 ID、用户名、姓名(不用把密码、头像、薪资这些也捞回来)。用户名和密码都要用”先占位、后填值”的安全写法,别把值直接拼进 SQL 字符串。查不到时方法应该返回什么?
写完回答:为什么不把 password 这一列也查出来返回给前端?
(练习文件 test_81_登录功能.java 的题目2-1 里给了写作区。)
一级 · 思路:一条带两个条件的查询,条件来自前端传的两个参数;JDBC/MyBatis 的世界里”安全写法”指的是预编译参数而不是字符串拼接
二级 · 方法:数据访问层方法上加注解写 SQL —— @Select("select id, username, name from emp where username = #{username} and password = #{password}");参数对象里正好有这两个属性,#{} 会按属性名取值;返回类型写 Emp(一条结果),查不到 MyBatis 返回 null
三级 · 骨架:@____("select id, username, ____ from emp where username = #{____} and password = #{____}") + Emp selectByUsernameAndPassword(Emp emp);
1 /**2 * 根据用户名和密码查询员工3 */4 @Select("select id, username, name from emp where username = #{username} and password = #{password}")5 Emp selectByUsernameAndPassword(Emp emp);回答:MyBatis 查不到结果时返回 null——Service 里正是拿这个 null 当”登录失败”的判定依据。
为什么不查 password:登录只需要”这个人是谁”(id、用户名、姓名),把密码查出来再往响应里传,等于把库里的密码泄露给前端和日志(log.info("登录成功, 员工信息: {}") 会把实体连同密码一起打出来);课程工程里这条 SQL 只 select 了三列,就是为了少一事。
检查点:① 两个条件必须同时满足(用 and 连起来),只写用户名的话任何人随便填个密码都能进;② 值为 null 时不能用 = 比较(那是另一回事,这里两个参数都是必填)。
2-2 写登录接口的控制层
需求:前端 POST 一个 JSON 过来,里面是 username 和 password;把这个 JSON 收进一个对象,交给业务层去登录。业务层回了东西(说明登录成功),就用项目统一的响应格式把它返回;业务层什么都没回(null),就回一句”用户名或密码错误”。另外,收到请求时往日志里打一条。
(练习文件 test_81_登录功能.java 的题目2-2 里给了写作区。)
一级 · 思路:控制层只做”收、调、包”三件事——收请求体里的两个字段、调业务层的登录方法、包成统一响应返回;判断成没成靠的是业务层返回的对象是不是 null
二级 · 方法:类上 @RestController(顺带 @Slf4j 打日志),方法上 @PostMapping("/login");参数上加 @RequestBody(把 JSON 请求体封装成对象);用 Result.success(...) / Result.error("用户名或密码错误")
三级 · 骨架:@____("/login") + public Result login(@____ Emp emp){ … };if (info != ____) { return Result.____(info); } return Result.____("用户名或密码错误");
1@Slf4j2@RestController3public class LoginController {4
5 @Autowired6 private EmpService empService;7
8 /**9 * 登录10 */11 @PostMapping("/login")12 public Result login(@RequestBody Emp emp){13 log.info("登录: {}", emp);14 LoginInfo info = empService.login(emp);15 if (info != null){16 return Result.success(info);17 }18 return Result.error("用户名或密码错误");19 }20}检查点:① 必须用 @RequestBody——不写的话 Spring 会去取 URL 上的查询参数,而登录的两个参数是在请求体里的(application/json);② 路径就是 /login,接口文档定死的;③ 成与败都用 Result 包起来,前端才不用做两套解析。
2-3 写登录的业务层实现
需求:拿用户名和密码去查员工。查到了,说明登录成功——此时先生成一个令牌(令牌里带上这个员工的 id 和用户名,具体怎么生成见82 篇,这里把”生成令牌的工具类”当成现成的用),再把 id、用户名、姓名、令牌四样东西组装成一个”登录结果对象”返回;查不到就返回 null。请在代码里标出”查、判、组装”三步。
(练习文件 test_81_登录功能.java 的题目2-3 里给了写作区。)
一级 · 思路:三步走——① 调用数据访问层的方法,把请求对象原样传进去;② 判断返回的对象是不是 null;③ 不空时,先准备”要装进令牌的信息”(一个 Map,放 id 和 username),生成令牌,再用登录结果类的全参构造把四样东西组装出来
二级 · 方法:类上 @Service(该类已有 @Autowired private EmpMapper empMapper;);Map 用 claims.put("id", e.getId())、claims.put("username", e.getUsername());生成令牌 JwtUtils.generateToken(claims);组装 new LoginInfo(e.getId(), e.getUsername(), e.getName(), jwt);日志用 log.info(...)
三级 · 骨架:
1@Override2public LoginInfo login(Emp emp) {3 Emp e = empMapper.____(emp); // 1. 查4 if (e != ____) { // 2. 判5 Map<String, Object> claims = new HashMap<>();6 claims.put("id", ____);7 claims.put("username", ____);8 String jwt = ____.generateToken(claims);9 return new ____(e.getId(), e.getUsername(), e.getName(), ____); // 3. 组装10 }11 return null;12}1 @Override2 public LoginInfo login(Emp emp) {3 //1. 调用mapper接口, 根据用户名和密码查询员工信息4 Emp e = empMapper.selectByUsernameAndPassword(emp);5
6 //2. 判断: 判断是否存在这个员工, 如果存在, 组装登录成功信息7 if(e != null){8 log.info("登录成功, 员工信息: {}", e);9 //生成JWT令牌10 Map<String, Object> claims = new HashMap<>();11 claims.put("id", e.getId());12 claims.put("username", e.getUsername());13 String jwt = JwtUtils.generateToken(claims);14
15 return new LoginInfo(e.getId(), e.getUsername(), e.getName(), jwt);16 }17
18 //3. 不存在, 返回null19 return null;20 }检查点:① 判断依据是”查回来的对象是不是 null”,不是”密码是不是空的”;② LoginInfo 的四个参数顺序别弄反(id、username、name、token——接口文档就是这个顺序),这是全参构造最容易错的地方;③ name 取 e.getName()(数据库里的姓名),别拿请求里的 emp 凑。
2-4 把”为什么登录的本质是查询”讲清楚,并解释联调暴露的问题
需求:回答三件事——
① 为什么说登录功能的本质是查询?(提示:从”登录成功/失败的判定标准”出发)
② 照接口文档,登录成功后前端拿到的那串令牌,之后要怎么用?一直不带会怎样?
③ 登录接口做完、也联调通过了,为什么 PPT 第 9 页说”这是个问题”?要满足”只有登录成功才能访问后台数据”,还需要补哪两块东西?
(练习文件 test_81_登录功能.java 的题目2-4 里给了写作区。)
一级 · 思路:① 从”怎么判断用户名密码对不对”想到”拿条件去表里找记录”;② 从接口文档”备注说明”里找答案(请求头、状态码);③ 从”登录接口只是发令牌、没人查令牌”这条线想
二级 · 方法:关键词——一次带条件的查询、select ... where username = ? and password = ?、查得到/查不到;请求头名称为 token、未登录响应 401;登录标记(会话技术)+ 统一拦截(过滤器 Filter、拦截器 Interceptor)
三级 · 骨架:① 判定标准是____,所以拿____去表里____,查得到就是____;② 后续每次请求都要在请求头 ____ 里带上令牌,不带的话接口会返回 ;③ 问题在于令牌发出去后,需要补上____和____
① 登录成功与否的判定标准是”窗口里填的用户名和密码都对”。服务器并不认识这个人,它能做的只是:拿这两个值当条件,去 emp 表里查一遍——查到了,说明这对用户名密码在库里存在,登录成功;查不到,就是用户名或密码错误。所以整件事在数据库层面就是一次 select ... where username = ? and password = ? 的查询(外加查回来之后的判定),既不新增也不修改任何数据;查回来的那个员工顺带提供了 id、姓名,用来组装登录结果。
② 接口文档的备注说明写得很明确:登录成功后系统下发 JWT 令牌,后续每次请求都要在请求头 header 里携带,请求头名称为 token、值为这个令牌;如果检测到用户未登录,服务端直接响应 401 状态码。也就是说令牌是”通行证”,前端要把它存起来(比如 localStorage)、每次请求带上——不带就等着 401。
③ 因为登录接口目前只负责发令牌,发出去之后就没人管了:前端带不带、服务端验不验,全凭自觉。所以不登录直接请求 /depts、/emps、/report/* 照样能拿到数据,登录形同虚设——这就是 PPT 第 9 页说的”未登录情况下,我们也可以直接访问部门管理、员工管理等功能”。要满足需求,还需要两块:登录标记(登录成功后在后续每次请求里都能拿到这个标记,靠会话技术实现,82 篇)和统一拦截(把所有请求先拦下来统一校验令牌,不用在每个接口里各写一份,靠过滤器 Filter / 拦截器 Interceptor,83、84 篇)。
3-1 照着接口文档,把登录功能从零做出来并联调 这一题把本篇从头到尾走一遍:先看文档 → 定封装类 → 写三层 → 启动验证 → 核对两种响应 → 回答问题。
LoginInfo(id、username、name、token 四个字段 + Lombok 三件套);EmpMapper 里加”按用户名和密码查员工”的方法(只查 id、username、name 三列,用预编译参数占位);EmpService 声明登录方法,EmpServiceImpl 实现——查员工 → 判定 →(查到则)生成令牌 → 组装 LoginInfo 返回,查不到返回 null;LoginController 里写 POST /login——收请求体、调业务层、成功返回 Result.success(info)、失败返回 Result.error("用户名或密码错误");tlias 库);token 抄出来,数一数它是几段、用哪个字符分隔;null 时,为什么 Controller 只回一句”用户名或密码错误”、而不是分别说”用户名不存在”/“密码错误”?
② 这枚令牌现在有没有被任何地方校验?要让它真正成为”通行证”,下一步要做什么?
(练习文件 test_81_登录功能.java 的”综合题”一段里按这 8 步给了写作区。)涉及知识点
| 知识点 | 在这里的应用 |
|---|---|
| 登录成功判定与”本质是查询”(PPT 7) | 第 3、4 步——一次带条件的 select 加一次判定 |
| 登录接口约定(接口文档 6.1) | 第 1、5、6 步——POST /login、请求体两个字段、响应 data 四项 |
| 三层分工(PPT 8) | 第 3~5 步——Controller 收/调/包、Service 查/判/组装、Mapper 一条 SQL |
@RequestBody 与 Result 统一响应 | 第 5 步——JSON 请求体 → 对象;成功 code=1、失败 code=0 |
| 令牌的生成(PPT 8、28) | 第 4、6、7 步——claims 放 id/username,工具类生成,随 LoginInfo 返回 |
| 联调暴露的问题(PPT 9) | 第 8 步——登录接口只发令牌,还没有登录校验 |
一级 · 思路:按”文档 → 封装类 → 数据访问层 → 业务层 → 控制层 → 启动验证”的顺序一层层搭,每层只做自己那一格的事
二级 · 方法:@Select("select id, username, name from emp where username = #{username} and password = #{password}");claims.put("id", ...) + JwtUtils.generateToken(claims);new LoginInfo(...);@PostMapping("/login") + @RequestBody;发请求用 Apifox 或前端登录页(POST + JSON 请求体)
三级 · 骨架:@Select("select id, username, name from emp where username = #{____} and password = #{____}");Emp e = empMapper.____(emp); if (e ____ null) { … };@____("/login") public Result login(@____ Emp emp){ … }
/login、请求方式 POST、请求参数 username/password(application/json,都必填)、响应 data 为 {id, username, name, token}。
2~5. 四段代码与课程工程一致:
1@Data2@NoArgsConstructor3@AllArgsConstructor4public class LoginInfo {5 private Integer id; // 员工ID6 private String username; // 用户名7 private String name; // 姓名8 private String token; // 令牌9}1@Select("select id, username, name from emp where username = #{username} and password = #{password}")2Emp selectByUsernameAndPassword(Emp emp);1@Override2public LoginInfo login(Emp emp) {3 Emp e = empMapper.selectByUsernameAndPassword(emp);4 if (e != null) {5 log.info("登录成功, 员工信息: {}", e);6 Map<String, Object> claims = new HashMap<>();7 claims.put("id", e.getId());8 claims.put("username", e.getUsername());9 String jwt = JwtUtils.generateToken(claims);10 return new LoginInfo(e.getId(), e.getUsername(), e.getName(), jwt);11 }12 return null;13}1@PostMapping("/login")2public Result login(@RequestBody Emp emp){3 log.info("登录: {}", emp);4 LoginInfo info = empService.login(emp);5 if (info != null){6 return Result.success(info);7 }8 return Result.error("用户名或密码错误");9}1# 正确 {"username":"shinaian","password":"123456"}2{"code":1,"msg":"success","data":{3 "id":1,"username":"shinaian","name":"施耐庵",4 "token":"eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJzaGluYWlhbiIsImV4cCI6MTc5MDczNjM0NX0.zHEWXHS6ZSoz8L2klv-8fFZQODftFlp4r_Ps1jGJFic"}}5
6# 密码错误7{"code":0,"msg":"用户名或密码错误","data":null}. 分隔(头、载荷、签名);中间那段 Base64 解出来是 {"id":1,"username":"shinaian","exp":1790736345}——正是 claims 里放进去的 id、username 和过期时间。token 有没有、合不合法,不合法直接 401。如果你喜欢,那么欢迎来到我的世界!
了解更多暂未播放



"所有的相遇都是久别重逢。"—— 未知


