Spring Security 的默认配置很简单:一条过滤链管所有请求。但在实际项目中,不同端点往往有完全不同的认证需求。本文介绍我在个人博客项目中实现的三链 SecurityFilterChain 架构。
博客系统的端点可以分为三类:
/actuator/health(运维监控)、/api/v1/articles(文章列表)、Swagger 文档/api/** 业务接口/actuator/env、/actuator/beans)如果用一条链处理,逻辑会变成一堆 if-else,难以维护且容易出错。
Spring Security 6 支持声明多个 SecurityFilterChain Bean,通过 @Order 控制优先级——只有第一条匹配的链会处理请求。
请求进来
↓
@Order(1) permitAllChain → 匹配白名单路径? → 是 → 直接放行
↓ 否
@Order(2) jwtChain → 匹配 /api/**? → 是 → JWT 认证
↓ 否
@Order(3) defaultChain → 兜底:HTTP Basic 认证
@Bean
@Order(1)
public SecurityFilterChain permitAllChain(HttpSecurity http) throws Exception {
List<String> urls = getSafePermitAllUrls();
if (CollectionUtils.isEmpty(urls)) {
// 防御性设计:空列表时该链不匹配任何请求
http.securityMatcher(r -> false);
return http.build();
}
http.securityMatcher(urls.toArray(new String[0]))
.cors(Customizer.withDefaults())
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
白名单路径从配置文件读取,而不是硬编码:
# application.yaml
security:
permit-all-urls:
- /actuator/health
- /actuator/info
- /v3/api-docs/**
- /swagger-ui/**
- /swagger-ui.html
这样不同环境(开发/生产)可以开放不同的端点,无需修改代码。
@Bean
@Order(2)
public SecurityFilterChain jwtChain(HttpSecurity http) throws Exception {
http.securityMatcher("/api/**")
.cors(Customizer.withDefaults())
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
// 认证相关 - 公开
.requestMatchers("/api/v1/auth/register", "/api/v1/auth/login").permitAll()
// 文章、分类、标签 - 公开只读
.requestMatchers("/api/v1/articles", "/api/v1/articles/**").permitAll()
.requestMatchers("/api/v1/categories/**", "/api/v1/tags/**").permitAll()
// 评论查询 - 公开
.requestMatchers("/api/v1/comments/tree", "/api/v1/comments").permitAll()
// 其他都需要认证
.anyRequest().authenticated())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
@Order(3)
public SecurityFilterChain defaultChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
第三链捕获所有未被前两条链处理的请求,使用 HTTP Basic 认证。未放行的 actuator 端点(如 /actuator/env、/actuator/beans)必须凭用户名密码访问,这些敏感端点不会暴露给前端 JWT 用户。
JWT 过滤器继承 OncePerRequestFilter,保证每个请求只执行一次:
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String BEARER_PREFIX = "Bearer ";
private final JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = extractJwtFromRequest(request);
if (StringUtils.isNotBlank(jwt) && jwtTokenProvider.validateToken(jwt)) {
authenticateUser(jwt, request);
}
} catch (Exception ex) {
// 认证失败不中断过滤链,只是不设置 SecurityContext
log.error("无法设置用户认证: {}", ex.getMessage());
}
filterChain.doFilter(request, response);
}
private void authenticateUser(String jwt, HttpServletRequest request) {
String username = jwtTokenProvider.getUsernameFromToken(jwt);
Long userId = jwtTokenProvider.getUserIdFromToken(jwt);
List<String> roles = jwtTokenProvider.getRolesFromToken(jwt);
// 将用户 ID 存入 Authentication details,供 SecurityUtils 读取
JwtAuthenticationDetails details = new JwtAuthenticationDetails(request, userId);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
auth.setDetails(details);
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
一个细节值得注意:我将 userId 存入了 Authentication.details,而不只是 username。这样在业务层就可以直接获取数字 userId,无需再查库:
// SecurityUtils.java
public static Long getCurrentUserId() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getDetails() instanceof JwtAuthenticationDetails details) {
return details.getUserId();
}
return null;
}
使用了 Java 16 的 Pattern Matching for instanceof,代码更简洁。
除了 URL 级别的控制,我还开启了方法级安全注解:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 启用 @PreAuthorize 等注解
public class SecurityConfig { ... }
// 只有管理员角色才能访问管理接口
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/admin/articles/{id}")
public Result<Void> deleteArticle(@PathVariable Long articleId) { ... }
1. 关注点分离:每条链只负责自己的认证逻辑,不互相干扰。
2. 微服务就绪:拆分时,每个服务复制自己需要的那条链即可,无需重构。
3. 配置驱动:白名单从配置文件读,可以精确控制不同环境的端点访问策略。
4. 测试友好:每条链可以独立测试,测试类也可以只配置需要的链。
在设计个人博客后端时,我面临一个经典抉择:用简单粗暴的单体应用,还是拥抱时髦的微服务?最终我选择了两者之间的 模块化单体架构(Modular Monolith),事实证明这是正确的决定。 为什么不用微服务? 微服务固然优秀,但对于个人项目来说成本太高: - 需要服务注册、配置中心、网关、链路追踪…… - 每个服务独立部署,运维复杂度指数级增长 - 分布式事务难以处理 - 单人开发,维护多个仓库效率...
在做个人博客后端时,我发现每个业务模块都在重复同样的代码:创建实体、校验参数、调用 Mapper、处理异常……于是我设计了 BaseServiceImpl,一个基于 MyBatis-Plus 和 MapStruct 的通用 CRUD 基类。
作为一个人项目,我的博客系统包含 4 个独立仓库:后端(Spring Boot)、前端(Next.js 博客展示)、管理后台(Next.js Admin)、部署配置(Docker Compose)。最初我每次更新代码都要手动构建镜像、推送 Docker Hub、SSH 到 VPS 拉取最新版——这套流程繁琐且容易出错。
从数据库到用户界面,这篇文章记录我在个人博客系统中每一层技术选型背后的思考。整个系统包含 4 个独立仓库,涵盖 Java 后端、两个 Next.js 前端、和 Docker 部署配置。 --- 一、整体架构图 ` 用户浏览器 │ ▼ Caddy(反向代理 + 自动 HTTPS) │ ├── chonkybird.com → Next.js 博客前端(SSR) ├── admin.
这篇文章记录我在开发个人博客系统全程中使用 AI 工具的真实体验——包括它在哪些环节显著提升了效率,在哪些地方需要谨慎,以及如何正确地使用 AI。 整个系统包含 Spring Boot 后端、两个 Next.js 前端、Docker Compose 部署配置,共 4 个独立仓库,全程有 AI 深度介入。