Spring Security认证(Authentication)授权(Authorization)是独立的,是分开的。一切授权操作都是在认证成功后的基础之上进行的,如果身份不合法,那也就没有授权的必要了。

Spring Security的整体架构 - 图1

一、 AuthenticationManager 接口

1. AuthenticationManager是什么?

AuthenticationManager 是一个接口,它定义了一个核心方法:

  1. Authentication authenticate(Authentication authentication) throws AuthenticationException;
  • 参数Authentication 对象,包含用户提供的凭证信息,例如用户名和密码。
  • 返回值:如果认证成功,返回一个经过认证的 Authentication 对象,表示用户身份已验证;如果失败,则抛出 AuthenticationException 异常。

简单来说,AuthenticationManager 的职责是对用户提交的凭证进行验证,确保用户身份合法。


2. AuthenticationManager的主要实现:ProviderManager

在Spring Security中,AuthenticationManager 最常见的实现类是 ProviderManagerProviderManager 通过协调一组 AuthenticationProvider 来完成认证工作。

  • AuthenticationProvider 接口
    AuthenticationProvider 是另一个关键接口,定义了两个主要方法:

    • Authentication authenticate(Authentication authentication):执行具体的认证逻辑。
    • boolean supports(Class<?> authentication):判断该 provider 是否支持当前传入的 Authentication 类型。
  • ProviderManager 的工作机制
    ProviderManager 内部维护了一个 AuthenticationProvider 列表。当它的 authenticate 方法被调用时:

    1. 遍历 AuthenticationProvider 列表。
    2. 找到第一个支持当前 Authentication 类型的 AuthenticationProvider(通过 supports 方法判断)。
    3. 调用该 AuthenticationProviderauthenticate 方法进行认证。
    4. 如果认证成功,返回一个已认证的 Authentication 对象;如果失败,抛出异常。

这种设计使得 AuthenticationManager 具有很高的灵活性,可以支持多种认证方式(例如用户名/密码认证、OAuth2、LDAP 等),只需配置不同的 AuthenticationProvider 即可。


3. 常用的AuthenticationProvider:DaoAuthenticationProvider

在实际应用中,一个常用的 AuthenticationProvider 实现是 DaoAuthenticationProvider。它主要用于基于数据库的用户名/密码认证,工作流程如下:

  1. 加载用户信息:通过 UserDetailsService 从数据库中获取用户信息(UserDetails 对象)。
  2. 密码验证:将用户提交的密码与数据库中的密码进行比对(通常结合密码加密器,如 BCryptPasswordEncoder)。
  3. 返回结果:如果验证通过,返回一个认证成功的 Authentication 对象;否则抛出异常。

开发者可以通过自定义 UserDetailsService 来指定如何从数据源加载用户信息,从而实现个性化的认证逻辑。


4. 其他AuthenticationManager实现

除了 ProviderManager,Spring Security 还提供了一些其他的 AuthenticationManager 实现,例如:

  • JaasAuthenticationManager:基于 Java Authentication and Authorization Service (JAAS) 进行认证,适用于需要集成 JAAS 的场景。
  • 自定义实现:开发者可以实现 AuthenticationManager 接口,创建完全定制化的认证逻辑。

5. AuthenticationManager在认证流程中的角色

AuthenticationManager 是 Spring Security 认证流程的核心协调者。它的典型使用场景如下:

  1. 用户提交登录请求(例如用户名和密码)。
  2. Spring Security 创建一个未认证的 Authentication 对象(通常是 UsernamePasswordAuthenticationToken)。
  3. AuthenticationManagerauthenticate 方法被调用。
  4. ProviderManager 委托合适的 AuthenticationProvider 处理认证。
  5. 认证结果被返回并存储到安全上下文中(SecurityContext),供后续授权使用。

6. 总结

  • AuthenticationManager 是 Spring Security 中负责用户认证的核心接口。
  • ProviderManager 是其主要实现,通过管理多个 AuthenticationProvider 来支持不同的认证方式。
  • DaoAuthenticationProvider 是一个常用的 provider,适合基于数据库的用户名/密码认证。
  • 通过灵活的设计,AuthenticationManager 能够轻松扩展,支持各种认证场景。

理解 AuthenticationManager 的工作原理,有助于开发者更好地配置和定制 Spring Security 的认证流程,从而满足不同的安全需求。

二、Authentication接口

在 Spring Security 中,Authentication 是一个核心接口,用于表示用户的认证信息。它不仅包含用户提交的凭证(例如用户名和密码),还包含认证成功后的用户权限等信息。下面我们将详细讲解 Authentication 接口及其在 Spring Security 认证过程中的作用。

1. Authentication 接口简介

Authentication 接口定义了用户认证信息的主要内容,通过以下方法提供访问:

  • getAuthorities():返回用户拥有的权限集合(例如角色或权限列表),类型为 Collection<? extends GrantedAuthority>
  • getCredentials():返回用户的凭证,通常是密码、证书等。
  • getDetails():返回用户的额外信息,例如 IP 地址或会话 ID。
  • getPrincipal():返回用户的主体信息,通常是用户名或 UserDetails 对象。
  • isAuthenticated():返回一个布尔值,表示用户是否已通过认证。
  • setAuthenticated(boolean):设置认证状态,通常由 Spring Security 框架内部调用。

这些方法使得 Authentication 成为一个灵活的载体,能够同时表示认证请求和认证结果。


2. Authentication 的两种状态

Authentication 对象在认证过程中有两种主要状态:

  1. 未认证状态

    • 当用户提交凭证(例如用户名和密码)时,Spring Security 会创建一个未认证的 Authentication 对象。
    • 此时,isAuthenticated() 返回 false,表示凭证尚未被验证。
    • 例如,一个 UsernamePasswordAuthenticationToken 对象会被初始化为包含用户名和密码,但认证状态为未完成。
  2. 已认证状态

    • 认证成功后,Spring Security 会生成一个新的 Authentication 对象。
    • 这个对象包含用户的权限(authorities)、主体(principal)等信息,且 isAuthenticated() 返回 true
    • 例如,认证后的 UsernamePasswordAuthenticationToken 会包含 UserDetails 对象和权限列表。

3. Authentication 的常见实现类

Spring Security 提供了多种 Authentication 接口的实现类,以支持不同的认证场景:

  • UsernamePasswordAuthenticationToken

    • 最常用的实现,适用于基于用户名和密码的认证。
    • 未认证时包含用户名和密码,认证成功后包含 UserDetails 和权限。
  • AnonymousAuthenticationToken

    • 表示匿名用户,通常用于未登录用户的访问。
  • RememberMeAuthenticationToken

    • 用于“记住我”功能,支持用户在关闭浏览器后仍保持登录状态。
  • JwtAuthenticationToken

    • 用于基于 JWT(JSON Web Token)的认证,常见于 OAuth2 场景。

4. Authentication 在认证流程中的作用

Authentication 在 Spring Security 的认证流程中起着关键作用,以下是其工作流程:

  1. 用户提交凭证

    • 用户通过登录表单输入用户名和密码。
    • Spring Security 创建一个未认证的 UsernamePasswordAuthenticationToken 对象,封装用户名和密码。
  2. 认证请求

    • AuthenticationManagerauthenticate 方法接收这个未认证的 Authentication 对象。
    • 具体的 AuthenticationProvider(例如 DaoAuthenticationProvider)负责验证凭证,比如通过数据库检查用户名和密码是否匹配。
  3. 认证成功

    • 如果凭证有效,AuthenticationProvider 返回一个已认证的 Authentication 对象。
    • 这个对象包含用户的权限、主体等信息,且认证状态为 true
  4. 存储到安全上下文

    • 认证成功后,Authentication 对象被存储到 SecurityContextHolder 中,供后续的授权和访问控制使用。

5. Authentication 使用示例

在大多数情况下,开发者无需直接操作 Authentication 对象,因为 Spring Security 的过滤器和配置会自动完成认证流程。但在某些自定义场景中,可能需要手动创建或访问 Authentication 对象。

示例 1:手动创建未认证的 Authentication 对象
  1. Authentication authentication = new UsernamePasswordAuthenticationToken("username", "password");

这个对象可以传递给 AuthenticationManager 进行认证。

示例 2:获取当前用户的认证信息
  1. Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  2. if (authentication != null && authentication.isAuthenticated()) {
  3. String username = authentication.getName(); // 获取用户名
  4. Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); // 获取权限
  5. // 处理用户信息
  6. }

6. 总结

  • Authentication 是 Spring Security 中表示用户认证信息的核心接口,包含凭证、权限、主体等内容。
  • 它在认证前后有未认证和已认证两种状态,分别对应认证请求和认证结果。
  • Spring Security 提供了多种实现类(如 UsernamePasswordAuthenticationToken),支持不同的认证方式。
  • 在认证流程中,Authentication 对象从用户提交凭证开始,经过验证后存储到安全上下文,用于后续的权限控制。

理解 Authentication 的作用和工作机制,可以帮助开发者更好地掌握 Spring Security 的认证流程,从而实现安全、高效的用户认证功能。

三、SecurityContextHolder

Spring Security 中的 SecurityContextHolder 详解

SecurityContextHolderSpring Security 中最核心的类之一,它用于存储和获取当前线程的安全上下文(SecurityContext),其中包含了当前用户的 身份认证信息(Authentication

1.SecurityContextHolder 作用

  1. 存储认证信息SecurityContextHolder 持有 SecurityContextSecurityContext 内部存储着 Authentication(当前用户身份)。
  2. 全局访问认证信息:无论在 Controller、Service 还是其他组件,都可以通过 SecurityContextHolder.getContext() 访问当前认证信息。
  3. 支持不同的存储策略:默认使用 ThreadLocal,即 线程级别存储,保证当前请求线程中的身份信息不会被其他线程访问。

2. SecurityContextHolder 的结构

核心组成

  1. public class SecurityContextHolder {
  2. private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
  3. public static SecurityContext getContext() {
  4. SecurityContext ctx = contextHolder.get();
  5. if (ctx == null) {
  6. ctx = createEmptyContext();
  7. contextHolder.set(ctx);
  8. }
  9. return ctx;
  10. }
  11. public static void setContext(SecurityContext context) {
  12. contextHolder.set(context);
  13. }
  14. public static void clearContext() {
  15. contextHolder.remove();
  16. }
  17. }
  • getContext():获取当前线程的安全上下文,默认使用 ThreadLocal 进行存储。
  • setContext(SecurityContext context):手动设置安全上下文。
  • clearContext():清除当前线程的安全上下文,避免线程复用时信息泄漏。

3. SecurityContext 与 Authentication

SecurityContext作用

SecurityContextSecurityContextHolder 内部存储的对象,用于持有 Authentication 信息

  1. public interface SecurityContext extends Serializable {
  2. Authentication getAuthentication();
  3. void setAuthentication(Authentication authentication);
  4. }
  • getAuthentication():获取当前认证的 Authentication
  • setAuthentication(Authentication authentication):手动设置认证信息。

示例:获取当前用户身份

  1. Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  2. String username = authentication.getName();
  3. Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

4. SecurityContextHolder 认证流程

默认认证流程

  1. 用户提交 用户名+密码 进行登录。
  2. AuthenticationManager.authenticate() 认证用户。
  3. 认证成功后,Spring Security Authentication 存入 SecurityContextHolder
    1. SecurityContextHolder.getContext().setAuthentication(authenticatedUser);
  4. 后续请求时,可以通过 SecurityContextHolder 获取当前用户信息。

5. SecurityContextHolder 的存储策略

Spring Security 默认使用 ThreadLocal 存储 SecurityContext,但也支持其他存储策略:

策略 描述
MODE_THREADLOCAL(默认) 使用 ThreadLocal,每个线程独立存储安全上下文
MODE_INHERITABLETHREADLOCAL 允许子线程继承 SecurityContext
MODE_GLOBAL 适用于无状态应用,所有线程共享同一个 SecurityContext

更改存储策略

在 Spring Security 启动前 设置:

  1. SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);

6. SecurityContextHolder 的常见使用场景

6.1 在 Controller 获取当前用户

  1. @RestController
  2. @RequestMapping("/user")
  3. public class UserController {
  4. @GetMapping("/me")
  5. public ResponseEntity<String> getCurrentUser() {
  6. Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  7. return ResponseEntity.ok("Current User: " + authentication.getName());
  8. }
  9. }

6.2 在 Service 层获取当前用户

  1. @Service
  2. public class UserService {
  3. public String getCurrentUsername() {
  4. Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  5. if (authentication != null && authentication.isAuthenticated()) {
  6. return authentication.getName();
  7. }
  8. return "Anonymous";
  9. }
  10. }

6.3 在 Spring Security 过滤器中手动设置 SecurityContext

如果使用 JWT 认证,我们通常需要手动设置 SecurityContextHolder

  1. public class JwtAuthenticationFilter extends OncePerRequestFilter {
  2. private final JwtService jwtService;
  3. private final UserDetailsService userDetailsService;
  4. public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) {
  5. this.jwtService = jwtService;
  6. this.userDetailsService = userDetailsService;
  7. }
  8. @Override
  9. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
  10. throws ServletException, IOException {
  11. String token = extractToken(request);
  12. if (token != null && jwtService.validateToken(token)) {
  13. String username = jwtService.getUsernameFromToken(token);
  14. UserDetails userDetails = userDetailsService.loadUserByUsername(username);
  15. Authentication authentication = new UsernamePasswordAuthenticationToken(
  16. userDetails, null, userDetails.getAuthorities());
  17. SecurityContextHolder.getContext().setAuthentication(authentication);
  18. }
  19. chain.doFilter(request, response);
  20. }
  21. }
  • 从 HTTP 头解析 JWT 令牌
  • 验证令牌 并解析用户信息。
  • 构造 Authentication 并存入 SecurityContextHolder

7. 线程安全问题

7.1 ThreadLocal 问题

SecurityContextHolder 默认使用 ThreadLocal,但在多线程环境(如异步任务)中,认证信息可能无法正确传递:

  1. @Async
  2. public void someAsyncTask() {
  3. Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  4. System.out.println(authentication.getName()); // 可能为 null
  5. }

解决方案 使用 MODE_INHERITABLETHREADLOCAL

  1. SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);

或者手动传递 SecurityContext

  1. @Async
  2. public void someAsyncTask(SecurityContext securityContext) {
  3. SecurityContextHolder.setContext(securityContext);
  4. Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  5. System.out.println(authentication.getName());
  6. }

8. 总结

  • SecurityContextHolder 是 Spring Security 认证信息的全局存储容器,默认使用 ThreadLocal 进行存储。
  • SecurityContext 内部存储 Authentication,用于保存当前用户身份信息
  • 可以在 ControllerServiceFilter 中访问当前用户
    1. SecurityContextHolder.getContext().getAuthentication();
  • 在 JWT 或 SSO 认证场景,需要手动设置 SecurityContextHolder
  • 多线程环境下需小心 ThreadLocal,可以使用 MODE_INHERITABLETHREADLOCAL 或手动传递 SecurityContext

SecurityContextHolder 在整个 Spring Security 体系中扮演着 身份认证信息存储 的关键角色,确保应用能够安全、高效地进行身份认证管理。

Spring Security的整体架构 - 图2

Spring Security的整体架构 - 图3

  1. @Configuration
  2. @EnableWebSecurity
  3. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  4. @Override
  5. protected void configure(HttpSecurity http) throws Exception {
  6. http.authorizeRequests()
  7. .antMatchers("/admin/**").hasRole("ADMIN") // 需要 ADMIN 角色
  8. .antMatchers("/user/**").hasAnyRole("USER", "ADMIN") // 需要 USER 或 ADMIN 角色
  9. .anyRequest().authenticated(); // 其他请求需要认证
  10. }
  11. }

在上述配置中,hasRole(“ADMIN”) 和 hasAnyRole(“USER”, “ADMIN”) 会被转换为 ConfigAttribute 对象。