在 Spring Security 2.06 中实现自定义 AuthenticationProvider

Posted

技术标签:

【中文标题】在 Spring Security 2.06 中实现自定义 AuthenticationProvider【英文标题】:Implement custom AuthenticationProvider in Spring Security 2.06 【发布时间】:2012-01-28 19:21:20 【问题描述】:

我正在使用 Spring Security 来保护 Struts2 Web 应用程序。由于项目限制,我使用的是 Spring Security 2.06。

我的团队构建了一个自定义用户管理 API,该 API 在接受用户名和密码参数后对用户进行身份验证,并返回一个自定义用户对象,其中包含角色列表和电子邮件、姓名等其他属性。

据我了解,典型的 Spring Security 用例使用默认的 UserDetailsS​​ervice 来检索 UserDetails 对象;该对象将包含(除其他外)一个密码字段,框架将使用该字段对用户进行身份验证。

在我的例子中,我想让我们的自定义 API 进行身份验证,然后返回一个包含角色和其他属性(电子邮件等)的自定义 UserDetails 对象。

经过一番研究,我发现我可以通过 AuthenticationProvider 的自定义实现来做到这一点。我也有 UserDetailsS​​ervice 和 UserDetails 的自定义实现。

我的问题是我真的不明白我应该在 CustomAuthenticationProvider 中返回什么。我是否在这里使用我的自定义 UserDetailsS​​ervice 对象?那还需要吗?对不起,我真的很困惑。

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>

总而言之,这就是我需要的:

    用户通过网络表单登录 使用内部用户管理 API 对用户进行身份验证 对于成功通过身份验证的用户,填充 GrantedAuthories 等。

    返回一个包含角色/权限的用户实体,以及电子邮件、姓名等其他属性。然后我应该能够像这样访问这个对象..

    //spring security get user name
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    userName = auth.getName(); //get logged in username
    logger.info("username: " + userName);
    
    //spring security get user role
    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。这让我觉得我可能缺少另一个自定义组件......也许是一个过滤器?

【问题讨论】:

【参考方案1】:

如果您要实现自己的AuthenticationProvider,如果您不想实现,则不必实现UserDetailsServiceUserDetailsService 只是提供了一个标准的 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 接口。

您还应该存储散列密码并使用相同的算法验证提供的密码,而不是简单的相等性检查。

【讨论】:

嗨卢克,感谢您的回复!在发布问题后,我实际上取得了一些进展,并且我所做的看起来与您的建议相似。我仍然有无法向身份验证对象添加额外字段(如(电子邮件等))的问题。我实现了一个自定义身份验证类,但出现错误。请看我的更新.. 看起来您在 CustomAuthenticationProvider 中进行了无效转换,第 27 行到 UsernamePasswordAuthenticationToken。通常不需要创建自定义 Authentication 对象。 哦..我明白了。你的意思是我应该传入包含我需要的所有额外字段的自定义用户对象,而不是将用户名作为主体传递给 UsernamePasswordAuthenticationToken 构造函数?那么当我需要数据时,我会在检索到 Authentication 对象后调用 getPrincipal 吗? 关于无效的转换..从我的代码中可以看出,我根本没有进行任何转换.. 嗯。那么 ClassCastException 正在您的代码中发生,因此您必须尝试将您的对象分配给某个地方的 UsernamePasswordAuthenticationToken 。【参考方案2】:

感谢您发布此卢克!

让我免于更多脑损伤。

我遇到的唯一值得注意的事情,对于任何关心的人:

我的设置:

Grails 2.0.4 Groovy 1.8 spring-security-core 1.2.7.3 spring-security-ui 0.2 休眠 2.0.4

当使用卢克建议的非常感谢的简化/优雅方法时,不要实现自定义 UserDetails(或 UserDetailsS​​ervice)对象 - 并且 - 使用您自己的 User domain 对象没有扩展任何特殊的东西,如果您使用来自 spring security 的“sec”自定义标签(当然是在您的页面中),您必须采取额外的步骤:

当你实例化一个基本的、非自定义的 UsernamePasswordAuthenticationToken 时,如果你希望你的 spring security 自定义间隙标签起作用,你必须再次向它传递一个扩展 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() 中测试的条件,因此,您知道,您使用 &lt;sec:loggedInUserInfo&gt; 的页面将实际呈现:

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

否则,如果您使用您的用户域对象(如 Luke 在他的示例中所示)实例化 UsernamePasswordAuthenticationToken,则该安全标签库方法(determineSource())将尽其所能并返回 org.codehaus.groovy.grails.commons.DefaultGrailsDomainClass 当标签去寻找用户名成员变量时你会得到一个错误:

 Error executing tag <sec:ifLoggedIn>: Error executing tag <sec:loggedInUserInfo>: No such property: username for class: org.codehaus.groovy.grails.commons.DefaultGrailsDomainClass

在我的 grails 项目中没有重新实现/子类化 spring-security-core 插件 taglibs,无法同时使用 taglibs 和使用您的自定义域用户类来实例化从过滤器传递到您的令牌提供者。

再一次,多出一行代码是非常需要付出的很小的代价:)

【讨论】:

以上是关于在 Spring Security 2.06 中实现自定义 AuthenticationProvider的主要内容,如果未能解决你的问题,请参考以下文章

在 Spring Security 中实现分层角色

如何在 Spring Security 中实现基于 JWT 的身份验证和授权

如何使用 spring-security-core-ldap 插件在 grails 中实现 LDAP 身份验证?

在我的(java spring mvc + mysql application,thymeleaf)中实现spring security之后,身份验证发生了一些奇怪的事情

仅使用 Spring-Security(或)Spring 进行授权

Spring security - 允许匿名访问