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

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

DaoAuthenticationProvider

定义与作用

DaoAuthenticationProvider(Data Access Object Authentication Provider)是 Spring Security 中最常用、最基础的认证提供者。它基于 UserDetailsService 从数据源(数据库、内存、LDAP 等)加载用户信息,然后使用 PasswordEncoder 比对密码,完成认证。

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    private PasswordEncoder passwordEncoder;
    private UserDetailsService userDetailsService;
    // ...
}

手写过滤器的痛点

手写登录认证时,通常把所有逻辑写在一个类里:

// 痛点:查库、比对密码、判断账户状态全混在一起,无法复用
public class HandmadeLoginService {
    public boolean login(String username, String password) {
        User user = userDao.findByUsername(username);  // 查库
        if (user == null) return false;
        if (user.isLocked()) return false;              // 状态检查
        if (user.isExpired()) return false;
        return user.getPassword().equals(password);      // 明文比对!无编码策略
    }
}

DaoAuthenticationProvider 将认证流程拆分为标准阶段:加载用户 → 预检查 → 密码比对 → 后检查 → 组装结果。每个阶段都有标准接口和扩展点,安全且可复用。


核心原理

认证四阶段流程

各阶段源码逻辑(简化)

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) {
    // 阶段1:加载用户
    UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
    if (loadedUser == null) {
        throw new InternalAuthenticationServiceException("UserDetailsService returned null");
    }
    return loadedUser;
}

@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
        UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    // 阶段3:密码比对
    if (authentication.getCredentials() == null) {
        throw new BadCredentialsException("Bad credentials");
    }
    String presentedPassword = authentication.getCredentials().toString();
    if (!this.getPasswordEncoder().matches(presentedPassword, userDetails.getPassword())) {
        throw new BadCredentialsException("Bad credentials");
    }
}

支持的 Token 类型

DaoAuthenticationProvider 只处理 UsernamePasswordAuthenticationToken:

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

这意味着表单登录、HTTP Basic 等产生 UsernamePasswordAuthenticationToken 的过滤器,最终都会汇聚到 DaoAuthenticationProvider。


示例一:自定义用户加载 + 密码编码升级

场景说明

系统早期使用 MD5 存储密码,现迁移到 BCrypt。需要让 DaoAuthenticationProvider 在验证 MD5 密码通过后,自动将其升级为 BCrypt。

操作前配置

密码以 MD5 明文存储,无编码器:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService);  // 没有 passwordEncoder,Spring Security 5.x 会报错
}

操作后配置

使用 DelegatingPasswordEncoder + UserDetailsPasswordService 实现自动升级:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        // 启用密码自动升级
        provider.setUserDetailsPasswordService((UserDetailsPasswordService) userDetailsService);
        auth.authenticationProvider(provider);
    }

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

自定义 UserDetailsService 同时实现 UserDetailsPasswordService:

@Service
public class CustomUserDetailsService implements UserDetailsService, UserDetailsPasswordService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity entity = userMapper.findByUsername(username);
        if (entity == null) {
            throw new UsernameNotFoundException("用户不存在: " + username);
        }
        return User.builder()
            .username(entity.getUsername())
            .password(entity.getPassword())   // 数据库中可能是 {MD5}xxx 或 {bcrypt}xxx
            .roles(entity.getRole().split(","))
            .build();
    }

    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        // 认证成功后,DaoAuthenticationProvider 会自动调用此方法
        userMapper.updatePassword(user.getUsername(), newPassword);
        return User.builder()
            .username(user.getUsername())
            .password(newPassword)
            .authorities(user.getAuthorities())
            .build();
    }
}

结果分析

  • 用户密码在数据库中存储为 {MD5}abc123...
  • DelegatingPasswordEncoder 识别 {MD5} 前缀,使用 MD5 编码器比对密码
  • 密码验证通过后,DaoAuthenticationProvider 检测到当前编码策略与默认策略不一致,调用 updatePassword() 将密码更新为 {bcrypt}$2a$10$...
  • 下次登录时,自动使用 BCrypt 比对

示例二:利用预检查实现账户锁定策略

场景说明

业务要求:用户连续 5 次密码错误后,账户锁定 30 分钟。通过自定义 UserDetails 返回的账户状态,让 DaoAuthenticationProvider 的预检查自动拦截锁定账户。

操作前配置

没有账户锁定机制,密码错误可无限重试:

@Override
public UserDetails loadUserByUsername(String username) {
    UserEntity entity = userMapper.findByUsername(username);
    return new org.springframework.security.core.userdetails.User(
        entity.getUsername(),
        entity.getPassword(),
        AuthorityUtils.createAuthorityList("ROLE_USER")
    );
}

操作后配置

自定义 UserDetails 实现,将登录失败次数和锁定时间纳入状态判断:

@Service
public class LockableUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private LoginAttemptService loginAttemptService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity entity = userMapper.findByUsername(username);
        if (entity == null) {
            throw new UsernameNotFoundException("用户不存在");
        }

        boolean isLocked = loginAttemptService.isBlocked(username);
        boolean isEnabled = entity.isActive();

        return new LockableUserDetails(
            entity.getUsername(),
            entity.getPassword(),
            isEnabled,
            !isLocked,          // accountNonLocked:锁定状态
            true,               // accountNonExpired
            true,               // credentialsNonExpired
            AuthorityUtils.createAuthorityList("ROLE_USER")
        );
    }
}
public class LockableUserDetails extends org.springframework.security.core.userdetails.User {

    public LockableUserDetails(String username, String password, boolean enabled,
                               boolean accountNonLocked, boolean accountNonExpired,
                               boolean credentialsNonExpired, Collection authorities) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
    }
}

监听认证失败事件,更新失败计数:

@Component
public class AuthenticationFailureListener implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> {

    @Autowired
    private LoginAttemptService loginAttemptService;

    @Override
    public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) {
        String username = event.getAuthentication().getPrincipal().toString();
        loginAttemptService.loginFailed(username);
    }
}

配置 DaoAuthenticationProvider:

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

结果分析

  • 当用户连续 5 次密码错误,loginAttemptService 标记该用户为锁定状态
  • 第 6 次登录时,loadUserByUsername() 返回的 UserDetails 中 accountNonLocked = false
  • DaoAuthenticationProvider 在 preAuthenticationChecks 阶段检测到 !isAccountNonLocked(),抛出 LockedException
  • 无需改动 DaoAuthenticationProvider 本身,仅通过 UserDetailsService 返回的状态即可实现账户锁定

易错场景:密码错误 vs 用户不存在的信息泄露

问题描述

新手常试图在 UserDetailsService 中区分"用户不存在"和"密码错误",给前端返回不同的错误提示,这会造成用户枚举漏洞。

错误代码

@Service
public class BadUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) {
        UserEntity user = userMapper.findByUsername(username);
        if (user == null) {
            // 危险!攻击者可以通过不同错误信息枚举哪些用户名存在
            throw new UsernameNotFoundException("用户不存在");  // ← 信息泄露
        }
        return new User(user.getUsername(), user.getPassword(), ...);
    }
}

正确做法

DaoAuthenticationProvider 默认已经将 UsernameNotFoundException 和 BadCredentialsException 统一包装为 BadCredentialsException:

// AbstractUserDetailsAuthenticationProvider 源码
try {
    user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
} catch (UsernameNotFoundException ex) {
    // 注意:如果 hideUserNotFoundExceptions = true(默认),会转为 BadCredentialsException
    if (this.hideUserNotFoundExceptions) {
        throw new BadCredentialsException("Bad credentials");
    } else {
        throw ex;
    }
}

默认情况下,无论用户不存在还是密码错误,前端收到的都是 BadCredentialsException: Bad credentials。保持 hideUserNotFoundExceptions = true(默认),不要试图关闭它。

面试考点:

  • DaoAuthenticationProvider 的认证四阶段分别是什么?
  • hideUserNotFoundExceptions 的作用是什么?为什么默认隐藏?
  • 如何通过 UserDetailsService 实现账户锁定/过期,而不改动 DaoAuthenticationProvider?
上一页
ProviderManager
下一页
UserDetails