首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Spring Security 進階:配置多個 AuthenticationProvider 實現混合認證

     編輯:Java 相關知識

前言

網路上關於 Spring Security 的文章大多停留在入門層級,要么是配置 UserDetailsService,要么就是簡單的路由控制,且不少還在使用 XML 配置。本文將跳過基礎內容,專注於以 Java Config 方式配置多個 AuthenticationProvider,實現記憶體認證與資料庫認證的混合模式。

Spring Security 認證架構核心解析

Spring Security 本質上是一組 Filter 鏈,核心流程如下:

  • Filter 層:如 UsernamePasswordAuthenticationFilter 攔截登入請求
  • AuthenticationManager:認證管理器,持有 AuthenticationProvider 集合
  • AuthenticationProvider:真正執行認證邏輯的組件
  • 成功/失敗 Handler:處理認證結果

當 AuthenticationProvider.authenticate() 返回 null 時,ProviderManager 會嘗試下一個支援該 Authentication 類型的 Provider,這就是多 Provider 鏈式調用的關鍵機制。

實作自定義記憶體 AuthenticationProvider

首先建立一個僅支援 root/root 帳號的記憶體認證 Provider:

@Component
public class InMemoryAuthenticationProvider implements AuthenticationProvider {

    private final String adminName = "root";
    private final String adminPassword = "root";
    private final List<GrantedAuthority> authorities = Arrays.asList(
        new SimpleGrantedAuthority("CAN_SEARCH"),
        new SimpleGrantedAuthority("CAN_EXPORT"),
        new SimpleGrantedAuthority("CAN_IMPORT"),
        new SimpleGrantedAuthority("CAN_BORROW"),
        new SimpleGrantedAuthority("CAN_RETURN"),
        new SimpleGrantedAuthority("CAN_REPAIR"),
        new SimpleGrantedAuthority("CAN_DISCARD"),
        new SimpleGrantedAuthority("CAN_EMPOWERMENT"),
        new SimpleGrantedAuthority("CAN_BREED")
    );

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if (isMatch(authentication)) {
            User user = new User(authentication.getName(), 
                authentication.getCredentials().toString(), authorities);
            return new UsernamePasswordAuthenticationToken(user, authentication.getCredentials(), authorities);
        }
        return null; // 交給下一個 Provider
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return true; // 簡化處理,實際建議判斷具體類型
    }

    private boolean isMatch(Authentication authentication) {
        return adminName.equals(authentication.getName()) 
            && adminPassword.equals(authentication.getCredentials());
    }
}

關鍵點:supports() 返回 true 表示支援所有類型,實際專案中建議精確匹配 UsernamePasswordAuthenticationToken.class。

整合資料庫認證 Provider

資料庫認證直接複用 Spring Security 內建的 DaoAuthenticationProvider:

@Bean
public DaoAuthenticationProvider daoAuthenticationProvider(UserDetailsService userServiceDetails) {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setPasswordEncoder(new BCryptPasswordEncoder());
    provider.setUserDetailsService(userServiceDetails);
    return provider;
}

組裝多 Provider 的 AuthenticationManager

在 WebSecurityConfigurerAdapter 中覆寫 authenticationManager():

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userServiceDetails;
    
    @Autowired
    private InMemoryAuthenticationProvider inMemoryAuthenticationProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .rememberMe().alwaysRemember(true).tokenValiditySeconds(86400).and()
            .authorizeRequests()
                .antMatchers("/", "/*swagger*/**", "/v2/api-docs").permitAll()
                .anyRequest().authenticated().and()
            .formLogin()
                .loginPage("/")
                .loginProcessingUrl("/login")
                .successHandler(new AjaxLoginSuccessHandler())
                .failureHandler(new AjaxLoginFailureHandler()).and()
            .logout().logoutUrl("/logout").logoutSuccessUrl("/");
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/public/**", "/webjars/**", "/v2/**", "/swagger**");
    }

    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        ProviderManager manager = new ProviderManager(
            Arrays.asList(inMemoryAuthenticationProvider, daoAuthenticationProvider())
        );
        // 關鍵:不清除憑證,否則 TokenBasedRememberMeServices 會因找不到 Credentials 拋出異常
        manager.setEraseCredentialsAfterAuthentication(false);
        return manager;
    }

    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setPasswordEncoder(new BCryptPasswordEncoder());
        provider.setUserDetailsService(userServiceDetails);
        return provider;
    }

    @Override
    protected UserDetailsService userDetailsService() {
        return userServiceDetails; // RememberMeServices 需要
    }
}

核心配置解析

Provider 順序決定優先級

Arrays.asList(inMemoryAuthenticationProvider, daoAuthenticationProvider()) 中,記憶體 Provider 在前,意味著 root 帳號會優先被攔截認證,失敗才轉交資料庫 Provider。

為什麼要 setEraseCredentialsAfterAuthentication(false)

預設認證成功後會清除密碼。但 TokenBasedRememberMeServices 在生成記住我 Token 時需要讀取憑證,若已清除會觸發 UserDetailsService.loadUserByUsername 導致 UsernameNotFoundException。

總結

多 AuthenticationProvider 配置的核心在於:

  1. 實作自定義 Provider 或複用內建 Provider
  2. 透過 ProviderManager 組裝 Provider 列表,順序即優先級
  3. 根據業務需求調整 eraseCredentialsAfterAuthentication 等行為

這種方式比在單一 UserDetailsService 中硬編碼多種認證邏輯更符合開閉原則,也更易於擴展(如加入 LDAP、OAuth2 等 Provider)。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved