web-dev-qa-db-ja.com

Spring Security LDAPと記憶する

LDAPと統合されたSpring Bootを使用してアプリを構築しています。 LDAPサーバーに正常に接続し、ユーザーを認証することができました。今、remember-me機能を追加する必要があります。別の投稿( this )を調べてみましたが、私の問題に対する答えを見つけることができませんでした。公式の春のセキュリティ ドキュメント は、

UserDetailsS​​erviceを使用しない認証プロバイダー(LDAPプロバイダーなど)を使用している場合は、アプリケーションコンテキストにUserDetailsS​​ervice Beanがない限り、機能しません。

ここに、remember-me機能を追加するためのいくつかの最初の考えを含む私の作業コード:

WebSecurityConfig

import com.ui.security.CustomUserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.event.LoggerListener;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
import org.springframework.security.ldap.userdetails.UserDetailsContextMapper;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    String DOMAIN = "ldap-server.com";
    String URL = "ldap://ds.ldap-server.com:389";


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/ui/**").authenticated()
                .antMatchers("/", "/home", "/UIDL/**", "/ui/**").permitAll()
                .anyRequest().authenticated()
        ;
        http
                .formLogin()
                .loginPage("/login").failureUrl("/login?error=true").permitAll()
                .and().logout().permitAll()
        ;

        // Not sure how to implement this
        http.rememberMe().rememberMeServices(rememberMeServices()).key("password");

    }

    @Override
    protected void configure(AuthenticationManagerBuilder authManagerBuilder) throws Exception {

        authManagerBuilder
                .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
                .userDetailsService(userDetailsService())
        ;
    }

    @Bean
    public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {

        ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(DOMAIN, URL);
        provider.setConvertSubErrorCodesToExceptions(true);
        provider.setUseAuthenticationRequestCredentials(true);
        provider.setUserDetailsContextMapper(userDetailsContextMapper());
        return provider;
    }

    @Bean
    public UserDetailsContextMapper userDetailsContextMapper() {
        UserDetailsContextMapper contextMapper = new CustomUserDetailsServiceImpl();
        return contextMapper;
    }

    /**
     * Impl of remember me service
     * @return
     */
    @Bean
    public RememberMeServices rememberMeServices() {
//        TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("password", userService);
//        rememberMeServices.setCookieName("cookieName");
//        rememberMeServices.setParameter("rememberMe");
        return rememberMeServices;
    }

    @Bean
    public LoggerListener loggerListener() {
        return new LoggerListener();
    }
}

CustomUserDetailsS​​erviceImpl

public class CustomUserDetailsServiceImpl implements UserDetailsContextMapper {

    @Autowired
    SecurityHelper securityHelper;
    Log ___log = LogFactory.getLog(this.getClass());

    @Override
    public LoggedInUserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<? extends GrantedAuthority> grantedAuthorities) {

        LoggedInUserDetails userDetails = null;
        try {
            userDetails = securityHelper.authenticateUser(ctx, username, grantedAuthorities);
        } catch (NamingException e) {
            e.printStackTrace();
        }

        return userDetails;
    }

    @Override
    public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {

    }
}

どういうわけかUserServiceを実装する必要があることはわかっていますが、それを実現する方法はわかりません。

29
Maksim

LDAPでのRememberMe機能の構成には、2つの問題があります。

  • 正しいRememberMe実装の選択(トークンとPersistentTokensの比較)
  • springのJava構成を使用した構成

私はこれらを一歩ずつやっていきます。

トークンベースの記憶機能(TokenBasedRememberMeServices)は、認証中に次のように機能します。

  • ユーザーが認証され(再度AD)、現在ユーザーのIDとパスワードがわかっている
  • 値username + expirationTime + password + staticKeyを作成し、そのMD5ハッシュを作成します
  • ユーザー名+有効期限+計算されたハッシュを含むCookieを作成します

ユーザーがサービスに戻って、remember me機能を使用して認証されることを望む場合、次のようにします。

  • cookieが存在し、期限が切れていないかどうかを確認します
  • cookieからユーザーIDを入力し、ユーザーのIDに関連する情報を返すことが予想される提供されたUserDetailsS​​erviceを呼び出しますパスワードを含む
  • 次に、返されたデータからハッシュを計算し、Cookieのハッシュが計算した値と一致することを確認します
  • 一致する場合は、ユーザーの認証オブジェクトを返します

ハッシュチェックプロセスは、誰も「偽の」覚えているcookieを作成できないようにするために必要です。これにより、他のユーザーになりすますことができます。問題は、このプロセスがリポジトリからパスワードをロードする可能性に依存していることです。ただし、これはActive Directoryでは不可能です。ユーザー名に基づいてプレーンテキストのパスワードをロードすることはできません。

これにより、トークンベースの実装がADでの使用に不適切になります(パスワードまたはその他の秘密のユーザーベースの資格情報を含むローカルユーザーストアの作成を開始しない限り、私は他の詳細がわからないため、このアプローチを提案しません。あなたのアプリケーション、それは行くのに良い方法かもしれませんが)。

もう1つは、実装が永続的なトークン(PersistentTokenBasedRememberMeServices)に基づいていることを思い出してください。

  • ユーザーが認証すると、ランダムなトークンが生成されます
  • 関連付けられたユーザーのIDに関する情報とともにトークンをストレージに保存します
  • トークンIDを含むCookieを作成します

ユーザーが認証を希望する場合:

  • トークンIDを含むCookieが利用可能かどうかを確認します
  • トークンIDがデータベースに存在するかどうかを確認する
  • データベースの情報に基づいてユーザーのデータをロードする

ご覧のとおり、パスワードは不要になりましたが、パスワード検証の代わりに使用されるトークンストレージ(通常はデータベース、テストにはインメモリを使用できます)が必要になりました。

これで、構成部分に進みます。永続的なトークンベースの基本的な構成は、次のようになっています。

@Override
protected void configure(HttpSecurity http) throws Exception {           
    ....
    String internalSecretKey = "internalSecretKey";
    http.rememberMe().rememberMeServices(rememberMeServices(internalSecretKey)).key(internalSecretKey);
}

 @Bean
 public RememberMeServices rememberMeServices(String internalSecretKey) {
     BasicRememberMeUserDetailsService rememberMeUserDetailsService = new BasicRememberMeUserDetailsService();
     InMemoryTokenRepositoryImpl rememberMeTokenRepository = new InMemoryTokenRepositoryImpl();
     PersistentTokenBasedRememberMeServices services = new PersistentTokenBasedRememberMeServices(staticKey, rememberMeUserDetailsService, rememberMeTokenRepository);
     services.setAlwaysRemember(true);
     return services;
 }

この実装は、本番環境ではJdbcTokenRepositoryImplに置き換える必要があるメモリ内トークンストレージを使用します。提供されたUserDetailsServiceは、remember me cookieからロードされたユーザーIDによって識別されるユーザーの追加データのロードを担当します。最も単純な実装は次のようになります。

public class BasicRememberMeUserDetailsService implements UserDetailsService {
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         return new User(username, "", Collections.<GrantedAuthority>emptyList());
     }
}

必要に応じて、ADまたは内部データベースから追​​加の属性またはグループメンバーシップをロードする別のUserDetailsService実装を提供することもできます。次のようになります。

@Bean
public RememberMeServices rememberMeServices(String internalSecretKey) {
    LdapContextSource ldapContext = getLdapContext();

    String searchBase = "OU=Users,DC=test,DC=company,DC=com";
    String searchFilter = "(&(objectClass=user)(sAMAccountName={0}))";
    FilterBasedLdapUserSearch search = new FilterBasedLdapUserSearch(searchBase, searchFilter, ldapContext);
    search.setSearchSubtree(true);

    LdapUserDetailsService rememberMeUserDetailsService = new LdapUserDetailsService(search);
    rememberMeUserDetailsService.setUserDetailsMapper(new CustomUserDetailsServiceImpl());

    InMemoryTokenRepositoryImpl rememberMeTokenRepository = new InMemoryTokenRepositoryImpl();

    PersistentTokenBasedRememberMeServices services = new PersistentTokenBasedRememberMeServices(internalSecretKey, rememberMeUserDetailsService, rememberMeTokenRepository);
    services.setAlwaysRemember(true);
    return services;
}

@Bean
public LdapContextSource getLdapContext() {
    LdapContextSource source = new LdapContextSource();
    source.setUserDn("user@"+DOMAIN);
    source.setPassword("password");
    source.setUrl(URL);
    return source;
}

これにより、LDAPで機能し、SecurityContextHolder.getContext().getAuthentication()で利用できるRememberMeAuthenticationToken内にロードされたデータを提供する機能を覚えていただけます。また、LDAPデータを解析してユーザーオブジェクト(CustomUserDetailsServiceImpl)にするための既存のロジックを再利用することもできます。

別の主題として、質問に投稿されたコードにも1つの問題があります。

    authManagerBuilder
            .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
            .userDetailsService(userDetailsService())
    ;

と:

    authManagerBuilder
            .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
    ;

UserDetailsS​​erviceの呼び出しは、DAOベースの認証(データベースなど)を追加するためにのみ行う必要があり、ユーザー詳細サービスの実際の実装で呼び出す必要があります。現在の構成では、無限ループが発生する可能性があります。

29

UserServiceへの参照が必要なRememberMeServiceのインスタンスがないようです。 LDAPを使用しているため、LDAPバージョンのUserServiceが必要になります。私はJDBC/JPA実装に精通しているだけですが、org.springframework.security.ldap.userdetails.LdapUserDetailsManagerがあなたが探しているもののようです。次に、設定は次のようになります。

@Bean
public UserDetailsService getUserDetailsService() {
    return new LdapUserDetailsManager(); // TODO give it whatever constructor params it needs
}

@Bean
public RememberMeServices rememberMeServices() {
    TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("password", getUserDetailsService());
    rememberMeServices.setCookieName("cookieName");
    rememberMeServices.setParameter("rememberMe");
    return rememberMeServices;
}
0
SergeyB