6-18笔记

*一、整体知识脉络*

课次27 ─→ 课次28 ─→ 课次29 ─→ 课次30
统一返回 JWT认证 拦截器校验 业务CRUD
全局异常 登录接口 ThreadLocal 权限控制

逻辑链条:

\1. 先规范API返回格式(统一封装),让前后端对接有标准

\2. 再做用户认证(JWT),让用户能登录获取令牌

\3. 然后做接口保护(拦截器),让未登录用户访问不了业务接口

\4. 最后实现业务功能(新闻CRUD),在保护下完成增删改查

*二、课次27:统一结果封装 & 全局异常处理*

*2.1 为什么要统一返回格式?*

不同接口返回格式混乱,前端难以处理:

• 有的返回 { "msg": "ok", "data": ... }

• 有的返回 { "status": 200, "result": ... }

• 有的直接返回裸数据

统一标准格式:

// json**
**{
"code": 200,
"message": "success",
"data": { ... }
}

*2.2 Result 类设计(泛型封装)*

// java**
**@Data
public class Result {
private Integer code; // 状态码:200成功,500失败
private String message; // 提示信息
private T data; // 返回数据(泛型)

// 私有构造,防止外部new
private Result(Integer code, String message, T data) { ... }

// 静态工厂方法(核心设计模式)
public static Result success(T data) { ... } // 成功,带数据
public static Result success(String message) { ... } // 成功,带消息
public static Result error(String message) { ... } // 失败
}

关键知识点:

• 使用泛型 让 data 可以承载任意类型数据

• 构造方法私有化 + 静态工厂方法,调用更简洁

• 使用 Lombok @Data 自动生成 getter/setter

调用方式:

// java**
**// 成功 - 带数据
return Result.success(userList);

// 成功 - 只带消息
return Result.success("操作成功");

// 失败
return Result.error("用户名或密码错误");

*2.3 全局异常处理器*

问题: 如果代码中抛出异常,会返回什么?

• 默认返回Spring的白色标签页(HTML),前端无法处理

解决: @RestControllerAdvice + @ExceptionHandler

// java**
**@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public Result<?> handleRuntimeException(RuntimeException e) {
e.printStackTrace(); // 打印堆栈(方便调试)
return Result.error(e.getMessage()); // 返回统一格式的错误
}
}

原理:

• @RestControllerAdvice = @ControllerAdvice + @ResponseBody

• 它会"拦截"所有Controller抛出的异常

• @ExceptionHandler(RuntimeException.class) 指定处理哪类异常

• 可以定义多个 @ExceptionHandler 处理不同类型的异常

执行流程:

Controller抛出RuntimeException

GlobalExceptionHandler 捕获

返回 Result.error(e.getMessage())

前端收到 { code: 500, message: "xxx", data: null }

*2.4 Postman 测试要点*

• 新建 Collection 组织请求

• GET 请求直接填 URL 点 Send

• 返回结果在 Body 中选 JSON 格式查看

*三、课次28:JWT工具类 & 登录接口*

*3.1 JWT 核心概念*

JWT(JSON Web Token) = 无状态认证方案

传统Session vs JWT:

对比项 Session JWT
存储位置 服务端 客户端
状态 有状态 无状态
扩展性 差(分布式问题) 好(服务端不存状态)
安全性 较高 中等(需保护密钥)

JWT 三段结构:

Header.Payload.Signature
↓ ↓ ↓
算法信息 用户数据 签名(防篡改)

*3.2 依赖配置*

// xml    io.jsonwebtoken    jjwt    0.9.1    javax.xml.bind    jaxb-api    2.3.1

为什么需要 jaxb-api?

JDK 9 开始移除了 javax.xml.bind 模块,而 JJWT 内部使用了它来做 Base64 编解码。

*3.3 application.yml 配置*

// yaml**
**jwt:
secret: weitoutiao_secret_key_2025 # 签名密钥(不能泄露!)
expire: 604800000 # 过期时间(毫秒)= 7天

使用 @Value 注入配置:

// java**
**@Value("${jwt.secret}")
private String secret;

@Value("${jwt.expire}")
private Long expire;

*3.4 JwtUtil 工具类(核心)*

// java**
**@Component
public class JwtUtil {

// 生成Token
public String generateToken(Integer userId, String username) {
Date now = new Date();
Date expiration = new Date(now.getTime() + expire);
return Jwts.builder()
.setSubject(String.valueOf(userId)) // 主题:用户ID
.claim("username", username) // 自定义声明:用户名
.setIssuedAt(now) // 签发时间
.setExpiration(expiration) // 过期时间
.signWith(SignatureAlgorithm.HS256, secret) // 签名算法+密钥
.compact(); // 生成字符串
}

// 解析Token
public Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}

// 从Token提取用户ID
public Integer getUserIdFromToken(String token) {
return Integer.parseInt(parseToken(token).getSubject());
}

// 验证Token是否有效
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (Exception e) {
return false; // 过期、篡改、格式错误 → 返回false
}
}
}

关键方法解析:

方法 作用 返回
generateToken 根据用户信息生成token token字符串
parseToken 解析token获取Claims Claims对象
getUserIdFromToken 从token提取用户ID Integer
validateToken 验证token是否有效 boolean

*3.5 登录接口完整流程*

前端发送 POST /user/login

接收 UserLoginDTO(username, password)

UserService.login(username, password)

LambdaQueryWrapper 构建查询条件

数据库查询:username = ? AND password = ?

┌── 查到用户 → JwtUtil.generateToken() → 返回token
└── 未查到 → 返回null → Result.error("用户名或密码错误")

Service 层代码要点:

// java**
**// MyBatis-Plus 的 LambdaQueryWrapper(类型安全,避免写错字段名)
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, username)
.eq(User::getPassword, password);
User user = this.getOne(wrapper);

注意: this.getOne() 要求查询结果唯一,如果有多条会报错。这里 username 应该是唯一的。

3.6 DTO 设计

** java**
@Data
public class UserLoginDTO {
private String username;
private String password;
}

为什么要用 DTO?

不直接暴露实体类(Entity)给前端

可以控制前端能传哪些字段(安全性)

可以做字段校验(后续可加 @NotBlank 等注解)

四、课次29:拦截器 Token 校验 + ThreadLocal

4.1 为什么需要拦截器?

问题: 登录接口返回了 token,但其他接口怎么知道用户已登录?

解决: 每次请求都在拦截器中验证 token。

拦截器执行位置:

请求 → 拦截器(preHandle) → Controller → 拦截器(afterCompletion) → 响应
↑ ↑
验证token,通过才放行 清理ThreadLocal

4.2 ThreadLocal 是什么?

一句话: 每个线程的"私有储物柜",线程之间互不干扰。

为什么需要 ThreadLocal?

请求A(用户1)──→ Thread-1 ──→ 需要知道当前用户是1
请求B(用户2)──→ Thread-2 ──→ 需要知道当前用户是2

如果用一个全局变量存用户ID,多个请求会互相覆盖!

ThreadLocal 让每个线程有自己的副本,互不影响。

使用方式:

** java**
public class UserContext {
// ThreadLocal:存储Integer类型的线程局部变量
private static final ThreadLocal currentUserId = new ThreadLocal<>();

// 设置当前用户ID(拦截器中调用)
public static void setCurrentUserId(Integer userId) {
currentUserId.set(userId);
}

// 获取当前用户ID(Controller/Service中调用)
public static Integer getCurrentUserId() {
return currentUserId.get();
}

// 清除(必须!防止内存泄漏)
public static void clear() {
currentUserId.remove();
}
}

⚠️ 重要:必须在请求结束后调用 clear(),否则线程池复用线程时会导致数据泄漏!

4.3 JwtInterceptor 拦截器

java
@Component
public class JwtInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
1. 从请求头获取 token
String token = request.getHeader("Authorization");

2. 检查格式:Bearer
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7); // 去掉 "Bearer " 前缀

3. 验证 token
if (jwtUtil.validateToken(token)) {
4. 提取用户ID,存入 ThreadLocal
Integer userId = jwtUtil.getUserIdFromToken(token);
UserContext.setCurrentUserId(userId);
return true; // 放行
}
}

5. token无效 → 返回401
response.setStatus(401);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{"code":401,"message":"未登录或token失效"}");
return false; // 拦截
}

@Override
public void afterCompletion(...) {
UserContext.clear(); // ⚠️ 必须清理!
}
}

请求头格式(HTTP标准):

Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx

4.4 拦截器配置

** java**
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/user/login", "/user/register"); // 排除登录注册
}
}

配置逻辑:

• addPathPatterns("/**"):拦截所有路径

• excludePathPatterns(...):白名单,不需要登录就能访问的接口

4.5 完整认证流程

\1. 用户登录 → POST /user/login → 返回 token
\2. 前端保存 token(localStorage / cookie)
\3. 后续请求:
请求头加 Authorization: Bearer

\4. 拦截器 preHandle:
├── 提取 token
├── 验证有效性
├── 提取 userId → 存入 ThreadLocal
└── 放行 / 拦截

\5. Controller 中:
Integer userId = UserContext.getCurrentUserId();

\6. 请求结束 → afterCompletion → UserContext.clear()

五、课次30:新闻模块 CRUD

5.1 RESTful API 设计规范

操作 HTTP方法 URL 说明
创建 POST /news/add 提交新资源
查询列表 GET /news/page 获取资源列表
查询详情 GET /news/ 获取单个资源
修改 PUT /news/ 更新资源
删除 DELETE /news/ 删除资源

RESTful 核心思想: URL 代表资源,HTTP方法代表操作。

5.2 新闻发布(Create)

java
@PostMapping("/add")
public Result<?> addNews(@RequestBody NewsAddDTO dto) {
// 从 ThreadLocal 获取当前用户ID(拦截器已经设置好了)
Integer userId = UserContext.getCurrentUserId();
if (userId == null) return Result.error("请先登录");

News news = new News();
news.setUserId(userId); // 关联当前用户
news.setTitle(dto.getTitle());
news.setContent(dto.getContent());
news.setViewCount(0); // 初始浏览量为0

return newsService.save(news)
? Result.success("发布成功")
: Result.error("发布失败");
}

要点:

@RequestBody 接收 JSON 请求体,自动反序列化为 DTO

UserContext.getCurrentUserId() 获取当前登录用户

浏览量初始化为0

5.3 分页查询(Read - List)

java
@GetMapping("/page")
public Result<Page> listNews(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {

Page pageObj = new Page<>(page, size);
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.orderByDesc(News::getPublishTime); // 按发布时间倒序

return Result.success(newsService.page(pageObj, wrapper));
}

MyBatis-Plus 分页要点:

Page 封装分页参数(当前页、每页大小)

查询结果自动包含:records(数据)、total(总数)、pages(总页数)等

orderByDesc 保证最新的在前面

返回格式示例:

json
{
"code": 200,
"message": "success",
"data": {
"records": [ ... ],
"total": 50,
"size": 10,
"current": 1,
"pages": 5
}
}

5.4 新闻详情(Read - Detail)

java
@GetMapping("/{id}")
public Result getNewsDetail(@PathVariable Integer id) {
News news = newsService.getById(id);
if (news == null) return Result.error("新闻不存在");

// 浏览量+1
news.setViewCount(news.getViewCount() + 1);
newsService.updateById(news);

return Result.success(news);
}

要点:

@PathVariable 获取 URL 中的路径参数

查看详情的同时累加浏览量(简单实现)

5.5 修改新闻(Update)+ 权限控制*

java
@PutMapping("/{id}")
public Result<?> updateNews(@PathVariable Integer id, @RequestBody NewsUpdateDTO dto) {
Integer userId = UserContext.getCurrentUserId();
News news = newsService.getById(id);

if (news == null) return Result.error("新闻不存在");
if (!news.getUserId().equals(userId)) return Result.error("只能修改自己的新闻");

news.setTitle(dto.getTitle());
news.setContent(dto.getContent());
newsService.updateById(news);
return Result.success("修改成功");
}

权限控制逻辑:

获取当前用户ID → 查询新闻 → 比较新闻作者ID和当前用户ID

相同 → 允许修改
不同 → 返回错误 "只能修改自己的新闻"

5.6 删除新闻(Delete)

java
@DeleteMapping("/{id}")
public Result<?> deleteNews(@PathVariable Integer id) {
Integer userId = UserContext.getCurrentUserId();
News news = newsService.getById(id);

if (news == null) return Result.error("新闻不存在");
if (!news.getUserId().equals(userId)) return Result.error("只能删除自己的新闻");

newsService.removeById(id);
return Result.success("删除成功");
}

注意: 这里用的是 removeById(物理删除)。如需逻辑删除,可在实体类字段上加 @TableLogic 注解。

六、核心知识点汇总

6.1 注解速查表

注解 作用 位置
@RestControllerAdvice 全局异常处理器 类级别
@ExceptionHandler 指定处理的异常类型 方法级别
@Value 注入配置文件中的值 字段级别
@RequestBody 将请求体JSON转为Java对象 参数级别
@PathVariable 获取URL路径参数 参数级别
@RequestParam 获取查询参数 参数级别
@Component 注册为Spring Bean 类级别

6.2 分层架构

Controller(控制层)
↓ 调用
Service(业务层)
↓ 调用
Mapper(数据访问层)
↓ 操作
Database(数据库)

各层职责:

Controller:接收请求、参数校验、调用Service、返回结果

Service:业务逻辑处理

Mapper:数据库操作(CRUD)

DTO:数据传输对象(前端 ↔ 后端)

Entity:数据库表映射

6.3 认证授权完整链路*

┌─────────────────────────────────────────────────┐
│ 前端 │
│ 1. POST /user/login → 获取token │
│ 2. 保存token到localStorage │
│ 3. 后续请求Header加 Authorization: Bearer
└─────────────────────┬───────────────────────────┘

┌─────────────────────────────────────────────────┐
│ Spring Boot 后端 │
│ │
│ 请求进入 │
│ ↓ │
│ JwtInterceptor.preHandle() │
│ ├── 提取 Authorization 头 │
│ ├── 去掉 "Bearer " 前缀 │
│ ├── JwtUtil.validateToken() 验证 │
│ ├── JwtUtil.getUserIdFromToken() 提取用户ID │
│ ├── UserContext.setCurrentUserId() 存入 │
│ └── return true → 放行 │
│ ↓ │
│ Controller │
│ └── UserContext.getCurrentUserId() 获取用户 │
│ ↓ │
│ afterCompletion() → UserContext.clear() 清理 │
└─────────────────────────────────────────────────┘

七、Postman 测试流程速查*

7.1 登录获取Token

POST http://localhost:8080/user/login
Body (JSON):
{
"username": "testuser",
"password": "123456"
}
→ 复制返回的 token

7.2 带Token请求*

POST http://localhost:8080/news/add
Headers:
Authorization: Bearer <粘贴token>
Body (JSON):
{
"title": "新闻标题",
"content": "新闻内容"
}

⚠️ 注意: Bearer 和 token 之间有一个空格

八、常见踩坑点*

问题 原因 解决
Token验证失败 token前没加"Bearer "或多了空格 检查前端传值格式
ThreadLocal取到null 请求没经过拦截器或被排除 检查拦截器配置路径
getOne() 报错 查询结果不唯一 确保查询字段有唯一约束
JJWT报错 NoClassDefFoundError 缺少 jaxb-api 依赖 添加 jaxb-api 依赖
内存泄漏 ThreadLocal没清理 在 afterCompletion 中 clear()
分页返回空 MyBatis-Plus 分页插件未配置 需配置 MybatisPlusInterceptor

九、工程目录结构*

com.weitoutiao/
├── common/
│ ├── Result.java ← 统一返回封装
│ └── GlobalExceptionHandler.java ← 全局异常处理
├── config/
│ └── InterceptorConfig.java ← 拦截器配置
├── controller/
│ ├── HelloController.java ← 测试接口
│ ├── UserController.java ← 用户(登录)
│ └── NewsController.java ← 新闻CRUD
├── dto/
│ ├── UserLoginDTO.java ← 登录请求体
│ ├── NewsAddDTO.java ← 新增新闻请求体
│ └── NewsUpdateDTO.java ← 修改新闻请求体
├── entity/
│ ├── User.java ← 用户实体
│ └── News.java ← 新闻实体
├── interceptor/
│ └── JwtInterceptor.java ← JWT拦截器
├── mapper/
│ ├── UserMapper.java ← 用户数据访问
│ └── NewsMapper.java ← 新闻数据访问
├── service/
│ ├── UserService.java ← 用户业务接口
│ ├── NewsService.java ← 新闻业务接口
│ └── impl/
│ ├── UserServiceImpl.java ← 用户业务实现
│ └── NewsServiceImpl.java ← 新闻业务实现
└── util/
├── JwtUtil.java ← JWT工具类
└── UserContext.java ← ThreadLocal用户上下文

posted @ 2026-06-18 15:02  伊沃·凡特森  阅读(3)  评论(0)    收藏  举报