web-dev-qa-db-ja.com

Java configを使用した単一のアプリでの複数の認証メカニズム

現在、アプリケーションには認証と承認にLDAPを使用する単一の認証メカニズムがあります。私のセキュリティ構成は次のようになります

@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .csrf().disable()
        .authorizeRequests()
            .anyRequest().fullyAuthenticated()
            .and()
            .httpBasic();
}

@Configuration
protected static class AuthenticationConfiguration extends GlobalAuthenticationConfigurerAdapter {

    @Value("${ldap-${env}.manager.dn}")
    private String managerDn;

    @Value("${ldap-${env}.manager.pass}")
    private String managerPass;

    @Value("${ldap-${env}.server.url}")
    private String url;

    @Value("${ldap.password.attribute:userPassword}")
    private String passwordAttr;

    @Override
    public void init(AuthenticationManagerBuilder auth) throws Exception {
        auth.ldapAuthentication().userDnPatterns("uid={0},ou=people").groupSearchBase("ou=groups")
                .groupSearchFilter("(member={0})").userSearchBase("ou=people").userSearchFilter("(uid={0})")
                .userDetailsContextMapper(new CustomLdapPersonContextMapper())
                // .passwordCompare()
                // .passwordAttribute(passwordAttr)
                // .passwordEncoder(new PlaintextPasswordEncoder())
                // .and()
                .contextSource().managerDn(managerDn).managerPassword(managerPass).url(url);
    }
}
}

ただし、ユーザーがセッションキーサーバーから認証可能なセッショントークンを受け取り、有効なトークンがユーザー名を返し、そのユーザーの認証情報をLDAPから読み込むために使用できる状況があります。したがって、セッショントークンがhttpヘッダーに存在する場合はトークン認証を実行し、次にLDAPルックアップを実行する必要があり、セッショントークンが存在しない場合は現在の認証メカニズムに該当するという2番目の認証メカニズムが最初に発生します。この2番目の認証レイヤーを追加するにはどうすればよいですか。

22
adeelmahmood

純粋なJava構成。これを機能させるにはいくつかのステップがあります。これらのラインに沿ったものでなければなりません。基本的なプロセス以下のとおりであります:

  • 特定の承認情報の要求を確認するカスタムフィルターを作成する

  • 各フィルターは、null(そのタイプの許可が見つからない場合)またはカスタムAbstractAuthenticationTokenを返します

  • フィルターがトークンを返す場合、各AuthenticationProviderのsupports(class)メソッドが呼び出され、そのトークンが認証を試みる必要がある場合にtrue | falseを返します

  • その後、トークンをサポートするAuthenticationProviderでattemptAuthenticationが呼び出されます。ここでは、ユーザーを認証するためのサービス呼び出しを行います。その後、LoginExceptionをスローするか、authentication.setAuthenticated(true)を呼び出して、認証が成功した場合にトークンを返します。

私はしばらくの間、さまざまな認証方法(署名付きリクエスト、ユーザー名/パスワード、oauthなど)をサポートするこのセットアップを使用していました。

AuthenticationSuccessHandlerとAuthenticationFailuersHandlerをカスタムセキュリティフィルターに渡して、カスタムリダイレクト戦略と障害処理を提供することもできます。

また、フィルターのコンストラクターでAntマッチャーをセットアップして、フィルターが適用するURLパターンを制御することも忘れないでください。たとえば、LDAPリクエストフィルタは、おそらくリクエスト "/ *"で確認する必要がありますが、ユーザー名/パスワードフィルタは、POSTで/ loginなどに確認することができます。

サンプルコード:

1)サポートする認証の種類ごとにカスタムAuthenticationTokenを作成します

public class LDAPAuthorizationToken extends AbstractAuthenticationToken {
    private String token;

    public LDAPAuthorizationToken( String token ) {
        super( null );
        this.token = token;
    }

    public Object getCredentials() {
        return token;
    }

    public Object getPrincipal() {
        return null;
    }
}

public class OTPAuthorizationToken extends UsernamePasswordAuthenticationToken {
    private String otp;

    public OTPAuthorizationToken( String username, String password, String otp ) {
        super( username, password );
        this.otp = otp;
    }

    public String getOTP() {
        return otp;
    }
}

2)タイプごとにカスタムセキュリティフィルターを作成する

public class LDAPAuthorizationFilter extends AbstractAuthenticationProcessingFilter {
    @Autowired
    private UserDetailsService userDetailsService;

    public LDAPAuthorizationFilter() {
        super( "/*" ); // allow any request to contain an authorization header
    }

    public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response ) throws AuthenticationException
    {

        if ( request.getHeader( "Authorization" ) == null ) {
            return null; // no header found, continue on to other security filters
        }

        // return a new authentication token to be processed by the authentication provider
        return new LDAPAuthorizationToken( request.getHeader( "Authorization" ) );
    }
}

public class OTPAuthorizationFilter extends AbstractAuthenticationProcessingFilter {
    @Autowired
    private UserDetailsService userDetailsService;

    public OTPAuthorizationFilter() {
        super( "/otp_login" );
    }

    public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response ) throws AuthenticationException
    {

        if ( request.getParameter( "username" ) == null || request.getParameter( "password" ) == null || request.getParameter( "otp" ) == null ) {
            return null;
        }

        // return a new authentication token to be processed by the authentication provider
        return new OTPAuthorizationToken( request.getParameter( "username" ), request.getParameter( "password" ), request.getParameter( "otp" ) );
    }
}

3)カスタムAuthenticationProviderを作成する

public class LDAPAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private MyAuthenticationService sampleService;

    @Override
    public Authentication authenticate( Authentication authentication ) throws AuthenticationException {
        LDAPAuthorizationToken auth = (LDAPAuthorizationToken)authentication;

        String username = sampleService.verifyToken( auth.getCredentials() );
        if ( username == null ) {
            throw new LoginException( "Invalid Token" );
        }

        auth.setAuthenticated( true );

        return auth;
    }

    @Override
    public boolean supports( Class<?> authentication ) {
        if ( authentication.isAssignableFrom( LDAPAuthorizationToken.class ) ) {
            return true;
        }
        return false;
    }
}

public class OTPAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private MyAuthenticationService sampleService;

    @Override
    public Authentication authenticate( Authentication authentication ) throws AuthenticationException {
        OTPAuthorizationToken auth = (OTPAuthorizationToken)authentication;

        String error = sampleService.loginWithOTP( auth.getPrincipal(), auth.getCredentials(), auth.getOTP() );
        if ( error != null ) {
            throw new LoginException( error );
        }

        auth.setAuthenticated( true );

        return auth;
    }

    @Override
    public boolean supports( Class<?> authentication ) {
        if ( authentication.isAssignableFrom( OTPAuthorizationToken.class ) ) {
            return true;
        }
        return false;
    }
}

4)スプリングセキュリティを構成する

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure( HttpSecurity http ) throws Exception {
        // configure filters
        http.addFilterBefore( new LDAPAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class );
        http.addFilterBefore( new OTPAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class );

        // configure authentication providers
        http.authenticationProvider( new LDAPAuthenticationProvider() );
        http.authenticationProvider( new OTPAuthenticationProvider() );

        // disable csrf
        http.csrf().disable();

        // setup security
        http.authorizeRequests()
            .anyRequest()
                .fullyAuthenticated()
                .and().httpBasic();
    }
}

お役に立てば幸いです!

48
Matt MacLean

2番目の認証プロバイダーを追加する別のオプション:AuthenticationManagerBuilderで別のプロバイダーを指定するだけです。なぜなら @EnableWebSecurityアノテーション自体にEnableGlobalAuthenticationアノテーションが付けられます。AuthenticationManagerBuilderのグローバルインスタンスを設定できます。 (詳細については javadocs を参照してください。)

たとえば、ここにはLDAP認証プロバイダーとメモリ内(ハードコードされた)認証プロバイダーがあります(これは開発中にローカルユーザーにテストを行うために行っていることです)。

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {

      @Value("${user.role}")
      private String userRole; // i.e. ROLE_APP_USER

      @Value("${include.test.users}")
      private boolean includeTestUsers;

      @Override
      protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
          .antMatchers("/**/js/**").permitAll()
          .antMatchers("/**/images/**").permitAll()
          .antMatchers("/**/favicon.ico").permitAll()
          .antMatchers("/**/css/**").permitAll()
          .antMatchers("/**/fonts/**").permitAll()
          .antMatchers("/**").hasAnyRole(userRole)
          .and().formLogin().loginPage("/login").permitAll().and().logout().permitAll();

        http.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
      }

      @Autowired
      public void configureGlobal(AuthenticationManagerBuilder auth, LdapContextSource contextSource) throws Exception {
        auth.ldapAuthentication()
          .userSearchBase("OU=Users OU")
          .userSearchFilter("sAMAccountName={0}")
          .groupSearchBase("OU=Groups OU")
          .groupSearchFilter("member={0}")
          .contextSource(contextSource);

        if (includeTestUsers) {
          auth.inMemoryAuthentication().withUser("user").password("u").authorities(userRole);
        }
      }
    }
12
Brice Roncace

私はmclemaの答えに加えたいだけです。認証を成功させるにはオーバーライドを追加してフィルターチェーンを続行する必要があります。そうしないと、ユーザーは元のURL(例:/ myrest/server/somemethod)ではなくデフォルトのURL( "/")にリダイレクトされます。

@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
        Authentication authResult) throws IOException, ServletException {
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    context.setAuthentication(authResult);
    SecurityContextHolder.setContext(context);
    chain.doFilter(request, response);
}
1
Mir3

受け入れられた答えには、現在のリクエストが許可されていないという問題があります。次のリクエストに対してのみセッションが確立されます!したがって、ポイント2で構成する必要がありました

public class MyAuthorizationFilter extends AbstractAuthenticationProcessingFilter {

    public MyAuthorizationFilter() {
        super( "/*" ); // allow any request to contain an authorization header
    }

    public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response ) throws AuthenticationException
    {

        if ( request.getHeader( "Authorization" ) == null ) {
            return null; // no header found, continue on to other security filters
        }

        // required to use the token 
        myNewToken = new MyAuthorizationToken( request.getHeader( "Authorization" ) );
        // and set in the current context ==> the current request is as well authorized
        SecurityContextHolder.getContext().setAuthentication(myNewToken);
        // return a new authentication token to be processed by the authentication provider
        return myNewToken;
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        // try to authenticate the current request
        attemptAuthentication((HttpServletRequest) req, (HttpServletResponse) res);
        super.doFilter(req, res, chain);
    }
}

それ以外セッションはすでに作成されていますが、現在のリクエストはまだ認証されていません!!! (そして、私が必要としないプロバイダー、つまりフィルターを追加するだけで十分です。)

0
LeO