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]で見つけることができます。