乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

认证流程

定义与作用

Spring Security 的认证流程,是指从用户提交凭据(通常是用户名和密码)开始,到系统确认用户身份并将其安全信息存入上下文为止的完整链路。它是整个安全框架的入口管线,决定了"谁能进入系统"。

手写过滤器的痛点

在没有 Spring Security 之前,开发者通常手写 Servlet Filter 处理登录:

// 典型手写登录过滤器(痛点累累)
public class HandmadeLoginFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
        HttpServletRequest request = (HttpServletRequest) req;
        if ("/login".equals(request.getRequestURI())) {
            String username = request.getParameter("username");
            String password = request.getParameter("password");
            // 痛点1:密码明文比对,没有标准编码策略
            // 痛点2:用户不存在 vs 密码错误,容易泄露信息
            // 痛点3:登录成功后往哪存用户信息?HttpSession?ThreadLocal?各写各的
            // 痛点4:异常处理不统一,有的抛异常,有的直接 response.sendError(401)
            // 痛点5:权限信息怎么挂到用户身上?Role 和 Authority 混为一谈
            // 痛点6:多个登录方式(表单、Basic、Token)无法复用同一套验证逻辑
        }
    }
}

Spring Security 的认证流程通过标准化接口 + 职责分离 + 策略模式一次性解决了以上所有问题。


核心原理

Spring Security 认证流程由过滤器链驱动,核心参与者如下:

组件职责类比
UsernamePasswordAuthenticationFilter拦截登录请求,提取凭据,封装为 Token前台接待
AuthenticationManager接收 Token,启动认证大堂经理
ProviderManager遍历 Provider 列表,找到能处理此 Token 的人调度中心
AuthenticationProvider执行具体认证逻辑(如查库、比对密码)业务专员
UserDetailsService根据用户名加载用户信息档案管理员
PasswordEncoder密码哈希比对密码校验机
SecurityContextHolder存放认证成功后的安全上下文身份档案柜

认证时序全流程

关键状态转换

认证流程中最核心的状态转换是 Authentication 从未认证变为已认证:

// 步骤1:过滤器构造未认证 Token(principal=username, credentials=password)
UsernamePasswordAuthenticationToken unauthenticated =
    new UsernamePasswordAuthenticationToken(username, password);

// 步骤2:经过 Provider 认证后,返回已认证 Token(principal=UserDetails, authorities=角色列表)
UsernamePasswordAuthenticationToken authenticated =
    new UsernamePasswordAuthenticationToken(
        userDetails,           // principal:用户主体
        null,                  // credentials:凭据已擦除
        userDetails.getAuthorities()  // authorities:权限列表
    );

示例一:表单登录的完整认证链路

场景说明

一个传统 Web 应用,用户通过表单提交用户名密码,Spring Security 完成认证并将会话存入 HttpSession。

操作前配置

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorize -> authorize
                .antMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/home")
                .permitAll()
            );
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("admin").password("{noop}admin123").roles("ADMIN")
            .and()
            .withUser("user").password("{noop}user123").roles("USER");
    }
}

操作后配置

将内存用户替换为数据库用户,并引入密码编码:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorize -> authorize
                .requestMatchers(new AntPathRequestMatcher("/public/**")).permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/home")
                .permitAll()
            );
    }
}
@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity user = userMapper.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在: " + username);
        }
        return org.springframework.security.core.userdetails.User.builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .roles(user.getRole().split(","))
            .build();
    }
}

结果分析

  • 登录请求被 UsernamePasswordAuthenticationFilter 拦截
  • ProviderManager 将认证委托给 DaoAuthenticationProvider
  • CustomUserDetailsService 从数据库加载用户
  • BCryptPasswordEncoder 比对密码哈希
  • 认证成功后,SecurityContextHolder 存入 Authentication,SecurityContextPersistenceFilter 将其同步到 Session

示例二:REST API + Basic 认证的流程观察

场景说明

内部管理接口需要支持 HTTP Basic 认证,供第三方系统调用,观察其认证流程与表单登录的区别。

操作前配置

仅开放表单登录,无 Basic 认证:

http
    .authorizeRequests(authorize -> authorize
        .antMatchers("/api/internal/**").authenticated()
    )
    .formLogin(Customizer.withDefaults());

第三方系统调用 GET /api/internal/status 时,由于没有表单页面,会收到 302 重定向到登录页,API 调用失败。

操作后配置

增加 Basic 认证支持,让同一 URL 支持两种认证入口:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorize -> authorize
                .requestMatchers(new AntPathRequestMatcher("/api/internal/**")).authenticated()
                .anyRequest().permitAll()
            )
            .httpBasic(Customizer.withDefaults())   // 启用 BasicAuthenticationFilter
            .formLogin(Customizer.withDefaults());
    }
}

结果分析

  • BasicAuthenticationFilter 与 UsernamePasswordAuthenticationFilter 共用同一套 AuthenticationManager → ProviderManager → DaoAuthenticationProvider 链路
  • 认证入口不同,但认证核心流程完全一致,体现了 Spring Security 认证架构的复用性

易错场景:认证成功后 SecurityContext 丢失

问题描述

开发者在登录成功后的 Controller 中直接新建线程处理业务,发现 SecurityContextHolder.getContext().getAuthentication() 返回 null。

错误代码

@Controller
public class LoginController {

    @GetMapping("/home")
    public String home() {
        // 主线程能获取到认证信息
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        System.out.println("主线程: " + auth.getName());  // 正常输出

        // 错误:新线程中 SecurityContext 丢失
        new Thread(() -> {
            Authentication authInThread = SecurityContextHolder.getContext().getAuthentication();
            System.out.println("子线程: " + authInThread);  // null!
        }).start();

        return "home";
    }
}

原因分析

SecurityContextHolder 默认使用 MODE_THREADLOCAL 策略,安全上下文绑定到当前线程。新建线程不继承父线程的 ThreadLocal 变量。

正确做法

方式一:切换策略(应用启动时全局设置)

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SecurityContextHolder.setStrategyName(
            SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
        );
        SpringApplication.run(Application.class, args);
    }
}

方式二:手动传递 Authentication(更可控)

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
new Thread(() -> {
    SecurityContextHolder.getContext().setAuthentication(auth);
    // 执行业务...
}).start();

面试考点:SecurityContextHolder 的默认策略是什么?@Async 异步方法中为什么拿不到当前用户?如何正确传递认证信息?

上一页
Authentication
下一页
AuthenticationManager