Spring Security 2.06でカスタムAuthenticationProviderを実装する。

Spring Securityを使ってStruts2のWebアプリケーションをセキュアにしています。プロジェクトの制約上、Spring Security 2.06を使用しています。

私のチームは、ユーザー名とパスワードのパラメータを入力した後にユーザーを認証し、役割のリストと電子メール、名前などの他の属性を含むカスタムユーザーオブジェクトを返すカスタムユーザー管理APIを構築しました。

私の理解では、典型的なSpring Securityのユースケースは、デフォルトのUserDetailsServiceを使用してUserDetailsオブジェクトを取得します。このオブジェクトには(とりわけ)フレームワークがユーザを認証するために使用するパスワードフィールドが含まれています。

私の場合、カスタムAPIに認証をさせ、ロールとその他の属性(Eメールなど)を含むカスタムUserDetailsオブジェクトを返したいのです。

いくつかの調査の後、私はAuthenticationProviderのカスタム実装によってこれを行うことができることを理解しました。また、UserDetailsServiceとUserDetailsのカスタム実装も持っています。

私の問題は、CustomAuthenticationProviderで何を返せばいいのかがよく分からないことです。ここで私のカスタムUserDetailsServiceオブジェクトを使用するのでしょうか?それは必要ですか?申し訳ありませんが、私は本当に混乱しています。

CustomAuthenticationProviderです。

public class CustomAuthenticationProvider implements AuthenticationProvider {

private Logger logger = Logger.getLogger(CustomAuthenticationProvider.class);

private UserDetailsService userDetailsService; //what am i supposed to do with this?

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
    String username = String.valueOf(auth.getPrincipal());
    String password = String.valueOf(auth.getCredentials());

    logger.info("username:" + username);
    logger.info("password:" + password);
    /* what should happen here? */

    return null;  //what do i return?
}

@Override
public boolean supports(Class aClass) {
    return true;  //To indicate that this authenticationprovider can handle the auth request. since there's currently only one way of logging in, always return true
}

public UserDetailsService getUserDetailsService() {
    return userDetailsService;
}

public void setUserDetailsService(UserDetailsService userDetailsService) {
    this.userDetailsService = userDetailsService;
}

}

applicationContext-security.xml:

<beans:bean id="customUserDetailsService" scope="prototype" class="com.test.testconsole.security.CustomUserDetailsService"/>

<beans:bean id="customAuthenticationProvider" class="com.test.testconsole.security.CustomAuthenticationProvider">
    <custom-authentication-provider />
    <beans:property name="userDetailsService" ref="customUserDetailsService" />
</beans:bean>

要約すると、これが私の必要としているものです。

1.ユーザーがWebフォームからログインする 2.社内ユーザー管理APIでユーザー認証 3.認証に成功したユーザーに対して、GrantedAuthoriesなどを入力する。 4.ロール/権限、および電子メール、名前などの他の属性を含むユーザーエンティティを返します。このオブジェクトに以下のようにアクセスできるようにします。

    //スプリングセキュリティ ユーザー名取得
    Authentication auth = SecurityContextHolder.getContext().getAuthentication().Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    userName = auth.getName(); //ログインしているユーザー名を取得する
    logger.info("username: " + userName);

    //春のセキュリティ ユーザーの役割を取得
    GrantedAuthority[] authorities = auth.getAuthorities();
    userRole = authorities[0].getAuthority()。
    logger.info("user role: " + userRole);

これで理解できればいいのですが。何かお手伝いやご指摘がありましたら、よろしくお願いします。

ありがとうございます。

更新情報

少しは進歩したと思います。

Authenticationインターフェイスを実装したカスタムAuthenticationオブジェクトを用意しました。

public class CustomAuthentication implements Authentication {

    String name;
    GrantedAuthority[] authorities;
    Object credentials;
    Object details;
    Object principal;
    boolean authenticated;

    public CustomAuthentication(String name, GrantedAuthority[] authorities, Object credentials, Object details, Object principal, boolean
                                authenticated){
        this.name=name;
        this.authorities=authorities;
        this.details=details;
        this.principal=principal;
        this.authenticated=authenticated;

    }
    @Override
    public GrantedAuthority[] getAuthorities() {
        return new GrantedAuthority[0];  //To change body of implemented methods use File | Settings | File Templates.
    }

    @Override
    public Object getCredentials() {
        return null;  //To change body of implemented methods use File | Settings | File Templates.
    }

    @Override
    public Object getDetails() {
        return null;  //To change body of implemented methods use File | Settings | File Templates.
    }

    @Override
    public Object getPrincipal() {
        return null;  //To change body of implemented methods use File | Settings | File Templates.
    }

    @Override
    public boolean isAuthenticated() {
        return false;  //To change body of implemented methods use File | Settings | File Templates.
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        //To change body of implemented methods use File | Settings | File Templates.
    }

    @Override
    public String getName() {
        return null;  
    }
}

と、CustomerAuthenticationProviderクラスを更新しました。

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
        String username = String.valueOf(auth.getPrincipal());
        String password = String.valueOf(auth.getCredentials());

        logger.info("username:" + username);
        logger.info("password:" + password);

        //no actual validation done at this time

        GrantedAuthority[] authorities = new GrantedAuthorityImpl[1];
        authorities[0] = new GrantedAuthorityImpl("ROLE_USER");

        CustomAuthentication customAuthentication = new CustomAuthentication("TestMerchant",authorities,"details",username,password,true);

    return customAuthentication;

    //return new UsernamePasswordAuthenticationToken(username,password,authorities); 
}

UsernamePasswordAuthenticationTokenオブジェクトを返せばうまくいくのですが、CustomAuthenticationを返そうとすると、以下のエラーが発生します。

java.lang.ClassCastException: com.test.testconsole.security.CustomAuthentication cannot be cast to org.springframework.security.providers.UsernamePasswordAuthenticationToken
    at com.test.testconsole.security.CustomAuthenticationProvider.authenticate(CustomAuthenticationProvider.java:27)
    at org.springframework.security.providers.ProviderManager.doAuthentication(ProviderManager.java:188)
    at org.springframework.security.AbstractAuthenticationManager.authenticate(AbstractAuthenticationManager.java:46)
    at org.springframework.security.intercept.AbstractSecurityInterceptor.authenticateIfRequired(AbstractSecurityInterceptor.java:319)
    at org.springframework.security.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:258)
    at org.springframework.security.intercept.web.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:106)
    at org.springframework.security.intercept.web.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:83)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
    at org.springframework.security.ui.SessionFixationProtectionFilter.doFilterHttp(SessionFixationProtectionFilter.java:67)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
    at org.springframework.security.ui.ExceptionTranslationFilter.doFilterHttp(ExceptionTranslationFilter.java:101)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
    at org.springframework.security.providers.anonymous.AnonymousProcessingFilter.doFilterHttp(AnonymousProcessingFilter.java:105)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
    at org.springframework.security.ui.rememberme.RememberMeProcessingFilter.doFilterHttp(RememberMeProcessingFilter.java:116)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
    at org.springframework.security.wrapper.SecurityContextHolderAwareRequestFilter.doFilterHttp(SecurityContextHolderAwareRequestFilter.java:91)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
    at org.springframework.security.ui.basicauth.BasicProcessingFilter.doFilterHttp(BasicProcessingFilter.java:174)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
    at org.springframework.security.ui.AbstractProcessingFilter.doFilterHttp(AbstractProcessingFilter.java:278)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
    at org.springframework.security.ui.logout.LogoutFilter.doFilterHttp(LogoutFilter.java:89)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
    at org.springframework.security.context.HttpSessionContextIntegrationFilter.doFilterHttp(HttpSessionContextIntegrationFilter.java:235)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
    at org.springframework.security.util.FilterChainProxy.doFilter(FilterChainProxy.java:175)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:236)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:167)
    at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
    at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:388)
    at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
    at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:182)
    at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:765)
    at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:418)
    at org.mortbay.jetty.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:230)
    at org.mortbay.jetty.handler.HandlerCollection.handle(HandlerCollection.java:114)
    at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)
    at org.mortbay.jetty.Server.handle(Server.java:326)
    at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:536)
    at org.mortbay.jetty.HttpConnection$RequestHandler.headerComplete(HttpConnection.java:915)
    at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:539)
    at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:212)
    at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:405)
    at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:409)
    at org.mortbay.thread.QueuedThreadPool$PoolThread.run(QueuedThreadPool.java:582)

これは、単なる Authentication オブジェクトではなく、UsernamePasswordAuthenticationToken という特定の実装を期待しているようなものです。このことから、私は他のカスタムコンポーネントが足りないのではないかと思っています。

ソリューション

もし、あなたが独自の AuthenticationProvider を実装しているのであれば、 UserDetailsService を実装する必要はないでしょう。UserDetailsService` はユーザー情報を読み込むための標準的な DAO を提供するだけであり、フレームワーク内の他のいくつかのクラスはそれを使用するために実装されています。

通常、ユーザー名とパスワードを使用して認証を行うには、DaoAuthenticationProvider をインスタンス化し、それを UserDetailsService にインジェクトすることになります。それでも、これが最善の方法かもしれません。独自のプロバイダを実装した場合、ユーザーが正しいパスワードを提供したかどうかなどを確認する責任を負います。しかし、場合によっては、この方がシンプルなアプローチになることもあります。

あなたのコードにある「ここで何が起こるべきか」というコメントに対する答えは、次のようなものです。

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
  String username = String.valueOf(auth.getPrincipal());
  String password = String.valueOf(auth.getCredentials());

  logger.info("username:" + username);
  logger.info("password:" + password); // Don't log passwords in real app

  // 1. Use the username to load the data for the user, including authorities and password.
  YourUser user = ....

  // 2. Check the passwords match (should use a hashed password here).
  if (!user.getPassword().equals(password)) {
    throw new BadCredentialsException("Bad Credentials");
  }

  // 3. Preferably clear the password in the user object before storing in authentication object
  user.clearPassword();

  // 4. Return an authenticated token, containing user data and authorities  

  return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()) ;
}

そして、ユーザーオブジェクトは

Authentication.getPrincipal()

メソッドにキャストすることで、追加のプロパティ (メールアドレスなど) にアクセスすることができます。

ユーザーデータをどのようにロードするかは、あなた次第です。Spring Securityがここで気にするのは、 AuthenticationProvider インターフェースだけです。

また、ハッシュ化されたパスワードを保存し、単純な等号チェックではなく、同じアルゴリズムを使用して、供給されたパスワードを検証する必要があります。

解説 (5)

ルークさん、投稿ありがとうございます。

私は、より多くの脳の損傷から救われました。

私が遭遇した唯一の注意点、気になる人のために。

私の設定です。

  • Grails 2.0.4
  • グルーヴィー 1.8
  • スプリングセキュリティーコア 1.2.7.3
  • スプリングセキュリティ-UI 0.2
  • hibernate 2.0.4

Lukeが提案する簡素でエレガントなアプローチを利用する場合、カスタムUserDetails(またはUserDetailsService)オブジェクトを実装せず、特別なものを拡張しない独自のUser ドメインオブジェクトを使用しますが、もしspring securityのカスタムタグ(もちろんあなたのページで) "sec" を使用している場合は、特別な手順を踏む**必要があります。

基本的な、カスタムでないUsernamePasswordAuthenticationTokenをインスタンス化するとき、Principalを拡張する何かのインスタンスを渡さなければなりません、再度、春のセキュリティのカスタムギャップタグを動作させたい場合。 私は、できるだけシンプルにするために、このようなことをしました(有用/適切な場所でユーザードメインオブジェクトの値を参照します)。

def principalUser = new org.springframework.security.core.userdetails.User(user.username, user.password, user.enabled, !user.accountExpired, !user.passwordExpired,!user.accountLocked, authorities)
def token = new UsernamePasswordAuthenticationToken(principalUser, presentedPassword, authorities)

これはgrails.plugins.springsecurity.SecurityTagLib.determineSource()でテストされる条件を満たすはずなので、 `` を使用するページは実際にレンダリングされます。

if (principal.metaClass.respondsTo(principal, 'getDomainClass')) {
            return principal.domainClass
}

そうでなければ、UsernamePasswordAuthenticationTokenをUserドメインオブジェクトでインスタンス化した場合(Lukeの例のように)、セキュリティタグのlibメソッド(determineSource())は最善を尽くし、org.codehaus.groovy.grails.commons.DefaultGrailsDomainClassという(meta)値を返し、タグがユーザ名のメンバー変数を見つけて、次のように述べるエラーを得ることでしょう。

 Error executing tag : Error executing tag : No such property: username for class: org.codehaus.groovy.grails.commons.DefaultGrailsDomainClass

私のgrailsプロジェクトでspring-security-core plugin taglibsを再実装/サブクラス化しない限り、タグリブを使用し、カスタムドメインのUserクラスを使用してフィルタからプロバイダに渡されるトークンをインスタンス化する方法はない。

とはいえ、1行の追加コードはとても小さな代償です :)

解説 (0)