登录功能

登录功能

2026年9月29日·#编程学习/javaweb学习笔记JavaWeb/后端/SpringBoot·7827 字 39 分钟
浏览量加载中...
AI 摘要- DeepSeek

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

封面图
登录功能 - 封面图

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
图:第 12 章开讲时 Tlias 的样子——这是班级管理页面,列表、查询、新增、修改、删除都已经做好并能用了;第 2~3 页把系统现有的页面挨个展示了一遍,本章要补的就是这些页面门口的那道”门禁”

PPT 第 5 页把本章拆成两块:

登录功能(01) → 登录校验(02)

第 6 页打出”01 登录功能”,正片开始。本章四篇笔记的分工如下:

编号标题PPT 页干什么
81(本篇)登录功能1~9按接口文档把 POST /login 做出来:用户名密码查库、成功返回登录信息 + 令牌
82会话技术与JWT令牌10~28登录校验的”登录标记”这一半:会话技术三种方案对比 + JWT 令牌的生成与解析
83过滤器Filter29~42用过滤器把令牌校验统一拦在所有请求前面
84拦截器Interceptor43~53同样的事用 Spring 的拦截器再做一遍,并对比两者

思路分析:登录功能的本质是查询(PPT 第 7 页)#

PPT 第 7 页一口气问了两件事,答案都很短:

PPT 的问题PPT 的答案
怎么样才算登录成功了呢?用户名和密码都输入正确,登录成功;否则,登录失败
登录功能的本质是什么?查询——根据用户名和密码查询员工信息

把这两句翻译成数据库的语言:用户在前端填的 username、password 就是两个查询条件,把它俩拿到 emp 表里”对一对”——

  • 查到了:说明数据库里存在”用户名 = 这个、密码 = 这个”的员工 → 登录成功。顺带还把这个人查出来了(id、姓名都有),后面”我是谁”的信息就从他身上取;
  • 查不到:这对用户名密码不匹配任何员工 → 登录失败,给前端一句”用户名或密码错误”。

所以登录接口从头到尾不需要新增、修改任何数据,它就是一次带条件的查询加一次判断。想清楚这一点,三层代码怎么写心里就有数了。

Tlias 的登录页面原型
图:Tlias 的登录页面原型(用户名、密码两个输入框 + 登录/重置按钮)。这一篇写的接口就是给页面上那个”登录”按钮用的:把两个框里的文字以 JSON 发过来,后端告诉它成没成、并把令牌发下去

接口文档:/login 的约定(PPT 第 7 页 + 资料/02. 接口文档)#

Tlias 是接口文档驱动(57 篇和实战章反复强调过),登录接口也早就定好了。它在接口文档”6. 其他接口 → 6.1 登录”里:

项目约定
请求路径/login
请求方式POST
接口描述该接口用于员工登录 Tlias 智能学习辅助系统,登录完毕后,系统下发 JWT 令牌

请求参数(application/json):

名称类型是否必须备注
usernamestring必须用户名
passwordstring必须密码

请求数据样例:

{
"username": "jinyong",
"password": "123456"
}

响应 data 里的四个字段:

名称类型是否必须备注
codenumber必须响应码,1 成功;0 失败
msgstring非必须提示信息
dataobject必须返回的数据
data.idnumber必须员工 ID
data.usernamestring必须用户名
data.namestring必须姓名
data.tokenstring必须令牌

(接口文档里后四行是缩进写的”|- + 字段名”,表示它们是 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根据用户名和密码查询员工信息;判定,组装数据并返回
MapperSQL: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 那一格的展开,三步对得整整齐齐:

  1. 查询——把用户名密码丢给 Mapper,拿回一个 Emp(查不到就是 null);
  2. 判定 + 组装——查到了,说明用户名密码都对,开始组装登录成功的信息:
    • 先造一个 claims(Map),把这次要”装进令牌”的信息放进去:员工的 id 和 username。为什么要放?因为令牌是后续请求唯一的”身份证”——服务器要靠它认出”这次请求是谁发的”,令牌里就得带上能标识这个人的信息;
    • 调工具类 JwtUtils.generateToken(claims) 生成令牌字符串;
    • 用四参构造把 id、username、name、token 组装成 LoginInfo 返回。 注意 name 是从数据库里查出来的(e.getName()),请求里只有用户名和密码;
  3. 查不到就返回 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 解出来是

    {"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 篇)

相关#

练习题#

一、知识回顾(读完直接做下面的实践题)#

  1. 第 12 章的两块内容:登录功能(登录成功后下发令牌)与登录校验(后续每个请求都要带令牌、由统一拦截器校验);本章四篇的分工是 81 登录功能 / 82 会话技术与JWT令牌 / 83 过滤器Filter / 84 拦截器Interceptor
  2. 登录成功的判定与本质:用户名和密码都输入正确才算成功;本质是查询——根据用户名和密码查询员工信息(查到了就是成功,查不到就是失败,不需要新增/修改任何数据)
  3. 登录接口的约定:POST /login,请求体格式 application/json,参数 username、password;响应 data 是 {id, username, name, token} 四项(code 1 成功 0 失败)
  4. 三层的分工(PPT 第 8 页):Controller 接收请求参数(用户名、密码)、调用 Service、响应结果;Service 根据用户名和密码查询员工信息、判定并组装数据返回;Mapper 执行 select ... from emp where username = ? and password = ?
  5. Controller 的写法:@RestController + @PostMapping("/login"),参数用 @RequestBody 接收 JSON 并封装成 Emp;Service 返回 null 就 Result.error("用户名或密码错误"),否则 Result.success(info)
  6. Service 的写法:先查(empMapper.selectByUsernameAndPassword(emp))→ 查到就组装 claims(放 id、username)生成令牌 → new LoginInfo(e.getId(), e.getUsername(), e.getName(), jwt) 返回;查不到返回 null(name 来自数据库,不是请求)
  7. Mapper 的写法:@Select("select id, username, name from emp where username = #{username} and password = #{password}"),返回单个 Emp;查不到返回 null(这就是 Service 判定的依据);#{} 是预编译参数
  8. 登录结果封装类:LoginInfo——id、username、name、token 四个字段,与接口文档 data 一一对应;类上 @Data、@NoArgsConstructor、@AllArgsConstructor
  9. 本机实测(本机 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}
  10. 联调暴露的问题:未登录也能直接访问部门管理、员工管理等功能;需求是”只有员工登录成功,才可以访问后台系统中的数据”——由此引出登录标记 + 统一拦截的登录校验(82~84 篇)

二、裸写题#

  • 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);

    参考答案(做完再点开)
    /**
    * 根据用户名和密码查询员工
    */
    @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 时不能用 = 比较(那是另一回事,这里两个参数都是必填)。

  • 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.____("用户名或密码错误");

    参考答案(做完再点开)
    @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 包起来,前端才不用做两套解析。

  • 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(...) 三级 · 骨架:

    @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;
    }
    参考答案(做完再点开)
    @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 凑。

  • 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 照着接口文档,把登录功能从零做出来并联调 这一题把本篇从头到尾走一遍:先看文档 → 定封装类 → 写三层 → 启动验证 → 核对两种响应 → 回答问题。

    1. 打开接口文档的”6.1 登录”,把请求路径、请求方式、请求参数、响应字段抄进练习文件;
    2. 建封装类 LoginInfo(id、username、name、token 四个字段 + Lombok 三件套);
    3. 数据访问层:在 EmpMapper 里加”按用户名和密码查员工”的方法(只查 id、username、name 三列,用预编译参数占位);
    4. 业务层:EmpService 声明登录方法,EmpServiceImpl 实现——查员工 → 判定 →(查到则)生成令牌 → 组装 LoginInfo 返回,查不到返回 null;
    5. 控制层:LoginController 里写 POST /login——收请求体、调业务层、成功返回 Result.success(info)、失败返回 Result.error("用户名或密码错误");
    6. 启动应用,发两次请求:先发一次正确的用户名密码,再发一次错误的密码,把两段响应原样抄到练习文件(连接信息用你自己 MySQL 的 tlias 库);
    7. 从成功的那段响应里把 token 抄出来,数一数它是几段、用哪个字符分隔;
    8. 回答: ① 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){ … }

    参考答案(做完再点开)
    1. 接口文档:请求路径 /login、请求方式 POST、请求参数 username/password(application/json,都必填)、响应 data 为 {id, username, name, token}。 2~5. 四段代码与课程工程一致:
      @Data
      @NoArgsConstructor
      @AllArgsConstructor
      public class LoginInfo {
      private Integer id; // 员工ID
      private String username; // 用户名
      private String name; // 姓名
      private String token; // 令牌
      }
      @Select("select id, username, name from emp where username = #{username} and password = #{password}")
      Emp selectByUsernameAndPassword(Emp emp);
      @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;
      }
      @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("用户名或密码错误");
      }
    2. 本机实测两段响应:
      # 正确 {"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}
    3. 令牌是三段,用 . 分隔(头、载荷、签名);中间那段 Base64 解出来是 {"id":1,"username":"shinaian","exp":1790736345}——正是 claims 里放进去的 id、username 和过期时间。
    4. ① 一句话不区分两种失败原因,是为了不给攻击者送情报:如果提示”用户名不存在”,对方就能拿这个接口慢慢试出系统里有哪些用户名;统一回”用户名或密码错误”,猜的人分不清是自己错在哪。② 这枚令牌目前没有任何地方校验——它只是被发出来、躺在响应里;在82 篇把令牌的生成与解析讲透之后,83、84 篇会用过滤器/拦截器把它”验”起来:每个请求先检查请求头 token 有没有、合不合法,不合法直接 401。

评论区

[ 标签 ]
# AI37# AI 编程2# AI工具1# AI绘图1# AI辅助开发2# Ajax7# Apifox1# AstrBot3# Astro2# CC Switch1# CDN2# Claude Code1# claudecode2# ClaudeCode1# Cloudflare2# CloudFlare2# CloudFlare-ImgBed3# coc4# CSS10# DeepSeek6# deepseek2# DELETE1# Docker5# EdgeOne3# Gist1# git1# GitHub1# hexo-circle-of-friends1# HTML9# HTTP10# ImageManager1# Java23# java13# JavaScript11# JavaWeb113# JDBC7# JSON2# JUnit1# LangChain25# Linux4# Logback1# Maven17# Muse Spark1# Mybatis1# MyBatis18# MySQL45# MySql1# NapCat1# Node.js1# obsidian2# Obsidian5# OpenCode4# ORM1# PathVariable1# PicGo1# PyCharm1# Python70# RequestBody1# RequestMapping1# RESTful风格1# skills1# Slf4j1# SpringBoot37# SpringMVC5# SQL2# Streamlit5# Svelte2# TailwindCSS1# Telegram3# Tlias2# Vercel1# vscode2# Vue20# Waline3# Web3# WebDAV1# Web基础6# Web开发6# WinSCP1# YAML1# 三层架构1# 中二宣言1# 书籍1# 使用文档10# 写作1# 函数2# 刷步数1# 前端52# 动态1# 动漫1# 包1# 单词2# 博客8# 博客工作流1# 博客开发2# 参数接收1# 友链1# 反思2# 后端48# 图床6# 地图1# 备份2# 大模型1# 奇思妙想1# 存储1# 学习方法6# 学校1# 宝塔面板3# 宝宝10# 实战4# 对象1# 导航栏1# 工具2# 布局2# 开发1# 开发工具1# 开发规范1# 开心1# 异常处理1# 影视2# 微信1# 性能优化2# 总结1# 想法15# 感受1# 感悟11# 指南1# 提示词工程2# 插件5# 故障排除1# 效率工具2# 教程10# 数据分析9# 数据库27# 数据结构1# 文件操作2# 斩神1# 日常95# 日志框架1# 朋友圈1# 朱元璋1# 模块1# 模板1# 正则表达式2# 测试3# 游戏2# 爬虫7# 生活迁移1# 电影2# 电脑1# 碎碎念1# 视觉识别1# 类1# 类型注解1# 网络基础2# 网络教室1# 羊毛2# 脚本2# 脚本工具1# 自动化2# 蓝奏云1# 表单1# 订阅推荐2# 记录2# 评论系统1# 词根1# 词缀1# 说说1# 足迹1# 跑步2# 路径参数1# 转载2# 运动1# 部署11# 部落冲突1# 配置1# 随机图1# 面向对象5# 音乐3# 音标1# 项目实战11# 饮食1# 驼峰命名1# 高德地图1
[ 公告 ]

如果你喜欢,那么欢迎来到我的世界!

了解更多
[ 音乐 ]
封面

音乐

暂未播放

0:000:00
暂无歌词
找不到相关结果。
[ contents ]
[ 全部文章 ]