Spring Securityの追加ログインフィールド

1前書き

この記事では、https://projects.spring.io/spring-security/[Spring Security]を使用したカスタム認証シナリオを、標準のログインフォームに追加のフィールドを追加して実装します。

フレームワークの多様性とそれを使用できる柔軟な方法を示すために、 2つの異なるアプローチ に焦点を当てます。

  • 私たちの最初のアプローチ** は、既存の中核となるSpring Securityの実装の再利用に焦点を当てた簡単な解決策です。

  • 私たちの2番目のアプローチは** 高度なユースケースにより適しているかもしれない、よりカスタムな解決策になるでしょう。

/spring-security-login[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>

Spring Bootセキュリティスターターの最新版はhttps://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22org.springframework.boot%22%20AND%20a%3A%22springにあります-boot-starter-security%22[Maven Centralで終わりました]。

3簡単なプロジェクト設定

最初のアプローチでは、Spring Securityが提供する実装の再利用に焦点を当てます。特に、 DaoAuthenticationProvider UsernamePasswordToken は、そのまま使用できるので再利用します。

主なコンポーネントは次のとおりです。

  • SimpleAuthenticationFilter - の拡張子

UsernamePasswordAuthenticationFilter SimpleUserDetailsS​​ervice - ** の実装

UserDetailsS​​ervice User - ** Springが提供する User クラスの拡張

追加の domain フィールドを宣言するセキュリティ SecurityConfig - ** 挿入するSpring Securityの設定

SimpleAuthenticationFilter をフィルタチェーンに追加し、宣言します。 セキュリティルールと依存関係の配線 login.html - ** ユーザー名 を収集するログインページ

password 、および domain

3.1. 簡易認証フィルタ

私たちの SimpleAuthenticationFilter では、 domainとusernameフィールドはリクエストから抽出されました 。これらの値を連結し、それらを使用して 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. 簡易 詳細サービス サービス

UserDetailsS​​ervice コントラクトは、 loadUserByUsername. という単一のメソッドを定義しています。** この実装では、 username と__domainを抽出しています。

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のセキュリティ設定

私たちの設定は標準のSpring Security設定とは異なります。なぜなら、私たちはデフォルトの前に 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");
}

SimpleUserDetailsS​​ervice を使用して設定しているので、提供された DaoAuthenticationProvider を使用できます。 SimpleUserDetailsS​​ervice username フィールドと domain フィールドを解析する方法を知っていて、認証時に使用する適切な 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());
    return filter;
}

3.4. ログインページ

使用するログインページは、 SimpleAuthenticationFilterによって抽出された追加の domain__フィールドを収集します。

<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. 概要

最初の例では、usernameフィールドを「偽造」することで、 DaoAuthenticationProvider UsernamePasswordAuthenticationToken を再利用できました。

その結果、最小限の設定と追加のコードで** 追加のログインフィールドのサポートを追加することができました。

4カスタムプロジェクト設定

私たちの2番目のアプローチは最初のものと非常に似ているでしょうが、自明でないユースケースのためにより適切であるかもしれません。

私たちの2番目のアプローチの主な要素は次のとおりです。

  • CustomAuthenticationFilter - の拡張子

UsernamePasswordAuthenticationFilter CustomUserDetailsS​​ervice - ** を宣言するカスタムインタフェース

loadUserbyUsernameAndDomain メソッド CustomUserDetailsS​​erviceImpl - ** 私たちの実装

CustomUserDetailsS​​ervice CustomUserDetailsAuthenticationProvider - ** の拡張子

AbstractUserDetailsAuthenticationProvider CustomAuthenticationToken - ** の拡張子

UsernamePasswordAuthenticationToken User - ** Springが提供する User クラスの拡張

追加の domain フィールドを宣言するセキュリティ SecurityConfig - ** 挿入するSpring Securityの設定

CustomAuthenticationFilter をフィルタチェーンに追加し、宣言します。 セキュリティルールと依存関係の配線 login.html - ** ユーザー名 を収集するログインページ

password 、および domain

4.1. カスタム認証フィルタ

CustomAuthenticationFilter では、リクエストからユーザー名、パスワード、ドメインの各フィールドを抽出します。これらの値は、認証のために AuthenticationProvider に渡されるCustom __AuthenticationToken __のインスタンスを作成するために使用されます。

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. カスタム ユーザー詳細 サービス

CustomUserDetailsS​​ervice コントラクトでは、 loadUserByUsernameAndDomain. という名前の単一のメソッドを定義しています。

作成した CustomUserDetailsS​​erviceImpl クラスは単純にコントラクトを実装し、 User を取得するために 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 に委任して User を取得します。 このクラスの最も重要な機能は 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. 概要

私たちの2番目のアプローチは、最初に紹介した単純なアプローチとほぼ同じです。独自の AuthenticationProvider および CustomAuthenticationToken を実装することで、ユーザー名フィールドをカスタム解析ロジックで調整する必要がなくなりました。

5結論

この記事では、Spring Securityに追加のログインフィールドを利用したフォームログインを実装しました。これを2つの方法で行いました。

  • 簡単な方法では、必要なコード量を最小限に抑えました。

書きます。 DaoAuthenticationProvider を再利用することができました。 ユーザー名 をカスタムに適応させたUsernamePasswordAuthentication 解析ロジック 私達のよりカスタマイズされたアプローチでは、私達はによってカスタムフィールドサポートを提供しました

  • AbstractUserDetailsAuthenticationProviderを拡張し、 CustomAuthenticationToken を使用して独自の CustomUserDetailsS​​ervice を提供する**

いつものように、すべてのソースコードはhttps://github.com/eugenp/tutorials/tree/master/spring-5-security[over GitHub]で見つけることができます。