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

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

UsernamePasswordAuthenticationToken

定义与作用

UsernamePasswordAuthenticationToken 是 Spring Security 中最常用的 Authentication 实现,封装基于用户名和密码的认证凭据。它在认证流程中扮演双重角色:

  • 未认证状态:由过滤器构造,携带用户提交的原始凭据,作为认证请求的"入场券"
  • 已认证状态:由 AuthenticationProvider 返回,携带用户主体、权限列表和已认证标记,作为认证成功的"身份证明"
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    private final Object principal;      // 未认证时=用户名,已认证时=UserDetails
    private Object credentials;            // 未认证时=密码,已认证时=null(已擦除)

    // 构造未认证 Token(用于提交认证请求)
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);  // 显式标记未认证
    }

    // 构造已认证 Token(用于返回认证结果)
    public UsernamePasswordAuthenticationToken(
            Object principal, Object credentials,
            Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);  // 标记已认证
    }
}

手写过滤器的痛点

手写登录时,通常自己定义登录请求对象,但各层之间传递的信息不统一:

// 痛点:登录请求对象、Session 中的用户对象、权限校验对象各写各的
public class LoginRequest {
    private String username;
    private String password;
}  // 仅用于接收请求

public class SessionUser {
    private String userId;
    private String nickname;
    private List<String> permissions;
}  // 用于存入 Session

// 两者没有统一接口,过滤器、Controller、Service 层各自转换
// 无法接入 Spring Security 的授权体系(如 @PreAuthorize)

UsernamePasswordAuthenticationToken 作为 Authentication 的标准实现,天然被 Spring Security 的所有组件识别,从过滤器到 ProviderManager 到 SecurityContextHolder 全程通用。


核心原理

两种状态转换

状态字段语义变化

字段未认证时已认证时说明
principalString(用户名)UserDetails(用户主体)认证成功后升级为完整用户对象
credentialsString(原始密码)null认证成功后主动擦除,防止密码泄露
authoritiesnullCollection<GrantedAuthority>从 UserDetails.getAuthorities() 复制
authenticatedfalsetrue通过 super.setAuthenticated(true) 设置,且会校验不可从外部设为 false 后 true

安全设计:防止外部伪造已认证 Token

@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
    if (isAuthenticated) {
        throw new IllegalArgumentException(
            "Cannot set this token to trusted - use constructor instead");
    }
    super.setAuthenticated(false);
}

开发者不能通过 setAuthenticated(true) 将未认证 Token 改为已认证,必须通过带 authorities 参数的构造器创建。这防止了恶意代码伪造认证状态。


示例一:自定义过滤器构造未认证 Token

场景说明

实现一个自定义的短信验证码登录过滤器,需要构造 UsernamePasswordAuthenticationToken 并提交给 AuthenticationManager。

操作前配置

没有自定义过滤器,只有标准表单登录:

http.formLogin(Customizer.withDefaults());  // 只支持用户名密码登录

操作后配置

自定义短信登录过滤器,手动构造并提交 Token:

public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public SmsAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login/sms", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                  HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {

        String mobile = request.getParameter("mobile");
        String code = request.getParameter("code");

        if (mobile == null) mobile = "";
        if (code == null) code = "";

        mobile = mobile.trim();

        // 构造未认证的 UsernamePasswordAuthenticationToken
        // 这里用 mobile 作为 principal,code 作为 credentials
        UsernamePasswordAuthenticationToken authRequest =
            new UsernamePasswordAuthenticationToken(mobile, code);

        // 设置请求详情(IP、SessionId),用于后续安全分析
        authRequest.setDetails(
            this.authenticationDetailsSource.buildDetails(request)
        );

        // 提交给 AuthenticationManager
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

配置自定义 Provider 处理此 Token:

@Component
public class SmsAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private SmsCodeService smsCodeService;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String mobile = authentication.getPrincipal().toString();
        String code = authentication.getCredentials().toString();

        if (!smsCodeService.verify(mobile, code)) {
            throw new BadCredentialsException("短信验证码错误");
        }

        UserDetails user = userDetailsService.loadUserByUsername(mobile);

        // 构造已认证的 Token:principal=UserDetails, credentials=null, authorities=权限列表
        return new UsernamePasswordAuthenticationToken(
            user, null, user.getAuthorities()
        );
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

注册过滤器:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SmsAuthenticationProvider smsAuthenticationProvider;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(smsAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorize -> authorize
                .antMatchers("/login/sms").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(new SmsAuthenticationFilter(),
                             UsernamePasswordAuthenticationFilter.class)
            .formLogin(Customizer.withDefaults());
    }
}

结果分析

  • 自定义过滤器通过 new UsernamePasswordAuthenticationToken(mobile, code) 构造未认证 Token
  • SmsAuthenticationProvider 验证通过后,通过 new UsernamePasswordAuthenticationToken(user, null, authorities) 构造已认证 Token
  • 两个构造器的区别仅在于第三个参数 authorities 的有无,这是框架区分认证状态的关键

示例二:已认证 Token 在 SecurityContext 中的流转

场景说明

观察认证成功后,UsernamePasswordAuthenticationToken 如何从 ProviderManager 流入 SecurityContextHolder,再到 Controller 层被使用。

操作前配置

认证成功后直接写 Session,没有使用 SecurityContext:

// 自定义过滤器内部
if (passwordCorrect) {
    request.getSession().setAttribute("user", user);  // 自己写 Session
    // Spring Security 的 @PreAuthorize 等方法级安全注解无法识别
}

操作后配置

使用标准 SecurityContextHolder 机制:

public class StandardLoginFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                              HttpServletResponse response,
                                              FilterChain chain,
                                              Authentication authResult) {
        // authResult 是 ProviderManager 返回的已认证 Token
        // principal=UserDetails, credentials=null, authorities=[ROLE_USER]

        // 1. 存入 SecurityContext
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authResult);
        SecurityContextHolder.setContext(context);

        // 2. 触发 SecurityContextPersistenceFilter 将其存入 Session
        // 无需手动操作,框架自动完成

        // 3. 调用成功处理器
        this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult);
    }
}

Controller 中获取已认证 Token 的信息:

@RestController
@RequestMapping("/api/user")
public class UserApiController {

    @GetMapping("/info")
    public ResponseEntity<?> getInfo(@AuthenticationPrincipal UserDetails userDetails) {
        // @AuthenticationPrincipal 自动从 SecurityContext 取出 principal
        String username = userDetails.getUsername();
        return ResponseEntity.ok(Map.of("username", username));
    }

    @GetMapping("/authorities")
    public ResponseEntity<?> getAuthorities() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        // auth 就是 UsernamePasswordAuthenticationToken(已认证状态)
        List<String> authorities = auth.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.toList());
        boolean isAuthenticated = auth.isAuthenticated();  // true
        Object principal = auth.getPrincipal();             // UserDetails 对象
        Object credentials = auth.getCredentials();         // null(已擦除)
        return ResponseEntity.ok(Map.of(
            "authenticated", isAuthenticated,
            "authorities", authorities,
            "principalType", principal.getClass().getSimpleName(),
            "credentials", credentials
        ));
    }
}

结果分析

  • UsernamePasswordAuthenticationToken 作为标准载体,贯穿"过滤器 → 认证管理器 → 安全上下文 → 业务层"全流程
  • 已认证状态下 credentials 为 null,防止密码在业务层泄露
  • principal 从 String 升级为 UserDetails,让业务层能获取完整用户信息

易错场景:错误地构造已认证 Token 导致安全漏洞

问题描述

开发者试图在业务代码中手动标记用户为已登录,直接使用 setAuthenticated(true) 或调用带 authorities 的构造器但传入伪造的权限。

错误代码

@Service
public class BadLoginService {

    public void quickLogin(String username) {
        // 极度危险!绕过所有认证逻辑,直接伪造已认证 Token
        UsernamePasswordAuthenticationToken auth =
            new UsernamePasswordAuthenticationToken(username, null);
        auth.setAuthenticated(true);  // 抛出异常!框架已阻止
        SecurityContextHolder.getContext().setAuthentication(auth);
    }
}
// 另一种错误:构造已认证 Token 但传入任意权限
List<GrantedAuthority> fakeAuthorities = Arrays.asList(
    new SimpleGrantedAuthority("ROLE_ADMIN")
);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
    username, null, fakeAuthorities  // 任何人都能伪造 ADMIN 权限!
);
SecurityContextHolder.getContext().setAuthentication(auth);

原因分析

UsernamePasswordAuthenticationToken 的 setAuthenticated(true) 被显式禁止,就是为了防止此类攻击。但如果开发者使用带 authorities 的构造器(这是合法途径),却绕过了真正的认证流程(如 AuthenticationManager),就能伪造任意权限。

正确做法

永远不要让未经验证的代码路径构造已认证的 UsernamePasswordAuthenticationToken。所有认证必须经过 AuthenticationManager:

// 正确:委托给 AuthenticationManager
UsernamePasswordAuthenticationToken token =
    new UsernamePasswordAuthenticationToken(username, password);
Authentication result = authenticationManager.authenticate(token);
// result 已经过 Provider 验证,权限来自 UserDetailsService,不可伪造
SecurityContextHolder.getContext().setAuthentication(result);

对于测试或内部系统调用,使用 TestingAuthenticationToken 或 RunAsManager,而不是绕过 AuthenticationManager。

面试考点:

  • UsernamePasswordAuthenticationToken 的两个构造器有什么区别?
  • 为什么 setAuthenticated(true) 会抛出异常?框架的设计意图是什么?
  • 认证成功后,principal 和 credentials 分别变成什么?
  • 手写过滤器时,如何正确构造和提交 UsernamePasswordAuthenticationToken?
上一页
SecurityContextHolder