Spring Security在標準登入表單中新增一個額外的欄位

程式猿Knight發表於2019-05-24

概述

在本文中,我們將通過向標準登入表單新增額外欄位來實現Spring Security的自定義身份驗證方案

我們將重點關注兩種不同的方法,以展示框架的多功能性以及我們可以使用它的靈活方式

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

我們的第二種方法是更加定製的解決方案,可能更適合高階用例。

2. Maven設定

我們將使用Spring Boot啟動程式來引導我們的專案並引入所有必需的依賴項。
我們將使用的設定需要父宣告,Web啟動器和安全啟動器;我們還將包括thymeleaf :

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.M7</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-springsecurity4</artifactId>
    </dependency>
</dependencies>

可以在Maven Central找到最新版本的Spring Boot安全啟動器。

3.簡單的專案設定

在我們的第一種方法中,我們將專注於重用Spring Security提供的實現。特別是,我們將重用DaoAuthenticationProvider和UsernamePasswordToken,因為它們是“開箱即用”的

關鍵元件包括:
  • SimpleAuthenticationFilter - UsernamePasswordAuthenticationFilter的擴充套件
  • SimpleUserDetailsService - UserDetailsService的實現
  • User - Spring Security提供的User類的擴充套件,它宣告瞭我們的額外域欄位
  • SecurityConfig - 我們的Spring Security配置,它將SimpleAuthenticationFilter插入到過濾器鏈中,宣告安全規則並連線依賴項
  • login.html - 收集使用者名稱,密碼和域的登入頁面

3.1. 簡單Authentication Filter

在我們的SimpleAuthenticationFilter中,域和使用者名稱欄位是從請求中提取的。我們連線這些值並使用它們來建立UsernamePasswordAuthenticationToken的例項。

然後將令牌傳遞給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的方法。我們的實現提取使用者名稱和域名。然後將值傳遞給我們的UserRepository以獲取使用者:

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插入到過濾器鏈中:

@Override
protected void configure(HttpSecurity http) throws Exception {
 
    http
      .addFilterBefore(authenticationFilter(), 
        UsernamePasswordAuthenticationFilter.class)
      .authorizeRequests()
        .antMatchers("/css/**", "/index").permitAll()
        .antMatchers("/user/**").authenticated()
      .and()
      .formLogin().loginPage("/login")
      .and()
      .logout()
      .logoutUrl("/logout");
}

我們可以使用提供的DaoAuthenticationProvider,因為我們使用SimpleUserDetailsService配置它。回想一下,我們的SimpleUserDetailsService知道如何解析我們的使用者名稱和域欄位,並返回在驗證時使用的相應使用者。

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());
    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上下文時,我們會看到一個訪問安全頁面的連結。單擊該連結將顯示登入頁面。正如所料,我們看到了額外的域名欄位
image

3.5.總結

在我們的第一個例子中,我們能夠通過“偽造”使用者名稱欄位來重用DaoAuthenticationProvider和UsernamePasswordAuthenticationToken

因此,我們能夠使用最少量的配置和其他程式碼新增對額外登入欄位的支援

4.自定義專案設定

我們的第二種方法與第一種方法非常相似,但可能更適合於非平凡用例。

我們的第二種方法的關鍵組成部分包括:

  • CustomAuthenticationFilter - UsernamePasswordAuthenticationFilter的擴充套件
  • CustomUserDetailsService - 宣告loadUserbyUsernameAndDomain方法的自定義介面
  • CustomUserDetailsServiceImpl - CustomUserDetailsService的實現
  • CustomUserDetailsAuthenticationProvider - AbstractUserDetailsAuthenticationProvider的擴充套件
  • CustomAuthenticationToken - UsernamePasswordAuthenticationToken的擴充套件
  • User - Spring Security提供的User類的擴充套件,它宣告瞭我們的額外域欄位
  • SecurityConfig - 我們的Spring Security配置,它將CustomAuthenticationFilter插入到過濾器鏈中,宣告安全規則並連線依賴項
  • login.html - 收集使用者名稱,密碼和域的登入頁面

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來獲取使用者

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.自定義UserDetailsAuthenticationProvider

我們的CustomUserDetailsAuthenticationProvider將AbstractUserDetailsAuthenticationProvider和委託擴充套件到我們的CustomUserDetailService以檢索使用者。這個類最重要的特性是retrieveUser方法的實現。

請注意,我們必須將身份驗證令牌強制轉換為CustomAuthenticationToken才能訪問我們的自定義欄位

@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.總結

我們的第二種方法幾乎與我們首先提出的簡單方法相同。通過實現我們自己的AuthenticationProvider和CustomAuthenticationToken,我們避免了需要使用自定義解析邏輯來調整我們的使用者名稱字段。

5.結論

在本文中,我們在Spring Security中實現了一個使用額外登入欄位的表單登入。我們以兩種不同的方式做到了這一點

  • 在我們簡單的方法中,我們最小化了我們需要編寫的程式碼量。通過使用自定義解析邏輯調整使用者名稱,我們能夠重用DaoAuthenticationProvider和UsernamePasswordAuthentication
  • 在我們更加個性化的方法中,我們通過擴充套件AbstractUserDetailsAuthenticationProvider並使用CustomAuthenticationToken提供我們自己的CustomUserDetailsService來提供自定義欄位支持。
與往常一樣,所有原始碼都可以在GitHub上找到

相關文章