Skip to content

用户认证

在 Spring Security 中,认证是确认用户身份的过程。Spring Security 提供了丰富的认证功能和多种认证策略,适应各种应用场景。

认证核心概念

认证的基本流程

  1. 用户提交凭证(如用户名和密码)
  2. 系统验证凭证的有效性
  3. 如果验证成功,创建认证对象并存储在安全上下文中
  4. 如果验证失败,抛出认证异常并处理

核心组件和接口

1. Authentication

Authentication 接口是 Spring Security 认证体系的核心,它有两个主要职责:

  1. 作为 AuthenticationManager 的输入,提供用户认证信息
  2. 代表当前已认证用户,存储在 SecurityContext
java
public interface Authentication extends Principal, Serializable {
    // 获取用户的权限集合
    Collection<? extends GrantedAuthority> getAuthorities();
    
    // 获取凭证(通常是密码),认证成功后会被清除
    Object getCredentials();
    
    // 获取详细信息(如IP地址、证书序列号等)
    Object getDetails();
    
    // 获取主体(通常是UserDetails对象)
    Object getPrincipal();
    
    // 是否已认证
    boolean isAuthenticated();
    
    // 设置认证状态
    void setAuthenticated(boolean isAuthenticated);
}

2. AuthenticationManager

AuthenticationManager 是处理认证请求的主要接口,其默认实现是 ProviderManager

java
public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

3. AuthenticationProvider

AuthenticationProvider 负责执行特定类型的认证:

java
public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
    boolean supports(Class<?> authentication);
}

主要的 AuthenticationProvider 实现包括:

  • DaoAuthenticationProvider:使用 UserDetailsService 进行认证
  • LdapAuthenticationProvider:LDAP 认证
  • JwtAuthenticationProvider:JWT 令牌认证
  • OAuth2LoginAuthenticationProvider:OAuth2 认证

4. UserDetailsService

UserDetailsService 负责从持久层加载用户信息:

java
public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

5. UserDetails

UserDetails 接口提供了核心用户信息:

java
public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}

常见认证方式实现

1. 表单登录认证

表单登录是最常见的认证方式,用户通过提交包含用户名和密码的表单进行认证。

基本配置

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")              // 自定义登录页面
                .loginProcessingUrl("/process-login")  // 处理登录请求的URL
                .defaultSuccessUrl("/home", true)      // 登录成功后的跳转页面
                .failureUrl("/login?error")            // 登录失败后的跳转页面
                .usernameParameter("username")         // 用户名参数名称
                .passwordParameter("password")         // 密码参数名称
                .permitAll()
            );
        
        return http.build();
    }
}

自定义登录页面

html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>登录</title>
</head>
<body>
    <h2>登录表单</h2>
    <div th:if="${param.error}">
        <p>用户名或密码错误</p>
    </div>
    <div th:if="${param.logout}">
        <p>您已成功注销</p>
    </div>
    <form th:action="@{/process-login}" method="post">
        <div>
            <label for="username">用户名:</label>
            <input type="text" id="username" name="username" required />
        </div>
        <div>
            <label for="password">密码:</label>
            <input type="password" id="password" name="password" required />
        </div>
        <button type="submit">登录</button>
    </form>
</body>
</html>

2. 基于数据库的认证

大多数应用会将用户信息存储在数据库中,可以通过实现 UserDetailsService 来从数据库加载用户信息。

java
@Service
public class DatabaseUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;
    
    @Autowired
    public DatabaseUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("User not found"));
        
        return org.springframework.security.core.userdetails.User
            .withUsername(user.getUsername())
            .password(user.getPassword())
            .roles(user.getRoles().toArray(new String[0]))
            .accountExpired(!user.isActive())
            .accountLocked(!user.isActive())
            .credentialsExpired(!user.isActive())
            .disabled(!user.isActive())
            .build();
    }
}

配置使用自定义的 UserDetailsService

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;
    
    @Autowired
    public SecurityConfig(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
    }
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .formLogin(withDefaults());
        
        return http.build();
    }
    
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder);
        return provider;
    }
    
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

3. HTTP Basic 认证

HTTP Basic 认证是一种简单的认证方式,常用于 API 访问。

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
        
        return http.build();
    }
}

4. 记住我功能

"记住我"功能允许用户会话过期后仍然保持登录状态。

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .formLogin(withDefaults())
            .rememberMe(rememberMe -> rememberMe
                .key("uniqueAndSecretKey")      // 用于标识令牌的密钥
                .tokenValiditySeconds(86400)     // 令牌有效期(秒)
                .rememberMeParameter("remember-me") // 复选框参数名
            );
        
        return http.build();
    }
    
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        // 使用数据库存储令牌,而不是内存
        JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
        repo.setDataSource(dataSource);
        return repo;
    }
}

登录表单中添加记住我复选框:

html
<form th:action="@{/login}" method="post">
    <!-- 用户名和密码字段 -->
    <div>
        <input type="checkbox" id="remember-me" name="remember-me" />
        <label for="remember-me">记住我</label>
    </div>
    <button type="submit">登录</button>
</form>

5. OAuth2 / OpenID Connect 认证

对于现代应用,OAuth2 和 OpenID Connect 是常用的认证协议,允许用户使用第三方身份提供商(如 Google、Facebook)登录。

java
@Configuration
@EnableWebSecurity
public class OAuth2LoginConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .defaultSuccessUrl("/home")
                .failureUrl("/login?error")
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(oauth2UserService())
                )
            );
        
        return http.build();
    }
    
    @Bean
    public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
        DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
        
        return request -> {
            OAuth2User oauth2User = delegate.loadUser(request);
            
            // 提取身份提供商的详细信息
            String registrationId = request.getClientRegistration().getRegistrationId();
            
            // 处理用户信息,可以将OAuth2用户映射到应用内部用户
            
            return oauth2User;
        };
    }
}

application.properties 或 application.yml 中配置 OAuth2 提供商:

yaml
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: your-client-id
            client-secret: your-client-secret
            scope: openid,profile,email
          github:
            client-id: your-github-client-id
            client-secret: your-github-client-secret

认证相关的高级特性

1. 多因素认证 (MFA)

多因素认证为应用提供了额外的安全层。可以通过自定义 AuthenticationProvider 实现:

java
@Component
public class MfaAuthenticationProvider implements AuthenticationProvider {

    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;
    private final MfaTokenService mfaTokenService;
    
    // 实现省略...
    
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 第一步:验证用户名和密码
        
        // 第二步:验证 MFA 令牌
        
        // 创建成功认证
    }
    
    @Override
    public boolean supports(Class<?> authentication) {
        return MfaAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

2. 自定义过滤器

有时需要添加自定义认证过滤器以支持特殊需求:

java
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {
        // 从请求中提取 JWT 令牌
        String jwt = extractJwtFromRequest(request);
        
        if (jwt != null && jwtService.validateToken(jwt)) {
            String username = jwtService.extractUsername(jwt);
            
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                
                UsernamePasswordAuthenticationToken authentication = 
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                        
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String extractJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        
        return null;
    }
}

注册自定义过滤器:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
            // 其他配置...
            
        return http.build();
    }
}

3. 处理认证成功和失败事件

可以通过自定义处理器处理认证事件:

java
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private final UserService userService;
    
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        // 获取当前认证用户
        String username = authentication.getName();
        
        // 更新最后登录时间
        userService.updateLastLogin(username);
        
        // 记录登录日志
        userService.logLoginSuccess(username, request.getRemoteAddr());
        
        // 重定向到适当的页面
        response.sendRedirect("/dashboard");
    }
}

@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

    private static final Logger logger = LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);
    
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        // 记录失败尝试
        logger.warn("Authentication failed for IP: {}, reason: {}", 
                    request.getRemoteAddr(), exception.getMessage());
        
        // 可以增加失败计数,实现账户锁定功能
        
        // 重定向到登录页面
        response.sendRedirect("/login?error=" + exception.getMessage());
    }
}

在配置中使用自定义处理器:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final AuthenticationSuccessHandler successHandler;
    private final AuthenticationFailureHandler failureHandler;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .formLogin(form -> form
                .successHandler(successHandler)
                .failureHandler(failureHandler)
            );
        
        return http.build();
    }
}

认证最佳实践

  1. 使用强密码策略:配置密码复杂度要求
  2. 实施账户锁定:防止暴力破解攻击
  3. 使用 HTTPS:加密认证数据传输
  4. 定期密码过期:强制用户定期更改密码
  5. 安全日志记录:记录所有认证尝试
  6. 使用多因素认证:提高安全级别
  7. 防止会话劫持:实施会话固定保护
  8. 最小特权原则:仅授予必要的权限

总结

Spring Security 提供了全面的认证功能,支持多种认证机制和策略。通过合理配置和扩展,可以实现从简单的表单登录到复杂的多因素认证等各种认证需求。在实际应用中,应该选择适合业务需求的认证方式,并遵循安全最佳实践,确保系统安全性。