Spring Security Oauth - 发送令牌请求时需要基本访问身份验证

Posted

技术标签:

【中文标题】Spring Security Oauth - 发送令牌请求时需要基本访问身份验证【英文标题】:Spring Security Oauth - Basic access authentication needed when sending token request 【发布时间】:2014-03-21 09:27:51 【问题描述】:

我在使用 Spring Security OAuth 2.0 时遇到了一个问题。

有spring-security的基本配置,取自样例:

<http pattern="/oauth/token" create-session="stateless"
    authentication-manager-ref="clientAuthenticationManager"
    xmlns="http://www.springframework.org/schema/security">
    <intercept-url pattern="/oauth/token" access="IS_AUTHENTICATED_FULLY" />
    <anonymous enabled="false" />
    <http-basic entry-point-ref="clientAuthenticationEntryPoint" />
    <custom-filter ref="clientCredentialsTokenEndpointFilter"
        after="BASIC_AUTH_FILTER" />
    <access-denied-handler ref="oauthAccessDeniedHandler" />
</http>

<http pattern="/path/**" create-session="never"
    entry-point-ref="oauthAuthenticationEntryPoint"
    access-decision-manager-ref="accessDecisionManager"
    xmlns="http://www.springframework.org/schema/security">
    <anonymous enabled="false" />
    <intercept-url pattern="/path/*" access="ROLE_USER" />
    <custom-filter ref="resourceServerFilter" before="PRE_AUTH_FILTER" />
    <access-denied-handler ref="oauthAccessDeniedHandler" />
</http>

<bean id="oauthAuthenticationEntryPoint"
    class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
    <property name="realmName" value="path" />
</bean>

<bean id="clientAuthenticationEntryPoint"
    class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
    <property name="realmName" value="path/client" />
    <property name="typeName" value="Basic" />
</bean>

<bean id="oauthAccessDeniedHandler"
    class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler" />

<bean id="clientCredentialsTokenEndpointFilter"
    class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter">
    <property name="authenticationManager" ref="clientAuthenticationManager" />
</bean>

<bean id="accessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased"
    xmlns="http://www.springframework.org/schema/beans">
    <constructor-arg>
        <list>
            <bean class="org.springframework.security.oauth2.provider.vote.ScopeVoter" />
            <bean class="org.springframework.security.access.vote.RoleVoter" />
            <bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
        </list>
    </constructor-arg>
</bean>

<bean id="oauthAuthenticationProvider" class="my.package.OAuthAuthenticationProvider" xmlns="http://www.springframework.org/schema/beans" />

<authentication-manager id="clientAuthenticationManager"
    xmlns="http://www.springframework.org/schema/security">
    <authentication-provider user-service-ref="clientDetailsUserService" />
</authentication-manager>

<authentication-manager alias="authenticationManager"
    xmlns="http://www.springframework.org/schema/security">
    <authentication-provider ref="oauthAuthenticationProvider" />
</authentication-manager>

<bean id="clientDetailsUserService"
    class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
    <constructor-arg ref="clientDetails" />
</bean>

<bean id="tokenStore"
    class="org.springframework.security.oauth2.provider.token.InMemoryTokenStore" />

<bean id="tokenServices"
    class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
    <property name="tokenStore" ref="tokenStore" />
    <property name="supportRefreshToken" value="true" />
    <property name="clientDetailsService" ref="clientDetails" />
</bean>

<bean id="userApprovalHandler"
    class="org.springframework.security.oauth2.provider.approval.TokenServicesUserApprovalHandler">
    <property name="tokenServices" ref="tokenServices" />
</bean>

<!-- authorization-server aka AuthorizationServerTokenServices is an interface 
    that defines everything necessary for token management -->
<oauth:authorization-server
    client-details-service-ref="clientDetails" token-services-ref="tokenServices"
    user-approval-handler-ref="userApprovalHandler">
    <oauth:authorization-code />
    <oauth:implicit />
    <oauth:refresh-token />
    <oauth:client-credentials />
    <oauth:password />
</oauth:authorization-server>

<oauth:resource-server id="resourceServerFilter"
    resource-id="test" token-services-ref="tokenServices" />
<!-- ClientsDeailsService: Entry Point to clients database (given is in 
    memory implementation) -->
<oauth:client-details-service id="clientDetails">
    <!-- client -->
    <oauth:client client-id="the_client"
        authorized-grant-types="authorization_code,client_credentials"
        authorities="ROLE_USER" scope="read,write,trust" secret="secret" />

    <oauth:client client-id="my-trusted-client-with-secret"
        authorized-grant-types="password,authorization_code,refresh_token,implicit"
        secret="somesecret" authorities="ROLE_USER" />

</oauth:client-details-service>

<sec:global-method-security
    pre-post-annotations="enabled" proxy-target-class="true">
    <sec:expression-handler ref="oauthExpressionHandler" />
</sec:global-method-security>

<oauth:expression-handler id="oauthExpressionHandler" />

<oauth:web-expression-handler id="oauthWebExpressionHandler" />

当我的 web.xml 文件看起来像这样时,它可以工作:

...
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/spring-security.xml</param-value>
</context-param>
...
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
...
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
...
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
...
<servlet>
    <servlet-name>Dispatcher Servlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
...
<servlet-mapping>
    <servlet-name>Dispatcher Servlet</servlet-name>
    <url-pattern>/*</url-pattern>
</servlet-mapping> 

但在这种情况下,系统的“旧部分”不起作用。 所以我必须更改 Dispatcher Servlet 映射:

<servlet-mapping>
    <servlet-name>Dispatcher Servlet</servlet-name>
    <url-pattern>/somepath/*</url-pattern>
</servlet-mapping> 

现在旧部分有效,但 Oauth Spring 安全性无效。我尝试了两种 url 模式:

<servlet-mapping>
    <servlet-name>Dispatcher Servlet</servlet-name>
    <url-pattern>/somepath/*</url-pattern>
    <url-pattern>/*</url-pattern>
</servlet-mapping> 

然后 oauth 有效,旧部分无效。

我又试了一次,改了:

<http pattern="/somepath/oauth/token" create-session="stateless"
    authentication-manager-ref="clientAuthenticationManager"
    xmlns="http://www.springframework.org/schema/security">
    <intercept-url pattern="/somepath/oauth/token" access="IS_AUTHENTICATED_FULLY" />
    <anonymous enabled="false" />
    <http-basic entry-point-ref="clientAuthenticationEntryPoint" />
    <custom-filter ref="clientCredentialsTokenEndpointFilter"
        after="BASIC_AUTH_FILTER" />
    <access-denied-handler ref="oauthAccessDeniedHandler" />
</http>

现在它是这样工作的:

    我正在访问地址(以前有效):

    https://localhost.server:8443/system/somepath/oauth/token?grant_type=password&client_id=my-trusted-client-with-secret&client_secret=somesecret&username=user&password=pass

    Web 浏览器要求输入登录名和密码(基本访问身份验证)。

    我需要在登录字段中输入客户端 ID,在密码字段中输入密码。

    生成 oauth 请求,然后调用 my.package.OAuthAuthenticationProvider 中的身份验证方法。

我应该改变什么来避免这个基本身份验证提示?

【问题讨论】:

【参考方案1】:

您可以使用这样的配置来避免登录页面:

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter 
    ...

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        // @formatter:off
        http
            .authorizeRequests()
                .antMatchers("/").permitAll()
            .and()
                .authorizeRequests().anyRequest().hasRole("USER")
            .and()
                .csrf()
                .requireCsrfProtectionMatcher(new AntPathRequestMatcher("/**")).disable();
        // @formatter:on
    
    ...

默认情况下,此方法包含一个登录页面。使用此新实现,如果您尝试在没有令牌的情况下执行请求,将显示 403 错误页面(您也可以管理此错误以重定向到自定义页面)

【讨论】:

【参考方案2】:

您的 servlet 和过滤器映射问题源于 Spring OAuth 需要 DispatcherServlet 用于其 OAuth 相关端点的事实。我建议您将它们放在默认的 servlet 映射 (/) 中,但这取决于您。在任何情况下,您的一站式安全配置都是 servlet 配置文件,而不是根上下文(因此您需要使用 DispatcherServlet 而不是 ContextLoaderListener 加载它)。这与示例中的相同(您需要 web.xml 中的 servlet init 参数),但没有什么可以阻止您添加自己的其他 servlet(当然,只要映射不冲突)。

身份验证问题不同。 OAuth2 规范建议使用基本身份验证保护 /token 端点,因此提示原则上是一件好事,尽管我可以看到您试图避免它。像您正在做的那样将秘密放入表单字段中并不够安全,无法在生产系统中使用,但如果出于某种原因您需要它,您可以使用ClientCredentialsTokenEndpointFilter(就像您所做的那样)。过滤器不起作用的原因可能是因为您发送的是 GET(而不是 POST),出于安全目的,这比首先使用过滤器更糟糕,但我没有看到设置的标志在过滤器中打开仅 POST 验证的任何地方(尽管我建议你在任何地方愤怒地使用这个东西时设置它)。

您没有显示 2 个 Spring XML 文件,但您的 web.xml 引用了 2。我认为问题最终可以追溯到您将它们混淆或 bean 定义在两者之间重复的事实。也许您可以澄清一下(或者按照上面的建议切换到从DispatcherServlet 加载单个文件)?

【讨论】:

感谢您的回复。我在另一个项目中使用spring oauth。我不得不使用默认的 servlet 映射,但这是可能的(该应用程序不能在生产环境中运行 :))。如果没有连接到 ContextLoaderListener 的 spring-security 配置文件,spring-security 将无法启动。但是在第二个应用程序的情况下,基本身份验证没有要求 client_id 和秘密,通过 GET 请求参数传递它就足以进行身份​​验证......这里发生了同样的情况,当我将 Dispatcher Servlet Mapping 更改为默认值时. 如果没有作为根上下文加载,Spring Security“不会启动”是不正确的(查看 oauth 示例以获取反例)。我想我记得您尚未启用非默认 servlet 映射所需的设置 - 必须显式设置身份验证和令牌端点 url。我需要再仔细看看(明天),看看我是否可以用更多信息更新答案,但也许这会让你开始。 再次感谢您的帮助。现在我正在使用 POST 方法和其他发送基本身份验证凭据的方式,它似乎工作正常。我应该如何设置这个“POST-only”验证器? 还有一个问题——如何正确发送令牌请求——我需要一个安全的解决方案。现在我只从 GET 更改为 POST 方法。 (我已经在使用 SSL) 仅 POST 验证在 ClientCredentialsTokenEndpointFilter 中。最好根本不使用该过滤器并为客户端使用一些其他更安全的身份验证(您已经启用了 HTTP Basic,这就是您的浏览器在手动测试中提示您的原因)。任何 HTTP 客户端都将支持基本身份验证(查看文档)。

以上是关于Spring Security Oauth - 发送令牌请求时需要基本访问身份验证的主要内容,如果未能解决你的问题,请参考以下文章

org.springframework.security.oauth 和 org.codehaus.spring-security-oauth 有啥区别?

Spring Security 入门(1-3)Spring Security oauth2.0 指南

使用 spring-session 和 spring-cloud-security 时,OAuth2ClientContext (spring-security-oauth2) 不会保留在 Redis

社交登录,spring-security-oauth2 和 spring-security-jwt?

弃用 spring-security-oauth 作为客户端

spring-security-oauth2中的HttpSecurity配置问题