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

    • 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
    • 最佳实践

Authentication

Authentication 是 Spring Security 认证体系的核心数据载体,它统一表示"从用户提交凭据到认证成功后的主体信息"这一完整过程。在手写登录逻辑时,开发者往往自行定义 UserSession 类,各项目写法参差不齐——有的存用户名、有的存密码、有的存权限、有的存登录时间,导致混乱和安全隐患。Spring Security 用 Authentication 接口统一封装了认证全过程,无论是认证前的未验证 Token,还是认证后的完整主体信息,都用同一个抽象表示。


定义与作用

Authentication 接口继承自 java.security.Principal,定义了以下核心方法:

方法说明认证前认证后
getPrincipal()用户主体通常是用户名(String)通常是 UserDetails 对象
getCredentials()凭据密码(String)通常被擦除(null)
getAuthorities()权限列表空集合(Collections.emptyList())填充用户的角色/权限
getDetails()额外详情可能包含 IP 地址、Session ID 等保持原样或补充
isAuthenticated()认证状态falsetrue
setAuthenticated(boolean)设置认证状态框架内部调用外部调用可能抛异常

为什么是统一接口?

在手写登录系统中,开发者通常这样做:

// 手写做法:混乱且不统一
HttpSession session = request.getSession();
session.setAttribute("user", username);           // 项目A这么写
session.setAttribute("loginUser", userObj);       // 项目B这么写
session.setAttribute("currentUser", userId);        // 项目C这么写
session.setAttribute("roles", roleList);            // 有的单独存权限

Spring Security 统一用 Authentication 表示:

  • 认证前(提交凭据):UsernamePasswordAuthenticationToken 是 Authentication 的实现,此时 principal 是用户名,credentials 是密码,isAuthenticated = false。
  • 认证后(验证通过):DaoAuthenticationProvider 新建另一个 UsernamePasswordAuthenticationToken,此时 principal 是 UserDetails 对象,credentials 被擦除,authorities 已填充,isAuthenticated = true。

这种统一带来了类型安全、代码一致、安全可审计三大好处。

核心方法详解

getPrincipal():认证前后类型变化

认证前(未验证的 Token):

UsernamePasswordAuthenticationToken token = 
    new UsernamePasswordAuthenticationToken("zhangsan", "123456");
// token.getPrincipal() → "zhangsan"(String 类型)
// token.getCredentials() → "123456"(String 类型)
// token.isAuthenticated() → false

认证后(验证通过):

// DaoAuthenticationProvider 验证通过后新建的 Token
UsernamePasswordAuthenticationToken authenticatedToken = 
    new UsernamePasswordAuthenticationToken(
        userDetails,                    // principal → UserDetails 对象
        null,                           // credentials → 已擦除
        userDetails.getAuthorities()    // authorities → 权限列表
    );
// authenticatedToken.getPrincipal() → UserDetails 对象
// authenticatedToken.isAuthenticated() → true

getCredentials():认证后擦除的安全设计

认证成功后,DaoAuthenticationProvider 默认会调用 eraseCredentials() 擦除密码:

// 认证成功后,credentials 被擦除
System.out.println(auth.getCredentials());  // 输出 null(不是 Bug,是安全设计)

安全原因:Authentication 对象会被存入 SecurityContextHolder,并可能在请求结束后被持久化到 Session。如果密码留在 credentials 中,任何能访问 Session 的代码(包括日志、调试、序列化)都可能意外泄露明文密码。

getAuthorities():权限列表的填充时机

权限列表只有在认证成功后才有值:

// 认证前
Collection<? extends GrantedAuthority> authorities = token.getAuthorities();
// authorities → 空集合(不可修改的空列表)

// 认证后
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
// authorities → [ROLE_STUDENT, ROLE_USER](从 UserDetails 复制)

getDetails():额外上下文信息

getDetails() 通常用于存储认证过程中的额外信息,例如:

  • WebAuthenticationDetails:包含请求 IP 地址、Session ID 等
  • 自定义 AuthenticationDetailsSource 生成的对象
// 默认的 WebAuthenticationDetails 包含 IP 和 SessionId
Object details = auth.getDetails();
if (details instanceof WebAuthenticationDetails) {
    WebAuthenticationDetails webDetails = (WebAuthenticationDetails) details;
    System.out.println("IP: " + webDetails.getRemoteAddress());
    System.out.println("SessionId: " + webDetails.getSessionId());
}

setAuthenticated(boolean):谨慎使用

Spring Security 对外部调用 setAuthenticated(true) 持严格警惕态度:

// ❌ 错误:自定义 Token 时直接设为 true,可能触发安全警告
token.setAuthenticated(true);
// 异常:Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead

正确做法:使用带 authorities 参数的构造函数:

// ✅ 正确:通过构造函数标记已认证状态
new UsernamePasswordAuthenticationToken(principal, credentials, authorities);

核心原理:认证状态变化完整链路

Spring Security 的认证状态变化遵循严格的链路:

步骤 1:用户提交凭据

用户提交表单(POST /login),请求参数 username=zhangsan&password=123456。

步骤 2:过滤器构造未认证 Token

UsernamePasswordAuthenticationFilter 从请求中提取参数,构造未认证的 UsernamePasswordAuthenticationToken:

// 源码示意(UsernamePasswordAuthenticationFilter)
UsernamePasswordAuthenticationToken authRequest =
    new UsernamePasswordAuthenticationToken(
        username,     // principal
        password      // credentials
    );
// authRequest.isAuthenticated() → false(默认)

步骤 3:AuthenticationManager 调度认证

AuthenticationManager(通常是 ProviderManager)遍历 AuthenticationProvider 列表,找到支持 UsernamePasswordAuthenticationToken 的 DaoAuthenticationProvider:

// ProviderManager 核心逻辑(简化)
for (AuthenticationProvider provider : getProviders()) {
    if (provider.supports(authRequest.getClass())) {
        Authentication result = provider.authenticate(authRequest);
        if (result != null) {
            return result;  // 认证成功,返回新的 Authentication
        }
    }
}

步骤 4:DaoAuthenticationProvider 执行验证

DaoAuthenticationProvider 完成实际验证:

  1. 调用 UserDetailsService.loadUserByUsername(username) 加载 UserDetails
  2. 预检查:账户是否锁定、过期、禁用
  3. 调用 PasswordEncoder.matches(rawPassword, encodedPassword) 比对密码
  4. 通过后检查:凭据是否过期
  5. 新建已认证的 UsernamePasswordAuthenticationToken(含 authorities)
// DaoAuthenticationProvider 验证成功后的构造(简化)
UsernamePasswordAuthenticationToken result =
    new UsernamePasswordAuthenticationToken(
        userDetails,                    // principal → UserDetails
        null,                           // credentials → 擦除
        userDetails.getAuthorities()    // authorities → 权限列表
    );
result.setDetails(authRequest.getDetails());  // 保留原 details
// result.isAuthenticated() → true

步骤 5:存入 SecurityContextHolder

认证成功后,结果存入 SecurityContextHolder:

SecurityContextHolder.getContext().setAuthentication(result);

步骤 6:请求结束后持久化到 Session

SecurityContextPersistenceFilter(或 SecurityContextHolderFilter)在请求结束时,将 SecurityContext 中的 Authentication 持久化到 HttpSession:

// 请求结束后,SecurityContext 被存入 Session
HttpSession session = request.getSession();
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);

下次请求时,过滤器从 Session 中加载 SecurityContext,恢复 Authentication。


Authentication 状态变化时序图

以下时序图展示认证前与认证后各字段的取值变化:

状态对比表

字段认证前(Token)认证后(已认证)说明
principal"zhangsan"(String)UserDetails 对象类型从字符串变为完整用户对象
credentials"123456"(String)null安全擦除,防止泄露
authorities[](空集合)[ROLE_STUDENT, ...]从 UserDetails 复制
detailsWebAuthenticationDetailsWebAuthenticationDetails保留原上下文信息
isAuthenticatedfalsetrue标记认证状态

完整示例

示例一:从 Controller 中获取当前 Authentication 并判断状态

场景:学生管理系统,用户访问个人中心时判断登录状态,并打印认证信息。

操作前:手写 Session 检查,各项目写法不一致:

// ❌ 手写做法:混乱、不安全、不可移植
@GetMapping("/profile")
public String profile(HttpSession session) {
    // 项目A:session.getAttribute("user")
    // 项目B:session.getAttribute("loginUser")
    // 项目C:session.getAttribute("currentUser")
    Object user = session.getAttribute("user");
    if (user == null) {
        return "redirect:/login";
    }
    // 权限检查?每个项目自己实现,逻辑不同
    return "profile";
}

操作后:统一通过 SecurityContextHolder 获取:

@RestController
@RequestMapping("/student")
public class StudentProfileController {

    @GetMapping("/profile")
    public Map<String, Object> getProfile() {
        // 统一获取当前 Authentication(无论认证方式)
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();

        Map<String, Object> result = new HashMap<>();

        // 1. 判断认证状态
        result.put("isAuthenticated", auth.isAuthenticated());
        result.put("authClass", auth.getClass().getSimpleName());

        // 2. 获取 principal 类型
        Object principal = auth.getPrincipal();
        result.put("principalType", principal.getClass().getSimpleName());

        if (principal instanceof UserDetails) {
            UserDetails userDetails = (UserDetails) principal;
            result.put("username", userDetails.getUsername());
            result.put("accountNonExpired", userDetails.isAccountNonExpired());
            result.put("accountNonLocked", userDetails.isAccountNonLocked());
        } else {
            // 匿名用户或 JWT 场景可能返回 String
            result.put("username", principal.toString());
        }

        // 3. 获取权限列表
        Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
        List<String> authorityList = authorities.stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.toList());
        result.put("authorities", authorityList);

        // 4. credentials(认证后已被擦除)
        result.put("credentials", auth.getCredentials());  // 通常为 null

        // 5. details(额外上下文)
        Object details = auth.getDetails();
        if (details instanceof WebAuthenticationDetails) {
            WebAuthenticationDetails webDetails = (WebAuthenticationDetails) details;
            result.put("remoteAddress", webDetails.getRemoteAddress());
            result.put("sessionId", webDetails.getSessionId());
        }

        return result;
    }
}

输出结果示例(已认证用户):

{
    "isAuthenticated": true,
    "authClass": "UsernamePasswordAuthenticationToken",
    "principalType": "User",
    "username": "zhangsan",
    "accountNonExpired": true,
    "accountNonLocked": true,
    "authorities": ["ROLE_STUDENT", "ROLE_USER"],
    "credentials": null,
    "remoteAddress": "192.168.1.100",
    "sessionId": "A7B3C9D2E1F4G5H6"
}

输出结果示例(匿名用户):

{
    "isAuthenticated": true,
    "authClass": "AnonymousAuthenticationToken",
    "principalType": "String",
    "username": "anonymousUser",
    "authorities": ["ROLE_ANONYMOUS"],
    "credentials": null
}

关键结论:SecurityContextHolder.getContext().getAuthentication() 是统一入口,无论用户通过表单登录、HTTP Basic、JWT 还是 OAuth2 登录,返回的都是 Authentication 对象,代码完全通用。


示例二:手动构造 Authentication 并设置到 SecurityContext(模拟登录/测试场景)

场景:后台定时任务需要以管理员身份执行敏感操作(如数据清理、统计报表生成),但任务本身没有用户请求上下文,需要手动构造 Authentication。

操作前:无安全上下文,方法调用被 @PreAuthorize 拦截:

@Service
public class DataCleanupService {

    @PreAuthorize("hasRole('ADMIN')")
    public void cleanupOldRecords() {
        // 清理 90 天前的记录
    }
}

// 定时任务中直接调用
@Component
public class CleanupJob {
    @Autowired
    private DataCleanupService cleanupService;

    @Scheduled(cron = "0 0 2 * * ?")
    public void run() {
        cleanupService.cleanupOldRecords();
        // ❌ 抛出 AccessDeniedException:没有 Authentication 上下文
    }
}

操作后:手动构造 UsernamePasswordAuthenticationToken 并设置到 SecurityContextHolder:

@Component
public class CleanupJob {

    @Autowired
    private DataCleanupService cleanupService;

    @Scheduled(cron = "0 0 2 * * ?")
    public void run() {
        // 1. 构造管理员权限列表
        List<GrantedAuthority> authorities = Arrays.asList(
            new SimpleGrantedAuthority("ROLE_ADMIN"),
            new SimpleGrantedAuthority("ROLE_SYSTEM")
        );

        // 2. 构造已认证的 Authentication Token
        // 注意:使用带 authorities 的构造函数,自动标记 isAuthenticated = true
        UsernamePasswordAuthenticationToken authToken =
            new UsernamePasswordAuthenticationToken(
                "system_admin",     // principal(可以是用户名或 UserDetails)
                null,                // credentials(无需密码,设为 null)
                authorities          // 权限列表
            );

        // 3. 可选:填充额外详情
        authToken.setDetails("ScheduledTask: DataCleanupJob");

        // 4. 存入 SecurityContextHolder
        SecurityContextHolder.getContext().setAuthentication(authToken);

        try {
            // 5. 执行受保护的方法
            cleanupService.cleanupOldRecords();
            System.out.println("数据清理完成");
        } finally {
            // 6. ✅ 必须清理上下文!防止泄漏到后续线程或任务
            SecurityContextHolder.clearContext();
            System.out.println("SecurityContext 已清理");
        }
    }
}

结果分析:

  • 方法调用成功:@PreAuthorize("hasRole('ADMIN')") 检查通过,因为 Authentication 包含 ROLE_ADMIN。
  • try-finally 确保清理:SecurityContextHolder.clearContext() 是强制步骤,如果忘记清理,当前线程的 SecurityContext 会残留管理员权限,导致后续任务(或线程池复用时的其他请求)意外拥有管理员权限,造成严重安全漏洞。
  • 无密码风险:手动构造时 credentials 设为 null,不会泄露密码。

易错场景与面试考点

易错场景一:认证后 getCredentials() 返回 null,以为是 Bug

现象:

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
System.out.println(auth.getCredentials());  // 输出 null
// 开发者疑惑:"密码怎么不见了?是不是认证失败了?"

原因:DaoAuthenticationProvider 的 eraseCredentials() 机制。认证成功后,Spring Security 会主动擦除 credentials 中的密码:

// 在 ProviderManager 中,认证成功后调用
Authentication result = provider.authenticate(authentication);
if (result != null) {
    // 擦除凭据(如果配置了)
    if (eraseCredentialsAfterAuthentication) {
        eraseCredentials(result);  // 将 credentials 设为 null
    }
    return result;
}

正确理解:credentials 为 null 是预期行为,不是 Bug。如果确实需要访问密码(不推荐),需要在 AuthenticationProvider 返回前自行获取,但绝不应将密码留在 Authentication 对象中存入 Session。


易错场景二:自定义 Token 时误用 setAuthenticated(true)

现象:

// ❌ 错误:自定义过滤器中直接 setAuthenticated(true)
UsernamePasswordAuthenticationToken token = 
    new UsernamePasswordAuthenticationToken(userDetails, password);
token.setAuthenticated(true);  // 抛出异常!

异常信息:

java.lang.IllegalArgumentException: Cannot set this token to trusted - 
use constructor which takes a GrantedAuthority list instead

原因:UsernamePasswordAuthenticationToken 的 setAuthenticated() 方法做了安全校验,防止开发者在未经过 AuthenticationProvider 验证的情况下,将 Token 标记为已认证。

正确做法:

// ✅ 正确:使用带 authorities 的构造函数
UsernamePasswordAuthenticationToken token = 
    new UsernamePasswordAuthenticationToken(
        userDetails, 
        null, 
        userDetails.getAuthorities()
    );
// 此时 isAuthenticated 自动为 true,无需手动设置

面试考点

Q1:Authentication 和 UserDetails 的区别是什么?

维度AuthenticationUserDetails
定位认证数据载体(Token)用户核心信息封装
生命周期认证前 → 认证后,贯穿请求认证时从数据源加载,认证后作为 principal
包含密码认证前含,认证后擦除始终包含(用于密码比对)
来源由过滤器构造或 Provider 返回由 UserDetailsService.loadUserByUsername() 加载
存储位置SecurityContextHolder → SecurityContext作为 Authentication.principal 的一部分
关系Authentication.getPrincipal() 返回 UserDetails被 Authentication 引用

Q2:principal 在认证前后分别是什么类型?

  • 认证前:通常是 String(用户名),由 UsernamePasswordAuthenticationFilter 从请求参数提取。
  • 认证后:通常是 UserDetails 对象(如 org.springframework.security.core.userdetails.User),由 UserDetailsService 加载并经过 DaoAuthenticationProvider 验证后封装进新的 Authentication。

特殊场景:

  • JWT 认证:principal 可能是 Jwt 对象(含 Claims)
  • OAuth2 登录:principal 可能是 OAuth2User 对象
  • 匿名用户:principal 是字符串 "anonymousUser"

Q3:为什么认证后要擦除 credentials?

  1. 安全存储:Authentication 会被存入 SecurityContextHolder,并可能在请求结束后持久化到 HttpSession。如果密码留在 credentials 中,Session 中的密码可能会被日志记录、序列化、调试工具暴露。
  2. 最小权限原则:认证成功后,系统不再需要原始密码。后续授权决策只依赖 authorities,不需要 credentials。
  3. 审计合规:安全审计要求密码在任何持久化存储中都不以明文形式出现。

Q4:如何安全地手动构造 Authentication?

  1. 使用带 authorities 参数的构造函数(自动标记 isAuthenticated = true)
  2. credentials 设为 null(不需要密码时)
  3. 操作完成后必须调用 SecurityContextHolder.clearContext()
  4. 如果场景允许,优先使用 @WithMockUser 等测试注解,而非手动构造
// 安全的手动构造模板
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
    principal, null, authorities
);
SecurityContextHolder.getContext().setAuthentication(auth);
try {
    // 执行受保护操作
} finally {
    SecurityContextHolder.clearContext();
}

关键类速查

类说明
Authentication认证接口,核心数据载体
UsernamePasswordAuthenticationToken用户名密码认证 Token 实现
AnonymousAuthenticationToken匿名用户认证 Token
RememberMeAuthenticationToken记住我认证 Token
PreAuthenticatedAuthenticationToken前置认证 Token(如 SSO)
SecurityContextHolder持有当前线程的 SecurityContext
SecurityContext封装 Authentication 的上下文
DaoAuthenticationProvider基于 UserDetailsService 的认证提供者
ProviderManager遍历 Provider 列表的认证管理器
AuthenticationDetailsSource生成 details 对象的来源
WebAuthenticationDetails默认的 details 实现,含 IP 和 SessionId

版本说明:本文基于 Spring Security 5.x(对应 Spring Boot 2.x)。Spring Security 5.x 中 WebSecurityConfigurerAdapter 仍可用,但核心 Authentication 接口与 6.x 保持一致。不涉及 Spring Core、Spring MVC、Spring Boot 的具体配置细节。

学习建议:理解 Authentication 是认证体系的数据核心,与 SecurityContextHolder(存储核心)、UserDetails(用户信息核心)构成铁三角。认证前关注 principal 和 credentials 的类型,认证后关注 authorities 和 isAuthenticated 的变化,是掌握 Spring Security 的第一步。

上一页
本章定位
下一页
认证流程