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

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

1. 前書き

この記事では、Spring Security xadding an extra field to the standard login formを使用したカスタム認証シナリオを実装します。

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

Our first approachは、既存のコアSpringSecurity実装の再利用に焦点を当てたシンプルなソリューションになります。

Our second approachは、よりカスタムなソリューションであり、高度なユースケースにより適している可能性があります。

previous articles on Spring Security loginで説明されている概念に基づいて構築します。

2. Mavenセットアップ

Spring Bootスターターを使用してプロジェクトをブートストラップし、必要なすべての依存関係を取り込みます。

使用するセットアップには、親宣言、Webスターター、およびセキュリティスターターが必要です。 thymeleafも含まれます:


    org.springframework.boot
    spring-boot-starter-parent
    2.0.0.M7
    



    
        org.springframework.boot
        spring-boot-starter-web
    
    
        org.springframework.boot
        spring-boot-starter-security
    
    
        org.springframework.boot
        spring-boot-starter-thymeleaf
     
     
        org.thymeleaf.extras
        thymeleaf-extras-springsecurity4
    

Spring Bootセキュリティスターターの最新バージョンはover at Maven Centralにあります。

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

最初のアプローチでは、SpringSecurityが提供する実装の再利用に焦点を当てます。 特に、DaoAuthenticationProviderUsernamePasswordTokenは「すぐに使用できる」状態で存在するため、再利用します。

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

  • SimpleAuthenticationFilterUsernamePasswordAuthenticationFilterの拡張です

  • SimpleUserDetailsServiceUserDetailsServiceの実装

  • Userは、Spring Securityによって提供されるUserクラスの拡張であり、追加のdomainフィールドを宣言します。

  • SecurityConfigは、SimpleAuthenticationFilterをフィルターチェーンに挿入し、セキュリティルールを宣言し、依存関係を結び付けるSpringSecurity構成です。

  • login.htmlは、usernamepassword、およびdomainを収集するログインページです。

3.1. シンプルな認証フィルター

SimpleAuthenticationFilterでは、the domain and username fields are extracted from the request。 これらの値を連結し、それらを使用してUsernamePasswordAuthenticationTokenのインスタンスを作成します。

認証用のThe token is then passed along to the 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.Our implementation extracts the username and domain.と呼ばれる単一のメソッドを定義します。次に、値が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の構成

addFilterBeforeを呼び出すwe insert our SimpleAuthenticationFilter into the filter chain before the defaultのため、セットアップは標準のSpringSecurity構成とは異なります。

@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");
}

SimpleUserDetailsServiceで構成しているため、提供されているDaoAuthenticationProviderを使用できます。 our SimpleUserDetailsService knows how to parse out our username and domain fieldsを思い出して、認証時に使用する適切な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フィールドを収集します

アプリケーションを実行してhttp://localhost:8081のコンテキストにアクセスすると、保護されたページにアクセスするためのリンクが表示されます。 リンクをクリックすると、ログインページが表示されます。 予想どおり、we see the additional domain field

image

3.5. 概要

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

その結果、add support for an extra login field with a minimal amount of configuration and additional codeを実行できました。

4. カスタムプロジェクトのセットアップ

2番目のアプローチは最初のアプローチに非常に似ていますが、重要なユースケースにより適している場合があります。

2番目のアプローチの主要なコンポーネントは次のとおりです。

  • CustomAuthenticationFilterUsernamePasswordAuthenticationFilterの拡張です

  • CustomUserDetailsServiceloadUserbyUsernameAndDomainメソッドを宣言するカスタムインターフェイス

  • CustomUserDetailsServiceImplCustomUserDetailsServiceの実装です

  • CustomUserDetailsAuthenticationProviderAbstractUserDetailsAuthenticationProviderの拡張です

  • CustomAuthenticationTokenUsernamePasswordAuthenticationTokenの拡張です

  • Userは、Spring Securityによって提供されるUserクラスの拡張であり、追加のdomainフィールドを宣言します。

  • SecurityConfigは、CustomAuthenticationFilterをフィルターチェーンに挿入し、セキュリティルールを宣言し、依存関係を結び付けるSpringSecurity構成です。

  • login.htmlusernamepassword、およびdomainを収集するログインページ

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

CustomAuthenticationFilterでは、extract the username, password, and domain fields from the requestです。 これらの値は、認証のためにAuthenticationProviderに渡されるCustomAuthenticationTokenのインスタンスを作成するために使用されます。

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. カスタムUserDetailsAuthenticationProvider

CustomUserDetailsAuthenticationProviderAbstractUserDetailsAuthenticationProviderを拡張し、CustomUserDetailServiceに委任してUserを取得します。 The most important feature of this class is the implementation of the retrieveUser method

カスタムフィールドにアクセスするには、認証トークンを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番目のアプローチは、最初に示した単純なアプローチとほぼ同じです。 独自のAuthenticationProviderCustomAuthenticationTokenを実装することで、ユーザー名フィールドをカスタム解析ロジックに適合させる必要がなくなりました。

5. 結論

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

  • 単純なアプローチでは、記述する必要のあるコードの量を最小限に抑えました。 カスタム解析ロジックでreuse DaoAuthenticationProvider and UsernamePasswordAuthentication by adapting the usernameを実行できました

  • よりカスタマイズされたアプローチでは、extending AbstractUserDetailsAuthenticationProvider and providing our own CustomUserDetailsService with a CustomAuthenticationTokenによるカスタムフィールドサポートを提供しました

いつものように、すべてのソースコードはover on GitHubで見つけることができます。