Spring Security Oauth2 - 多用户认证服务

Posted

技术标签:

【中文标题】Spring Security Oauth2 - 多用户认证服务【英文标题】:Spring Secuirty Oauth 2 - multiple user authentication services 【发布时间】:2015-03-11 02:32:08 【问题描述】:

我的应用程序提供的 oauth2 令牌服务与以下 github 项目中提供的服务相同:https://github.com/iainporter/oauth2-provider

它基于 Spring Security OAuth2。

我提供了 UserDetailsS​​ervice 的自定义实现:

<bean id="userService" class="org.example.core.service.DBUserServiceImpl" />

以及以下用户身份验证管理器:

<sec:authentication-manager alias="userAuthenticationManager">
    <sec:authentication-provider user-service-ref="userService">
        <sec:password-encoder ref="passwordEncoder" />
    </sec:authentication-provider>
</sec:authentication-manager>

现在我想提供其他的用户认证方法(其他UserDetailsS​​ervice),例如:

<bean id="otherUserService" class="org.example.core.service.LDAPUserServiceImpl" />

不幸的是,我没有找到如何在文档中做到这一点的方法。在请求级别上,我想区分使用哪种方法(哪种用户服务):

查询参数 http 标头(例如 RealmName)

【问题讨论】:

【参考方案1】:

您需要使用DelegatingAuthenticationEntryPoint 来配置多个入口点。这意味着您可以有多种身份验证方式。以下是示例代码:

DBUser 入口点:

public class DBUserAuthencticationEntryPoint extends BasicAuthenticationEntryPoint 

    @Override
    public void commence(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException 
        super.commence(request, response, authException);
    

LDAP 入口点:

public class LDAPAuthencticationEntryPoint extends BasicAuthenticationEntryPoint 

    @Override
    public void commence(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException 
         super.commence(request, response, authException);
    

然后您需要创建RequestMatchers 来选择正确的入口点(基于标头/领域名称):

DBUser 请求匹配器:

RequestMatcher dbUserMatcher = new RequestMatcher()        
    @Override
    public boolean matches(HttpServletRequest request) 
        // Logic to identify a DBUser kind of reqeust
    
;

LDAP 用户请求匹配器:

RequestMatcher ldapMatcher = new RequestMatcher()      
    @Override
    public boolean matches(HttpServletRequest request) 
        // Logic to identify a LDAP kind of reqeust
    
;

现在我们需要将这些匹配器和入口点添加到DelegatingAuthenticationEntryPoint。在运行时DelegatingAuthenticationEntryPoint 获取入口点并基于返回true 的匹配器进行身份验证。

DBUserAuthencticationEntryPoint dbUserEntryPoint = new DBUserAuthencticationEntryPoint();
LDAPAuthencticationEntryPoint ldapEntryPoint  = new LDAPAuthencticationEntryPoint();

LinkedHashMap<RequestMatcher,AuthenticationEntryPoint> entryPoints = new LinkedHashMap<RequestMatcher,AuthenticationEntryPoint>();
entryPoints.put(ldapMatcher, ldapEntryPoint);
entryPoints.put(dbUserMatcher, dbUserEntryPoint);

DelegatingAuthenticationEntryPoint delegatingAuthenticationEntryPoint  = new DelegatingAuthenticationEntryPoint(entryPoints);

现在在configure() 方法中将DelegatingAuthenticationEntryPoint 映射到HttpSecurity

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter 
    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http.
            authorizeRequests().
                regexMatchers("/login.*").permitAll().
                regexMatchers("/api.*").fullyAuthenticated().        
        and().
            formLogin().loginPage("/login").
        and().
            exceptionHandling().authenticationEntryPoint(delegatingAuthenticationEntryPoint);
    

配置提供者管理器:

@Bean
public AuthenticationManager authenticationManager() 
    return new ProviderManager(Arrays.asList(provider1, provider2);

【讨论】:

好的,如果我能够重定向到我的自定义身份验证页面(例如,取决于 HTTP RealmName 标头),这将起作用。不幸的是,我不提供身份验证页面,只提供 OAuth API。因此,当请求到来时,我需要根据 RealmName 标头选择适当的身份验证提供程序,并使用来自 Authorization http 标头的凭据进行身份验证。 不,这适用于任何类型的身份验证过程。我刚刚出于演示目的采用了基于表单的登录。通过调用http.authenticationProvider(authenticationProvider)删除基于表单的登录代码并设置身份验证提供程序 好的,但是我想使用标准的 Oauth 流程并将 org.springframework.security.authentication.ProviderManager 用于用户特定的身份验证管理器。我无法从身份验证入口点执行此操作。 我已修改入口点类以调用父类上的commence() 方法,并添加了配置多个提供程序的方法。当从开始方法触发身份验证过程时,将触发相应的提供程序。 Spring 将通过 AuthenticationProviders 列表迭代 Authentication 请求。 AuthenticationProvider 通常会按顺序尝试,直到提供非空响应为止。【参考方案2】:

我找到了与 Mithun 提供的解决方案不同的解决方案。

应用程序上下文包含由不同身份验证提供程序启动的用户身份验证管理器:

<sec:authentication-manager alias="userAuthenticationManager">
    <sec:authentication-provider ref="customerAuthProvider" />
    <sec:authentication-provider ref="adminAuthProvider" />
</sec:authentication-manager>

其中 customerAuthProvider 和 adminAuthProvider 是 DaoAuthenticationProvider 的扩展,具有不同的 userDetails 服务:

<bean id="customerAuthProvider" class="org.example.security.authentication.provider.CustomerAuthenticationProvider">
    <property name="userDetailsService" ref="userService" />
    <property name="passwordEncoder" ref="passwordEncoder" />
</bean>

<bean id="adminAuthProvider" class="org.example.security.authentication.provider.AdminAuthenticationProvider">
    <property name="userDetailsService" ref="otherUserService" />
</bean>

您需要做的就是重写“supports”方法,该方法指示当前身份验证提供程序是否能够处理特定身份验证:

public class CustomerAuthenticationProvider extends DaoAuthenticationProvider 

    @Override
    public boolean supports ( Class<?> authentication ) 
        return CustomerUsernamePasswordAuthenticationToken.isAssignableFrom(authentication);
    


public class AdminAuthenticationProvider extends DaoAuthenticationProvider 

    @Override
    public boolean supports ( Class<?> authentication ) 
        return AdminUsernamePasswordAuthenticationToken.isAssignableFrom(authentication);
    

最后你需要扩展令牌授予者。就我而言,我扩展了 ResourceOwnerPasswordTokenGranter,这意味着它支持“密码”授权:

<oauth:authorization-server client-details-service-ref="client-details-service" token-services-ref="tokenServices">
    <oauth:refresh-token/>
    <oauth:custom-grant token-granter-ref="customPasswordTokenGranter"/>
</oauth:authorization-server>

您可以使用 TokenRequest 对象来区分要实例化哪个 Authentication 类(AdminUsernamePasswordAuthenticationToken 或 CustomerUsernamePasswordAuthenticationToken)

public class CustomResourceOwnerPasswordTokenGranter extends ResourceOwnerPasswordTokenGranter 

    protected OAuth2Authentication getOAuth2Authentication ( ClientDetails client, TokenRequest tokenRequest ) 
        Map parameters = tokenRequest.getRequestParameters();
        String username = (String) parameters.get("username");
        String password = (String) parameters.get("password");

        String realmName = (String) parameters.get("realm_name");

        Authentication userAuth = createAuthentication(username, password, realmName);
        try 
            userAuth = this.authenticationManager.authenticate(userAuth);
         catch ( AccountStatusException ase ) 
            throw new InvalidGrantException(ase.getMessage());
         catch ( BadCredentialsException e ) 
            throw new InvalidGrantException(e.getMessage());
        
        if ( ( userAuth == null ) || ( ! ( userAuth.isAuthenticated() ) ) ) 
            throw new InvalidGrantException("Could not authenticate user: " + username);
        

        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    

    private Authentication createAuthentication ( String username, String password, String realmName ) throws InvalidGrantException 
       // TODO: decide basing on realm name
    

【讨论】:

以上是关于Spring Security Oauth2 - 多用户认证服务的主要内容,如果未能解决你的问题,请参考以下文章

Spring-Security OAuth2 设置 - 无法找到 oauth2 命名空间处理程序

Spring Security OAuth2 v5:NoSuchBeanDefinitionException:'org.springframework.security.oauth2.jwt.Jwt

针对授权标头的Spring Security OAuth2 CORS问题

OAuth2.0学习(4-1)Spring Security OAuth2.0 - 代码分析

Spring Security---Oauth2详解

如何使用spring-security,oauth2调整实体的正确登录?