前言

在 Web 后端开发中,“登录拦截”与“身份识别”是系统最基础的安全防线。我们需要确保只有经过认证的用户才能访问受保护的资源,并且在后续的业务逻辑中能够随时获取当前操作用户的身份信息。

在 Spring Boot 生态中,实现这一功能主要有两大流派:

  1. 轻量级派:不引入额外框架,利用 Spring MVC 的 拦截器 (HandlerInterceptor) 配合 ThreadLocal 手动实现。
  2. 框架派:使用业界标准安全框架 Spring Security,利用其过滤器链和上下文管理机制。

本文将深入代码底层,详细拆解这两种方案的实现方式,并从架构维度分析两者的本质区别与选型策略。


章节一:轻量级方案——基于 HandlerInterceptor + ThreadLocal

这种方案的核心思想是:拦截器负责“把门”,ThreadLocal 负责“存人”。 它依赖于 Spring MVC 的机制,实现简单,逻辑直观,非常适合中小型项目或内部系统。

1. 核心架构设计

  • 请求拦截:利用 HandlerInterceptorpreHandle 方法拦截 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, 会话劫持等)。
  • 项目未来有扩展需求,需要遵循行业标准安全规范。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐