第一部分:拦截器与ThreadLocal(课次29)
一、ThreadLocal概述
作用: 每个线程独立存储变量,互不干扰,用于在同一请求链路中传递用户信息。
方法 说明
set(T value) 存入当前线程
get() 获取当前线程的值
remove() 移除(防止内存泄漏)
二、UserContext工具类
java
/**
-
用户上下文,存储当前请求的用户ID
*/
public class UserContext {private static final ThreadLocal
CURRENT_USER = new ThreadLocal<>(); public static void setCurrentUserId(Integer userId) {
CURRENT_USER.set(userId);
}public static Integer getCurrentUserId() {
return CURRENT_USER.get();
}public static void remove() {
CURRENT_USER.remove();
}
}
三、Token校验拦截器
拦截器实现
java
@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
// 不需要拦截的路径
private static final List
"/api/auth/login",
"/api/auth/register",
"/api/hello"
);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 1. 检查是否放行路径
String path = request.getRequestURI();
if (EXCLUDE_PATHS.stream().anyMatch(path::startsWith)) {
return true;
}
// 2. 从请求头获取Token
String token = request.getHeader("Authorization");
if (token == null || token.isEmpty()) {
throw new BusinessException(401, "未登录,请先登录");
}
// 3. 去掉Bearer前缀
if (token.startsWith("Bearer ")) {
token = token.substring(7);
}
// 4. 验证Token
if (!jwtUtil.validateToken(token)) {
throw new BusinessException(401, "Token无效或已过期");
}
// 5. 解析用户ID存入ThreadLocal
Integer userId = jwtUtil.getUserIdFromToken(token);
UserContext.setCurrentUserId(userId);
log.info("用户 {} 访问: {}", userId, path);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 请求结束后清除,防止内存泄漏
UserContext.remove();
}
}
注册拦截器
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private TokenInterceptor tokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/api/**") // 拦截所有/api路径
.excludePathPatterns( // 排除登录注册
"/api/auth/login",
"/api/auth/register"
);
}
}
四、在Controller中使用
java
@RestController
@RequestMapping("/api/news")
public class NewsController {
@PostMapping
public Result
// 直接从ThreadLocal获取当前用户ID
Integer currentUserId = UserContext.getCurrentUserId();
news.setAuthorId(currentUserId);
newsService.save(news);
return Result.success("发布成功");
}
}
第二部分:新闻模块CRUD(课次30)
五、RESTful API设计
方法 路径 说明
POST /api/news 发布新闻
GET /api/news/page 分页列表
GET /api/news/{id} 新闻详情
PUT /api/news/{id} 修改新闻
DELETE /api/news/{id} 删除新闻
六、News实体(补充字段)
java
@Data
@TableName("news")
public class News {
@TableId(type = IdType.AUTO)
private Integer id;
private String title;
private String content;
private Integer authorId;
private String authorName;
private Integer viewCount;
private Integer status; // 0-草稿,1-已发布
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
private Integer deleted;
}
七、新闻Service
java
@Service
public class NewsService {
@Autowired
private NewsMapper newsMapper;
// 发布
public boolean save(News news) {
return newsMapper.insert(news) > 0;
}
// 分页查询(只查已发布)
public Page
Page
LambdaQueryWrapper
wrapper.eq(News::getStatus, 1)
.orderByDesc(News::getCreateTime);
return newsMapper.selectPage(page, wrapper);
}
// 详情
public News getById(Integer id) {
return newsMapper.selectById(id);
}
// 修改(只能修改自己的)
public boolean update(News news, Integer currentUserId) {
News exist = newsMapper.selectById(news.getId());
if (exist == null || !exist.getAuthorId().equals(currentUserId)) {
throw new BusinessException("无权限修改该新闻");
}
return newsMapper.updateById(news) > 0;
}
// 删除(只能删除自己的)
public boolean delete(Integer id, Integer currentUserId) {
News exist = newsMapper.selectById(id);
if (exist == null || !exist.getAuthorId().equals(currentUserId)) {
throw new BusinessException("无权限删除该新闻");
}
return newsMapper.deleteById(id) > 0;
}
}
八、新闻Controller
java
@Slf4j
@RestController
@RequestMapping("/api/news")
public class NewsController {
@Autowired
private NewsService newsService;
// 1. 发布新闻
@PostMapping
public Result
news.setAuthorId(UserContext.getCurrentUserId());
news.setStatus(1); // 直接发布
newsService.save(news);
return Result.success("发布成功");
}
// 2. 分页列表
@GetMapping("/page")
public Result<Page
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
return Result.success(newsService.getPage(pageNum, pageSize));
}
// 3. 新闻详情(增加浏览量)
@GetMapping("/{id}")
public Result
News news = newsService.getById(id);
if (news == null) {
throw new BusinessException(404, "新闻不存在");
}
// 增加浏览量
news.setViewCount(news.getViewCount() + 1);
newsService.update(news, UserContext.getCurrentUserId()); // 这里只更新浏览量,不校验权限
return Result.success(news);
}
// 4. 修改新闻
@PutMapping("/{id}")
public Result
news.setId(id);
newsService.update(news, UserContext.getCurrentUserId());
return Result.success("修改成功");
}
// 5. 删除新闻
@DeleteMapping("/{id}")
public Result
newsService.delete(id, UserContext.getCurrentUserId());
return Result.success("删除成功");
}
}
九、权限控制核心逻辑
java
// 关键:只有作者本人才能修改/删除
News exist = newsMapper.selectById(id);
if (!exist.getAuthorId().equals(UserContext.getCurrentUserId())) {
throw new BusinessException("无权限操作该新闻");
}
第三部分:前后端联调(课次31)
十、联调准备
后端启动
bash
1. 确保MySQL已启动
2. 启动SpringBoot
mvn spring-boot:run
或直接运行 NewsApplication.java
前端启动
bash
cd weitoutiao-front
npm install
npm run serve
访问 http://localhost:8080
十一、常见问题排查
问题 可能原因 解决方案
跨域报错 前后端端口不同 后端添加CORS配置
401未授权 未携带Token或Token过期 检查请求头Authorization
404 路径错误 检查@PathVariable与URL一致
500 后端异常 查看后端日志
CORS跨域配置
java
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:8080")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true);
}
};
}
}
十二、请求示例
登录获取Token
bash
POST /api/auth/login
{"username":"admin","password":"123456"}
→ 返回token
发布新闻(携带Token)
bash
POST /api/news
Headers: Authorization: Bearer eyJhbGc...
Body: {"title":"标题","content":"内容"}
分页查询
bash
GET /api/news/page?pageNum=1&pageSize=10
Headers: Authorization: Bearer token
修改新闻
bash
PUT /api/news/1
Headers: Authorization: Bearer token
Body: {"title":"新标题","content":"新内容"}
附录:知识点速查
拦截器流程
text
请求 → 拦截器preHandle → 解析Token → 存入ThreadLocal → Controller处理 → 拦截器afterCompletion → 清除ThreadLocal
ThreadLocal使用注意
java
// ⚠️ 必须在使用完后调用remove(),防止内存泄漏
@Override
public void afterCompletion(...) {
UserContext.remove();
}
权限控制模式
层级 控制方式
Controller层 UserContext.getCurrentUserId() 获取当前用户
Service层 比对资源作者ID与当前用户ID
异常 无权限抛出 BusinessException
浙公网安备 33010602011771号