知識庫 / Spring / Spring Security RSS 訂閱

Spring Security 中增加額外登錄字段

Spring Security
HongKong
10
02:07 PM · Dec 06 ,2025

1. 簡介

本文將通過在標準登錄表單中Spring Security添加額外字段來實現自定義身份驗證場景。

我們將重點關注2種不同的方法,以展示框架的多樣性和我們靈活地使用它的方式。

我們的第一種方法將是一個簡單的解決方案,專注於重用現有的 Spring Security 實現。

我們的第二種方法將是一個更自定義的解決方案,可能更適合高級用例。

我們將構建在我們在 Spring Security 登錄文章中討論的概念之上。

2. Maven 設置

我們將使用 Spring Boot Starter 來初始化我們的項目並引入所有必要的依賴。

我們將採用的設置需要父模塊聲明、Web Starter 和 Security Starter;我們還將包含 Thymeleaf:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.2</version>
    <relativePath/>
</parent>
 
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
     </dependency>
     <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    </dependency>
</dependencies>

Spring Boot 安全啓動器的最新版本可以在 Maven 中央倉庫 找到。

3. 簡單項目設置

在我們的首次方法中,我們將重點重用 Spring Security 提供的實現。特別是,我們將重用 DaoAuthenticationProviderUsernamePasswordToken,因為它們“開箱即用”。

關鍵組件包括:

  • SimpleAuthenticationFilter UsernamePasswordAuthenticationFilter 的擴展
  • SimpleUserDetailsService UserDetailsService 的實現
  • User – Spring Security 提供的 User 類的擴展,聲明我們的額外 domain 字段
  • SecurityConfig – 我們的 Spring Security 配置,將 SimpleAuthenticationFilter 插入到過濾器鏈中,聲明安全規則並配置依賴關係
  • login.html 一個收集 usernamepassworddomain 的登錄頁面

3.1. 簡單身份驗證過濾器

在我們的 SimpleAuthenticationFilter 中,請求中的 domain 和 username 字段會被提取出來。 我們將這些值連接起來,並使用它們創建一個 UsernamePasswordAuthenticationToken 實例。

然後,該 token 會傳遞給 AuthenticationProvider 進行身份驗證

public class SimpleAuthenticationFilter
  extends UsernamePasswordAuthenticationFilter {

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

        // ...

        UsernamePasswordAuthenticationToken authRequest
          = getAuthRequest(request);
        setDetails(request, authRequest);
        
        return this.getAuthenticationManager()
          .authenticate(authRequest);
    }

    private UsernamePasswordAuthenticationToken getAuthRequest(
      HttpServletRequest request) {
 
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        String domain = obtainDomain(request);

        // ...

        String usernameDomain = String.format("%s%s%s", username.trim(), 
          String.valueOf(Character.LINE_SEPARATOR), domain);
        return new UsernamePasswordAuthenticationToken(
          usernameDomain, password);
    }

    // other methods
}

3.2. 簡單的 UserDetails 服務

UserDetailsService 接口定義了一個名為 loadUserByUsername 的單一方法。 我們的實現提取 usernamedomain。 這些值隨後傳遞給我們的 UserRepository 以獲取 User

public class SimpleUserDetailsService implements UserDetailsService {

    // ...

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String[] usernameAndDomain = StringUtils.split(
          username, String.valueOf(Character.LINE_SEPARATOR));
        if (usernameAndDomain == null || usernameAndDomain.length != 2) {
            throw new UsernameNotFoundException("Username and domain must be provided");
        }
        User user = userRepository.findUser(usernameAndDomain[0], usernameAndDomain[1]);
        if (user == null) {
            throw new UsernameNotFoundException(
              String.format("Username not found for domain, username=%s, domain=%s", 
                usernameAndDomain[0], usernameAndDomain[1]));
        }
        return user;
    }
}

3.3. Spring Security 配置

我們的配置與標準的 Spring Security 配置不同,因為我們會在過濾器鏈中在默認配置之前,通過調用 addFilterBefore 方法,插入我們的 SimpleAuthenticationFilter

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.csrf(AbstractHttpConfigurer::disable)
        .authorizeRequests()
        .requestMatchers("/css/**", "/index")
        .permitAll()
        .requestMatchers("/user/**")
        .authenticated()
        .and()
        .formLogin(httpSecurityFormLoginConfigurer -> httpSecurityFormLoginConfigurer.loginPage("/login").permitAll())
        .logout(httpSecurityLogoutConfigurer -> httpSecurityLogoutConfigurer.logoutUrl("/logout").permitAll())
        .with(securityConfig(), Customizer.withDefaults());
    return http.getOrBuild();
}
我們能夠使用提供的 DaoAuthenticationProvider,是因為我們用它配置了我們的 SimpleUserDetailsService。請記住,我們的 SimpleUserDetailsService 知道如何解析我們的 usernamedomain 字段,並返回用於身份驗證時使用的適當 User
public AuthenticationProvider authProvider() {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(userDetailsService);
    provider.setPasswordEncoder(passwordEncoder());
    return provider;
}

由於我們使用了 SimpleAuthenticationFilter,因此我們配置了自定義的 AuthenticationFailureHandler,以確保失敗/成功的登錄嘗試得到適當的處理:

public SimpleAuthenticationFilter authenticationFilter() throws Exception {
    SimpleAuthenticationFilter filter = new SimpleAuthenticationFilter();
    filter.setAuthenticationManager(authenticationManagerBean());
    filter.setAuthenticationFailureHandler(failureHandler());
    filter.setAuthenticationSuccessHandler(new SavedRequestAwareAuthenticationSuccessHandler());
    filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository());
    return filter;
}

請注意,我們還需要添加一個安全上下文存儲庫。

3.4. 登錄頁面

本登錄頁面收集我們的附加 字段,該字段由我們的 SimpleAuthenticationFilter提取。

<form class="form-signin" th:action="@{/login}" method="post">
 <h2 class="form-signin-heading">Please sign in</h2>
 <p>Example: user / domain / password</p>
 <p th:if="${param.error}" class="error">Invalid user, password, or domain</p>
 <p>
   <label for="username" class="sr-only">Username</label>
   <input type="text" id="username" name="username" class="form-control" 
     placeholder="Username" required autofocus/>
 </p>
 <p>
   <label for="domain" class="sr-only">Domain</label>
   <input type="text" id="domain" name="domain" class="form-control" 
     placeholder="Domain" required autofocus/>
 </p>
 <p>
   <label for="password" class="sr-only">Password</label>
   <input type="password" id="password" name="password" class="form-control" 
     placeholder="Password" required autofocus/>
 </p>
 <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button><br/>
 <p><a href="/index" th:href="@{/index}">Back to home page</a></p>
</form>

當我們運行應用程序並訪問上下文,在 http://localhost:8081,我們會看到一個鏈接,用於訪問受保護的頁面。點擊該鏈接將導致登錄頁面顯示。正如預期的那樣,我們看到額外的域名字段

3.5. 總結

在第一個示例中,我們成功地重用了 DaoAuthenticationProviderUsernamePasswordAuthenticationToken,通過“欺騙”用户名字段來實現。

因此,我們能夠通過最小的配置和額外代碼,添加對額外登錄字段的支持

4. 自定義項目設置

我們的第二種方法與第一種方法非常相似,但更適合非簡單的用例。

我們的第二種方法的主要組件包括:

  • CustomAuthenticationFilter UsernamePasswordAuthenticationFilter 的擴展
  • CustomUserDetailsService 聲明一個 loadUserbyUsernameAndDomain 方法的自定義接口
  • CustomUserDetailsServiceImpl CustomUserDetailsService 的實現
  • CustomUserDetailsAuthenticationProvider AbstractUserDetailsAuthenticationProvider 的擴展
  • CustomAuthenticationToken UsernamePasswordAuthenticationToken 的擴展
  • User Spring Security 提供的 User 類,聲明我們的額外 domain 字段
  • SecurityConfig 我們的 Spring Security 配置,將 CustomAuthenticationFilter 插入到過濾器鏈中,聲明安全規則並連接依賴項
  • login.html 收集 username, passworddomain 的登錄頁面

4.1. 自定義身份驗證過濾器

在我們的 CustomAuthenticationFilter 中,我們 從請求中提取用户名、密碼和域名字段。 這些值用於創建我們的 CustomAuthenticationToken 實例,該實例傳遞給 AuthenticationProvider 進行身份驗證:

public class CustomAuthenticationFilter 
  extends UsernamePasswordAuthenticationFilter {

    public static final String SPRING_SECURITY_FORM_DOMAIN_KEY = "domain";

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

        // ...

        CustomAuthenticationToken authRequest = getAuthRequest(request);
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    private CustomAuthenticationToken getAuthRequest(HttpServletRequest request) {
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        String domain = obtainDomain(request);

        // ...

        return new CustomAuthenticationToken(username, password, domain);
    }

4.2. 自定義 UserDetails 服務

我們的 CustomUserDetailsService 契約定義了一個名為 loadUserByUsernameAndDomain 的單一方法。

CustomUserDetailsServiceImpl 類我們創建的僅僅是實現了該契約,並委託給我們的 CustomUserRepository 來獲取 User

 public UserDetails loadUserByUsernameAndDomain(String username, String domain) 
     throws UsernameNotFoundException {
     if (StringUtils.isAnyBlank(username, domain)) {
         throw new UsernameNotFoundException("Username and domain must be provided");
     }
     User user = userRepository.findUser(username, domain);
     if (user == null) {
         throw new UsernameNotFoundException(
           String.format("Username not found for domain, username=%s, domain=%s", 
             username, domain));
     }
     return user;
 }

4.3. 自定義 <em>UserDetailsAuthenticationProvider</em>

我們的 <em>CustomUserDetailsAuthenticationProvider</em> 繼承了 <em>AbstractUserDetailsAuthenticationProvider</em> 並委託給我們的 <em>CustomUserDetailService</em> 以檢索 <em>User</em>。該類的最重要功能是實現 <em>retrieveUser</em> 方法。

請注意,為了訪問我們的自定義字段,必須將身份驗證令牌轉換為我們的 <em>CustomAuthenticationToken</em>

@Override
protected UserDetails retrieveUser(String username, 
  UsernamePasswordAuthenticationToken authentication) 
    throws AuthenticationException {
 
    CustomAuthenticationToken auth = (CustomAuthenticationToken) authentication;
    UserDetails loadedUser;

    try {
        loadedUser = this.userDetailsService
          .loadUserByUsernameAndDomain(auth.getPrincipal()
            .toString(), auth.getDomain());
    } catch (UsernameNotFoundException notFound) {
 
        if (authentication.getCredentials() != null) {
            String presentedPassword = authentication.getCredentials()
              .toString();
            passwordEncoder.matches(presentedPassword, userNotFoundEncodedPassword);
        }
        throw notFound;
    } catch (Exception repositoryProblem) {
 
        throw new InternalAuthenticationServiceException(
          repositoryProblem.getMessage(), repositoryProblem);
    }

    // ...

    return loadedUser;
}

4.4. 總結

我們的第二種方法與我們之前呈現的簡單方法非常相似。通過實現我們自己的 AuthenticationProviderCustomAuthenticationToken,我們避免了需要使用自定義解析邏輯來調整用户名字段的需求。

5. 結論

在本文中,我們實現了使用額外登錄字段的 Spring Security 形式登錄。我們通過兩種不同的方式來實現的:

  • 在我們的簡單方法中,我們儘量減少了需要編寫的代碼量。我們成功地重用 DaoAuthenticationProvider並使用自定義解析邏輯調整用户名
  • 在我們的更定製化的方法中,我們通過擴展 AbstractUserDetailsAuthenticationProvider並提供自定義 CustomUserDetailsService以及 CustomAuthenticationToken來提供自定義字段支持
user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.