80 篇把第 11 章的实战收尾了——班级管理、学员管理、数据统计三大块做完并和前端联调通过,员工、部门那两块更早就是能用的状态。到这里,Tlias 的侧边栏点哪儿都有内容,可整个系统还是"裸奔"的:任何人打开浏览器敲一下 http://localhost:8080/emps,员工数据就全出来了。
本篇开始是第 12 章:后端 Web 实战(登录认证),给系统补上"门禁"。它分成两块:登录功能(本篇,PPT 第 1~9 页)和登录校验(82 篇、83 篇、84 篇)。先看这一章的全貌。
第 12 章在做什么(PPT 第 1~6 页)
PPT 第 1 页是章节大标题(Web 后端开发),第 4 页是本章章节页——"Web 后端开发 / 登录认证"。这是后端 Web 开发的最后一块:前面的章节把 CRUD、分页、文件上传、事务这些都讲完了,最后要解决的是"谁有资格用这个系统"。
PPT 第 2~3 页是"现在的 Tlias":部门管理、员工管理、班级管理、学员管理、员工信息统计都已经做出来了(页面上列表、查询、新增、修改、删除一应俱全),但页面里还躺着一张登录页——它背后的接口一个都还没写。
图:第 12 章开讲时 Tlias 的样子——这是班级管理页面,列表、查询、新增、修改、删除都已经做好并能用了;第 2~3 页把系统现有的页面挨个展示了一遍,本章要补的就是这些页面门口的那道"门禁"
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 第 7 页一口气问了两件事,答案都很短:
| PPT 的问题 | PPT 的答案 |
|---|---|
| 怎么样才算登录成功了呢? | 用户名和密码都输入正确,登录成功;否则,登录失败 |
| 登录功能的本质是什么? | 查询——根据用户名和密码查询员工信息 |
把这两句翻译成数据库的语言:用户在前端填的 username、password 就是两个查询条件,把它俩拿到 emp 表里"对一对"——
- 查到了:说明数据库里存在"用户名 = 这个、密码 = 这个"的员工 → 登录成功。顺带还把这个人查出来了(id、姓名都有),后面"我是谁"的信息就从他身上取;
- 查不到:这对用户名密码不匹配任何员工 → 登录失败,给前端一句"用户名或密码错误"。
所以登录接口从头到尾不需要新增、修改任何数据,它就是一次带条件的查询加一次判断。想清楚这一点,三层代码怎么写心里就有数了。
图:Tlias 的登录页面原型(用户名、密码两个输入框 + 登录/重置按钮)。这一篇写的接口就是给页面上那个"登录"按钮用的:把两个框里的文字以 JSON 发过来,后端告诉它成没成、并把令牌发下去
接口文档:/login 的约定(PPT 第 7 页 + 资料/02. 接口文档)
Tlias 是接口文档驱动(57 篇和实战章反复强调过),登录接口也早就定好了。它在接口文档"6. 其他接口 → 6.1 登录"里:
| 项目 | 约定 |
|---|---|
| 请求路径 | /login |
| 请求方式 | POST |
| 接口描述 | 该接口用于员工登录 Tlias 智能学习辅助系统,登录完毕后,系统下发 JWT 令牌 |
请求参数(application/json):
| 名称 | 类型 | 是否必须 | 备注 |
|---|---|---|---|
username | string | 必须 | 用户名 |
password | string | 必须 | 密码 |
请求数据样例:
{
"username": "jinyong",
"password": "123456"
}响应 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 结构就是这四行。)
响应数据样例:
{
"code": 1,
"msg": "success",
"data": {
"id": 2,
"username": "songjiang",
"name": "宋江",
"token": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJzb25namlhbmciLCJleHAiOjE2OTg3MDE3NjJ9.w06EkRXTep6SrvMns3w5RKe79nxauDe7fdMhBLK-MKY"
}
}IMPORTANT
接口文档在"备注说明"里还提前交代了 token 的用法,这一条对后面三篇都关键:
用户登录成功后,系统会自动下发 JWT 令牌,然后在后续的每次请求中,都需要在请求头 header 中携带到服务端,请求头的名称为
token,值为登录时下发的 JWT 令牌。 如果检测到用户未登录,则直接响应 401 状态码。
换句话说:登录接口负责发令牌,后面每个接口都要收令牌。发令牌就是本篇要做的(data.token 那个字段),收令牌、验令牌是82 篇和 83、84 篇的事。
图:接口文档里的登录接口——POST /login、请求体是 username 与 password 两个字段、响应 data 里有 id、username、name、token 四项。开发时对着这张表写,写完对着它联调
三层实现:Controller / Service / Mapper(PPT 第 8 页)
PPT 第 8 页把登录的思路画成了三层分工图,正好对应37 篇的三层架构:
| 层 | 干什么(PPT 原文) |
|---|---|
| Controller | 接收请求参数(用户名、密码);调用 Service 方法;响应结果 |
| Service | 根据用户名和密码查询员工信息;判定,组装数据并返回 |
| Mapper | SQL:select * from emp where username = ? and password = ? |
实体类 LoginInfo:装登录成功的结果
接口文档规定响应 data 里有 id、username、name、token 四项,工程里就为它建一个封装类:
/**
* 封装登录结果
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfo {
private Integer id; // 员工ID
private String username; // 用户名
private String name; // 姓名
private String token; // 令牌
}四个字段和接口文档 data 下的四项一一对应。Lombok 三件套的用法和第 68~80 篇一样:@Data 生成 getter/setter(Spring 转 JSON 时要用),@NoArgsConstructor + @AllArgsConstructor 让 new LoginInfo(...) 能一行写完四参构造。
Controller:接收请求参数、调用 Service、响应结果
/**
* 登录Controller
*/
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
/**
* 登录
*/
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
log.info("登录: {}", emp);
LoginInfo info = empService.login(emp);
if (info != null){
return Result.success(info); // 登录成功:返回登录信息(含令牌)
}
return Result.error("用户名或密码错误"); // 登录失败
}
}几点说明:
- 路径
@PostMapping("/login")、请求体是 JSON(32 篇讲过的数据格式),所以用@RequestBody把请求体里的{"username": ..., "password": ...}直接封装进Emp对象——Emp是项目里现成的实体类,它本来就有username、password两个属性,登录这里不需要为了两个字段再单独造一个参数类; - Controller 的职责就三个:收(
@RequestBody收到Emp)、调(empService.login(emp))、包(用Result统一响应格式,35 篇); - 判断写在 Controller 里很薄:Service 返回
null就是"没查到",回一句Result.error("用户名或密码错误")(code = 0);成功则Result.success(info)(code = 1)。 @Slf4j+log.info("登录: {}", emp)是跟着课程走的日志习惯(63 篇):谁在什么时候试图登录,控制台留一条记录。
Service:查询、判定、组装数据
业务接口 EmpService 里加一个方法:
/**
* 员工登录
*/
LoginInfo login(Emp emp);实现(EmpServiceImpl):
@Override
public LoginInfo login(Emp emp) {
//1. 调用mapper接口, 根据用户名和密码查询员工信息
Emp e = empMapper.selectByUsernameAndPassword(emp);
//2. 判断: 判断是否存在这个员工, 如果存在, 组装登录成功信息
if(e != null){
log.info("登录成功, 员工信息: {}", e);
//生成JWT令牌
Map<String, Object> claims = new HashMap<>();
claims.put("id", e.getId());
claims.put("username", e.getUsername());
String jwt = JwtUtils.generateToken(claims);
return new LoginInfo(e.getId(), e.getUsername(), e.getName(), jwt);
}
//3. 不存在, 返回null
return null;
}这段代码就是 PPT 第 8 页 Service 那一格的展开,三步对得整整齐齐:
- 查询——把用户名密码丢给 Mapper,拿回一个
Emp(查不到就是null); - 判定 + 组装——查到了,说明用户名密码都对,开始组装登录成功的信息:
- 先造一个
claims(Map),把这次要"装进令牌"的信息放进去:员工的 id 和 username。为什么要放?因为令牌是后续请求唯一的"身份证"——服务器要靠它认出"这次请求是谁发的",令牌里就得带上能标识这个人的信息; - 调工具类
JwtUtils.generateToken(claims)生成令牌字符串; - 用四参构造把
id、username、name、token组装成LoginInfo返回。 注意 name 是从数据库里查出来的(e.getName()),请求里只有用户名和密码;
- 先造一个
- 查不到就返回
null,交给 Controller 决定怎么响应。
NOTE
JwtUtils 是这一篇先"当它存在"的工具类:令牌的生成与解析就是82 篇的主题,那里会把它一层层拆开(jjwt 依赖、Jwts.builder()、ExpiredJwtException…)。现在只要知道这一步在"登录成功后生成令牌"即可。
Mapper:一条查询 SQL
/**
* 根据用户名和密码查询员工
*/
@Select("select id, username, name from emp where username = #{username} and password = #{password}")
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 里那个"判定"的依据 |
NOTE
课程里 emp 表的 password 存的是明文(比如 123456),比较时也是明文比较。真实项目里不会这么干(密码要加盐哈希后存、登录时比对哈希值,数据库被拖走也不能直接看到密码),第 12 章按课程的做法来,心里有这根弦就行。
联调测试:接口通了,但系统还是"裸奔"(PPT 第 9 页)
登录接口写完,按项目惯例要联调:把工程启动起来,用 Apifox 或前端的登录页发一次 POST /login,确认响应和接口文档一致(成功、失败两种情况都要试)。这一步没问题——但 PPT 第 9 页的重点不是"成功",而是联调之后暴露的问题:
问题:在未登录情况下,我们也可以直接访问部门管理、员工管理等功能。 需求:只有员工登录成功,才可以访问后台系统中的数据。
PPT 这一页画的是浏览器与 Web 服务器之间的那几张流程:Login、Emp、Dept、Report 全都直通服务器,中间少了一道闸门。具体说:
- 登录接口只是"发令牌";令牌发出去之后,前端带不带、服务器验不验,目前都没有任何约束;
- 于是不登录、直接请求
/depts、/emps、/report/*,照样返回 200 和数据——登录这个动作"白做了"; - 需求说得很清楚:只有登录成功,才能访问后台数据。这就需要第二块东西——登录校验。
从82 篇开始解决它。登录校验要想清楚两件事:登录标记(登录成功后,后续每次请求都能拿到这个标记——靠会话技术,82 篇)和统一拦截(不用在每个接口里写校验代码,而是在请求进业务之前统一拦一道——靠过滤器 Filter、拦截器 Interceptor,83、84 篇)。
本机实测:登录成功与密码错误
TIP
本机实测(工程连的是本机 MySQL 的 tlias 库、用户名 root,课程示例密码 1234——动手时把 password 换成你自己 MySQL 的密码;emp 表里有账号 shinaian / 123456):
# ① 登录成功 POST /login {"username":"shinaian","password":"123456"}
{"code":1,"msg":"success","data":{
"id":1,"username":"shinaian","name":"施耐庵",
"token":"eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJzaGluYWlhbiIsImV4cCI6MTc5MDczNjM0NX0.zHEWXHS6ZSoz8L2klv-8fFZQODftFlp4r_Ps1jGJFic"}}
# ② 密码错误(username 不变,password 改成错的)
{"code":0,"msg":"用户名或密码错误","data":null}对着接口文档逐项核对:
返回的
data就是LoginInfo:id、username、name三项来自查到的那个员工,token是 Service 里JwtUtils.generateToken(claims)生成的;token是一枚标准的 JWT:三段用.分隔,中间那段 Base64 解出来是json{"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 篇) |
相关
练习题
一、知识回顾(读完直接做下面的实践题)
- 第 12 章的两块内容:登录功能(登录成功后下发令牌)与登录校验(后续每个请求都要带令牌、由统一拦截器校验);本章四篇的分工是 81 登录功能 / 82 会话技术与JWT令牌 / 83 过滤器Filter / 84 拦截器Interceptor
- 登录成功的判定与本质:用户名和密码都输入正确才算成功;本质是查询——根据用户名和密码查询员工信息(查到了就是成功,查不到就是失败,不需要新增/修改任何数据)
- 登录接口的约定:
POST /login,请求体格式application/json,参数username、password;响应data是{id, username, name, token}四项(code1 成功 0 失败) - 三层的分工(PPT 第 8 页):Controller 接收请求参数(用户名、密码)、调用 Service、响应结果;Service 根据用户名和密码查询员工信息、判定并组装数据返回;Mapper 执行
select ... from emp where username = ? and password = ? - Controller 的写法:
@RestController+@PostMapping("/login"),参数用@RequestBody接收 JSON 并封装成Emp;Service 返回null就Result.error("用户名或密码错误"),否则Result.success(info) - Service 的写法:先查(
empMapper.selectByUsernameAndPassword(emp))→ 查到就组装claims(放id、username)生成令牌 →new LoginInfo(e.getId(), e.getUsername(), e.getName(), jwt)返回;查不到返回null(name来自数据库,不是请求) - Mapper 的写法:
@Select("select id, username, name from emp where username = #{username} and password = #{password}"),返回单个Emp;查不到返回null(这就是 Service 判定的依据);#{}是预编译参数 - 登录结果封装类:
LoginInfo——id、username、name、token四个字段,与接口文档data一一对应;类上@Data、@NoArgsConstructor、@AllArgsConstructor - 本机实测(本机
tlias库):登录成功 →{"code":1,"msg":"success","data":{"id":1,"username":"shinaian","name":"施耐庵","token":"eyJ…"}};密码错误 →{"code":0,"msg":"用户名或密码错误","data":null};令牌中间那段 Base64 解出来是{"id":1,"username":"shinaian","exp":1790736345} - 联调暴露的问题:未登录也能直接访问部门管理、员工管理等功能;需求是"只有员工登录成功,才可以访问后台系统中的数据"——由此引出登录标记 + 统一拦截的登录校验(82~84 篇)
二、裸写题
- 提示(先自己想,实在想不出再点开)
一级 · 思路:一条带两个条件的查询,条件来自前端传的两个参数;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);- 参考答案(做完再点开)
java/** * 根据用户名和密码查询员工 */ @Select("select id, username, name from emp where username = #{username} and password = #{password}") Emp selectByUsernameAndPassword(Emp emp);回答:MyBatis 查不到结果时返回
null——Service 里正是拿这个null当"登录失败"的判定依据。 为什么不查password:登录只需要"这个人是谁"(id、用户名、姓名),把密码查出来再往响应里传,等于把库里的密码泄露给前端和日志(log.info("登录成功, 员工信息: {}")会把实体连同密码一起打出来);课程工程里这条 SQL 只 select 了三列,就是为了少一事。 检查点:① 两个条件必须同时满足(用and连起来),只写用户名的话任何人随便填个密码都能进;② 值为null时不能用=比较(那是另一回事,这里两个参数都是必填)。- 提示(先自己想,实在想不出再点开)
一级 · 思路:控制层只做"收、调、包"三件事——收请求体里的两个字段、调业务层的登录方法、包成统一响应返回;判断成没成靠的是业务层返回的对象是不是
null二级 · 方法:类上@RestController(顺带@Slf4j打日志),方法上@PostMapping("/login");参数上加@RequestBody(把 JSON 请求体封装成对象);用Result.success(...)/Result.error("用户名或密码错误")三级 · 骨架:@____("/login")+public Result login(@____ Emp emp){ … };if (info != ____) { return Result.____(info); } return Result.____("用户名或密码错误");- 参考答案(做完再点开)
java@Slf4j @RestController public class LoginController { @Autowired private EmpService empService; /** * 登录 */ @PostMapping("/login") public Result login(@RequestBody Emp emp){ log.info("登录: {}", emp); LoginInfo info = empService.login(emp); if (info != null){ return Result.success(info); } return Result.error("用户名或密码错误"); } }检查点:① 必须用
@RequestBody——不写的话 Spring 会去取 URL 上的查询参数,而登录的两个参数是在请求体里的(application/json);② 路径就是/login,接口文档定死的;③ 成与败都用Result包起来,前端才不用做两套解析。- 提示(先自己想,实在想不出再点开)
一级 · 思路:三步走——① 调用数据访问层的方法,把请求对象原样传进去;② 判断返回的对象是不是
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(...)三级 · 骨架:java@Override public LoginInfo login(Emp emp) { Emp e = empMapper.____(emp); // 1. 查 if (e != ____) { // 2. 判 Map<String, Object> claims = new HashMap<>(); claims.put("id", ____); claims.put("username", ____); String jwt = ____.generateToken(claims); return new ____(e.getId(), e.getUsername(), e.getName(), ____); // 3. 组装 } return null; }- 参考答案(做完再点开)
java@Override public LoginInfo login(Emp emp) { //1. 调用mapper接口, 根据用户名和密码查询员工信息 Emp e = empMapper.selectByUsernameAndPassword(emp); //2. 判断: 判断是否存在这个员工, 如果存在, 组装登录成功信息 if(e != null){ log.info("登录成功, 员工信息: {}", e); //生成JWT令牌 Map<String, Object> claims = new HashMap<>(); claims.put("id", e.getId()); claims.put("username", e.getUsername()); String jwt = JwtUtils.generateToken(claims); return new LoginInfo(e.getId(), e.getUsername(), e.getName(), jwt); } //3. 不存在, 返回null return null; }检查点:① 判断依据是"查回来的对象是不是
null",不是"密码是不是空的";②LoginInfo的四个参数顺序别弄反(id、username、name、token——接口文档就是这个顺序),这是全参构造最容易错的地方;③name取e.getName()(数据库里的姓名),别拿请求里的emp凑。- 提示(先自己想,实在想不出再点开)
一级 · 思路:① 从"怎么判断用户名密码对不对"想到"拿条件去表里找记录";② 从接口文档"备注说明"里找答案(请求头、状态码);③ 从"登录接口只是发令牌、没人查令牌"这条线想 二级 · 方法:关键词——一次带条件的查询、
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 篇)。
三、综合题
- 打开接口文档的"6.1 登录",把请求路径、请求方式、请求参数、响应字段抄进练习文件;
- 建封装类
LoginInfo(id、username、name、token 四个字段 + Lombok 三件套); - 数据访问层:在
EmpMapper里加"按用户名和密码查员工"的方法(只查 id、username、name 三列,用预编译参数占位); - 业务层:
EmpService声明登录方法,EmpServiceImpl实现——查员工 → 判定 →(查到则)生成令牌 → 组装LoginInfo返回,查不到返回null; - 控制层:
LoginController里写POST /login——收请求体、调业务层、成功返回Result.success(info)、失败返回Result.error("用户名或密码错误"); - 启动应用,发两次请求:先发一次正确的用户名密码,再发一次错误的密码,把两段响应原样抄到练习文件(连接信息用你自己 MySQL 的
tlias库); - 从成功的那段响应里把
token抄出来,数一数它是几段、用哪个字符分隔; - 回答: ① Service 返回
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. 四段代码与课程工程一致:java@Data @NoArgsConstructor @AllArgsConstructor public class LoginInfo { private Integer id; // 员工ID private String username; // 用户名 private String name; // 姓名 private String token; // 令牌 }java@Select("select id, username, name from emp where username = #{username} and password = #{password}") Emp selectByUsernameAndPassword(Emp emp);java@Override public LoginInfo login(Emp emp) { Emp e = empMapper.selectByUsernameAndPassword(emp); if (e != null) { log.info("登录成功, 员工信息: {}", e); Map<String, Object> claims = new HashMap<>(); claims.put("id", e.getId()); claims.put("username", e.getUsername()); String jwt = JwtUtils.generateToken(claims); return new LoginInfo(e.getId(), e.getUsername(), e.getName(), jwt); } return null; }java@PostMapping("/login") public Result login(@RequestBody Emp emp){ log.info("登录: {}", emp); LoginInfo info = empService.login(emp); if (info != null){ return Result.success(info); } return Result.error("用户名或密码错误"); } - 本机实测两段响应:text
# 正确 {"username":"shinaian","password":"123456"} {"code":1,"msg":"success","data":{ "id":1,"username":"shinaian","name":"施耐庵", "token":"eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJzaGluYWlhbiIsImV4cCI6MTc5MDczNjM0NX0.zHEWXHS6ZSoz8L2klv-8fFZQODftFlp4r_Ps1jGJFic"}} # 密码错误 {"code":0,"msg":"用户名或密码错误","data":null} - 令牌是三段,用
.分隔(头、载荷、签名);中间那段 Base64 解出来是{"id":1,"username":"shinaian","exp":1790736345}——正是claims里放进去的 id、username 和过期时间。 - ① 一句话不区分两种失败原因,是为了不给攻击者送情报:如果提示"用户名不存在",对方就能拿这个接口慢慢试出系统里有哪些用户名;统一回"用户名或密码错误",猜的人分不清是自己错在哪。② 这枚令牌目前没有任何地方校验——它只是被发出来、躺在响应里;在82 篇把令牌的生成与解析讲透之后,83、84 篇会用过滤器/拦截器把它"验"起来:每个请求先检查请求头
token有没有、合不合法,不合法直接 401。
