Spring Boot 登录拦截的两种实现:手写拦截器 vs Spring Security
前言
在 Web 后端开发中,“登录拦截”与“身份识别”是系统最基础的安全防线。我们需要确保只有经过认证的用户才能访问受保护的资源,并且在后续的业务逻辑中能够随时获取当前操作用户的身份信息。
在 Spring Boot 生态中,实现这一功能主要有两大流派:
- 轻量级派:不引入额外框架,利用 Spring MVC 的 拦截器 (HandlerInterceptor) 配合 ThreadLocal 手动实现。
- 框架派:使用业界标准安全框架 Spring Security,利用其过滤器链和上下文管理机制。
本文将深入代码底层,详细拆解这两种方案的实现方式,并从架构维度分析两者的本质区别与选型策略。
章节一:轻量级方案——基于 HandlerInterceptor + ThreadLocal
这种方案的核心思想是:拦截器负责“把门”,ThreadLocal 负责“存人”。 它依赖于 Spring MVC 的机制,实现简单,逻辑直观,非常适合中小型项目或内部系统。
1. 核心架构设计
- 请求拦截:利用
HandlerInterceptor的preHandle方法拦截 HTTP 请求,解析 Header 中的 Token。 - 上下文传递:利用 JDK 原生的
ThreadLocal,将解析出的用户信息绑定到当前线程,供 Controller 或 Service 层随时调用。 - 资源清理:利用
afterCompletion方法清理 ThreadLocal,防止内存泄漏(尤其在线程池环境下)。
2. 代码实现
第一步:定义用户上下文工具类
我们需要一个容器来存放当前请求的用户信息。
public class UserContext {
// 核心存储容器
private static final ThreadLocal<LoginUser> USER_HOLDER = new ThreadLocal<>();
public static void set(LoginUser user) {
USER_HOLDER.set(user);
}
public static LoginUser get() {
return USER_HOLDER.get();
}
// 必须提供清理方法
public static void remove() {
USER_HOLDER.remove();
}
// 简单的用户实体
@Data
public static class LoginUser {
private Long id;
private String username;
private String role;
}
}
第二步:编写登录拦截器
这是鉴权逻辑的核心载体。
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取 Token
String token = request.getHeader("Authorization");
// 2. 校验 Token (此处省略具体 JWT 解析逻辑)
if (!isValid(token)) {
response.setStatus(401);
return false; // 拦截请求
}
// 3. 解析用户信息并存入 ThreadLocal
UserContext.LoginUser user = parseUserFromToken(token);
UserContext.set(user);
return true; // 放行
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 【关键步骤】请求结束,必须清理线程变量,防止数据污染和内存泄漏
UserContext.remove();
}
private boolean isValid(String token) {
return token != null && !token.isEmpty();
}
private UserContext.LoginUser parseUserFromToken(String token) {
// 模拟解析
UserContext.LoginUser user = new UserContext.LoginUser();
user.setId(1001L);
user.setUsername("admin");
return user;
}
}
第三步:注册拦截器
通过实现 WebMvcConfigurer 接口,将拦截器加入 Spring MVC 的执行链路。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") // 默认拦截所有
.excludePathPatterns("/login", "/register", "/static/**"); // 放行白名单
}
}
第四步:业务中使用
@GetMapping("/api/profile")
public Result getProfile() {
// 直接获取,无需参数传递
LoginUser currentUser = UserContext.get();
return Result.success(currentUser);
}
为什么要使用ThreadLocal而不是普通变量来存储用户信息
在Spring Boot等Web应用架构中,核心组件(如Controller和Service)默认是单例的,这意味着在多线程并发处理请求时,所有线程共享同一个组件实例。如果使用普通的成员变量存储用户信息,这些数据就会变成共享资源,导致后一个请求的数据覆盖前一个请求的数据,从而引发严重的线程安全问题(即不同用户的数据发生错乱)。
使用ThreadLocal是为了实现数据在线程层面的物理隔离。它在内存中为每一个正在运行的线程分配独立的存储空间,确保每个线程只能访问和修改属于自己的数据副本,从而彻底杜绝了并发环境下的数据干扰。此外,ThreadLocal允许数据在当前线程的整个执行链路中(如从Controller到Service再到Dao)隐式传递,避免了在所有方法签名中显式添加用户参数的冗余代码。
工业级方案——基于 Spring Security
Spring Security 是 Spring 家族提供的安全标准答案。与手写拦截器不同,它不仅仅做登录拦截,更提供了一套完整的安全防护体系(鉴权、防攻击、OAuth2 等)。它的核心基于 Servlet Filter Chain(过滤器链)。
随着 Spring Boot 版本的迭代(特别是从 2.7 到 3.0),Spring Security 的配置方式发生了重大变革。本章将展示如何编写通用的认证逻辑,并分别演示新旧两种配置写法。
1. 核心架构设计
- 过滤器链:请求会经过一系列 Filter,我们需要自定义一个 Filter 插入其中来解析 Token。
- 上下文存储:使用
SecurityContextHolder存储认证信息(底层默认封装了 ThreadLocal)。 - 配置方式:从“继承适配器类”演变为“注册 Bean 组件”。
2. 通用实现步骤
无论使用哪种配置写法,以下两个步骤(依赖引入和过滤器编写)是通用的。
第一步:引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
第二步:编写 JWT 认证过滤器
我们需要自定义一个过滤器,用于解析请求头中的 Token,并将其转换为 Spring Security 认可的 Authentication 对象。
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 1. 获取 Token
String token = request.getHeader("Authorization");
// 2. 判空与校验 (省略具体 JWT 工具类校验细节)
if (StringUtils.hasText(token) && JwtUtils.validate(token)) {
// 3. 解析用户信息
String username = JwtUtils.getUsername(token);
// 4. 构建 Authentication 对象
// 三个参数:用户信息(Principal)、密码(Credentials,已登录通常为null)、权限集合(Authorities)
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(username, null, AuthorityUtils.createAuthorityList("ROLE_ADMIN"));
// 5. 【关键步骤】将认证信息存入 Security 上下文
// 这一步相当于手动方案中的 UserContext.set(user)
SecurityContextHolder.getContext().setAuthentication(authentication);
}
// 6. 放行,继续执行过滤器链中的下一个 Filter
filterChain.doFilter(request, response);
}
}
3. 配置安全策略(核心分歧点)
这里是 Spring Security 最大的变化点。请根据你的 Spring Boot 版本选择一种方式。
方式 A:经典写法(适用于 Spring Boot 2.6 及以下)
核心机制:继承 WebSecurityConfigurerAdapter 并重写 configure 方法。这种方式在 Spring Security 5.7 后被标记为废弃,但在大量存量项目中依然广泛存在。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启注解鉴权
public class LegacySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 关闭 CSRF (前后端分离通常不需要)
.csrf().disable()
// 不使用 Session (因为是 JWT 无状态模式)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 放行登录接口
.antMatchers("/login", "/register").permitAll()
// 其他接口均需认证
.anyRequest().authenticated();
// 将自定义的 JWT 过滤器添加到 UsernamePasswordAuthenticationFilter 之前
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
}
方式 B:现代写法(适用于 Spring Boot 3.0+ / Security 6.0+)
核心机制:组合优于继承。不再继承任何类,而是通过注册一个 SecurityFilterChain 类型的 Bean 来定义过滤链。这是目前官方推荐的唯一标准写法。
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 开启注解鉴权 (Spring Security 6.0+ 的新注解,替代了 @EnableGlobalMethodSecurity)
public class ModernSecurityConfig {
@Autowired
private JwtAuthenticationTokenFilter jwtFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// Spring Security 6.0+ 推荐使用 Lambda DSL 写法,更加简洁
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/register").permitAll() // 注意这里方法名变成了 requestMatchers
.anyRequest().authenticated()
)
// 注册自定义过滤器
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
4. 业务中使用
配置完成后,我们就可以在 Controller 层安心地写业务代码了。
@RestController
@RequestMapping("/api")
public class TestController {
// 方式一:利用注解鉴权
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')") // 只有具备 ADMIN 权限才能访问
public Result adminData() {
return Result.success("Admin Data");
}
// 方式二:获取当前用户信息
@GetMapping("/me")
public Result me() {
// 从 SecurityContextHolder 获取当前用户
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return Result.success("当前登录用户: " + auth.getName());
}
}
提示:如果你是新建项目,强烈建议直接采用 方式 B。Spring Boot 3.0 已经彻底移除了 WebSecurityConfigurerAdapter,掌握 Bean 注册式的配置写法是未来的必备技能。
对比与选型指南
这两种方式虽然都能实现“不登录就拦截”的效果,但在底层机制和能力边界上有着本质的区别。
1. 技术维度的区别
| 维度 | 手动实现 (Interceptor) | Spring Security (Filter) |
|---|---|---|
| 生效层级 | Spring MVC 层。只能拦截进入 DispatcherServlet 的请求(即 Controller 请求)。 | Servlet 容器层。基于 Filter,早于 Spring MVC 执行,能拦截所有资源。 |
| 上下文存储 | 手动维护 ThreadLocal,需手动清理。 |
SecurityContextHolder 自动管理,默认封装了 ThreadLocal,且支持线程间继承模式。 |
| 鉴权能力 | 弱。通常需要在代码中写 if (user.isAdmin())。 |
强。支持 @PreAuthorize、RBAC 模型、动态权限加载。 |
| 防御能力 | 无。需要自己手动防 XSS、CSRF。 | 强。默认集成 CSRF、HSTS、X-Frame-Options 等安全响应头。 |
| 扩展性 | 低。对接 OAuth2、LDAP 需要大量硬编码。 | 高。生态完善,几行配置对接 GitHub/Google 登录。 |
2. 核心联系
- 底层同源:Spring Security 的
SecurityContextHolder在默认策略下,底层依然是使用ThreadLocal来存储用户信息的,这点与手动实现方案是一致的。 - 设计模式:两者都使用了拦截器模式/责任链模式。手动方案是 MVC 的拦截器链,Security 是 Servlet 的过滤器链。
3. 选型建议
选择手动实现方案,如果:
- 项目非常小,或者是一个单纯的内部微服务,逻辑极简。
- 团队成员对 Spring Security 不熟悉,且学习成本过高。
- 只需要简单的登录校验,不需要复杂的角色权限控制(RBAC)。
- 性能极其敏感,希望调用链路越短越好。
选择 Spring Security,如果:
- 这是一个正式的商业项目,尤其是面向公网的。
- 需要精细的权限控制(如:菜单级、按钮级、数据级权限)。
- 需要对接第三方登录(微信、钉钉、OAuth2)。
- 需要防御常见的 Web 攻击(CSRF, 会话劫持等)。
- 项目未来有扩展需求,需要遵循行业标准安全规范。
更多推荐

所有评论(0)