Spring 安全失败 JdbcClientDetailsService 错误
Spring security fails JdbcClientDetailsService error
我目前收到此错误:
org.springframework.dao.EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0
然而,它由 DaoAuthenticationProvider 处理,它抛出一个 InternalAuthenticationServiceException 异常。这绕过了 ProviderManager 的异常处理,ProviderManager 不会调用我的 customDaoProvider 来验证用户。
有 2 个 DaoAuthenticationProvider 注册到 ProviderManager。我的观察是,在 ProviderManager 的第一次迭代中,它 selects DefaultAuthenticationProvider。我认为这就是它的工作原理,然后 ProviderManager 会失败,在它的第二次迭代中 select 我的自定义 DaoAuthenticationProvider (customDaoProvider) 会验证用户凭据。事情发生在第一次迭代中,当它失败时,它会抛出 ProviderManager 会重新抛出的特定错误。
这是将捕获特定错误的 ProviderManager 内部:
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
} catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to invalid account status
throw e;
} catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
注意 InternalAuthenticationServiceException
的问题。在这里它重新抛出异常并停止身份验证过程。
我下面的整个security.xml(元素除外):
<http pattern="/oauth/token" create-session="stateless" authentication-manager-ref="authenticationManager" xmlns="http://www.springframework.org/schema/security">
<intercept-url pattern="/oauth/token" access="ROLE_FRONTEND" />
<anonymous enabled="false" />
<http-basic entry-point-ref="clientAuthenticationEntryPoint" />
<custom-filter ref="clientCredentialsTokenEndpointFilter" before="BASIC_AUTH_FILTER" />
<access-denied-handler ref="oauthAccessDeniedHandler" />
</http>
<authentication-manager alias="authenticationManager" xmlns="http://www.springframework.org/schema/security">
<sec:authentication-provider user-service-ref="clientDetailsUserService" />
<sec:authentication-provider ref="customDaoProvider">
</sec:authentication-provider>
</authentication-manager>
<beans:bean id="customUserDetailService" class="com.xxxx.api.services.UserDetailsServiceImpl">
<beans:property name="accountService" ref="userAccountServiceImpl"></beans:property>
</beans:bean>
<!-- DAO Providers -->
<beans:bean id="customDaoProvider" class="com.xxxx.api.daoauthproviders.CustomDaoAuthenticationProvider">
<beans:property name="userDetailsService" ref="customUserDetailService"/>
<beans:property name="passwordEncoder" ref="passwordEncoder" />
<beans:property name="accountService" ref="userAccountServiceImpl"></beans:property>
<beans:property name="clientService" ref="clientServiceImpl"></beans:property>
</beans:bean>
<!-- End of DAO Providers -->
<beans:bean id="clientDetails" class="org.springframework.security.oauth2.provider.JdbcClientDetailsService">
<beans:constructor-arg ref="dataSource" />
</beans:bean>
<beans:bean id="accessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased" xmlns="http://www.springframework.org/schema/beans">
<beans:constructor-arg>
<beans:list>
<beans:bean class="org.springframework.security.oauth2.provider.vote.ScopeVoter" />
<beans:bean class="org.springframework.security.access.vote.RoleVoter" />
<beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
</beans:list>
</beans:constructor-arg>
</beans:bean>
<oauth:authorization-server client-details-service-ref="clientDetails" token-services-ref="tokenServices">
<oauth:refresh-token />
<oauth:client-credentials/>
<oauth:custom-grant token-granter-ref="customPasswordGrant"/>
<oauth:custom-grant token-granter-ref="randomTokenGrant" />
</oauth:authorization-server>
<beans:bean id="restServicesSuccessHandler" class="com.xxxx.api.handlers.RestAuthenticationSuccessHandler" />
<oauth:resource-server id="resourceServerFilter" resource-id="api" token-services-ref="tokenServices" />
<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" />
<beans:bean id="clientAuthenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
<beans:property name="realmName" value="api/" />
</beans:bean>
<!-- Custom Grant Types -->
<beans:bean id="randomTokenGrant" class="com.xxxx.api.grants.RandomTokenGrant">
<beans:constructor-arg index="0"><beans:value>${api.common.login.oauth.token}</beans:value></beans:constructor-arg>
<beans:constructor-arg index="1"><beans:value>${user.login.username.param}</beans:value></beans:constructor-arg>
<beans:constructor-arg index="2"><beans:value>${user.login.password.param}</beans:value></beans:constructor-arg>
<beans:constructor-arg index="3" ref="authenticationManager"></beans:constructor-arg>
<beans:constructor-arg index="4" ref="tokenServices"></beans:constructor-arg>
<beans:constructor-arg index="5" ref="clientDetails"></beans:constructor-arg>
<beans:constructor-arg index="6"><beans:value>${api.common.login.oauth.grant_type.two_factor}</beans:value></beans:constructor-arg>
<beans:constructor-arg index="7" ref="userAccountServiceImpl"></beans:constructor-arg>
</beans:bean>
<beans:bean id="customPasswordGrant" class="com.xxxx.api.grants.CustomPasswordGrant">
<beans:constructor-arg index="0" ref="authenticationManager"></beans:constructor-arg>
<beans:constructor-arg index="1" ref="tokenServices"></beans:constructor-arg>
<beans:constructor-arg index="2" ref="clientDetails"></beans:constructor-arg>
<beans:constructor-arg index="3" ref="userAccountServiceImpl"></beans:constructor-arg>
<beans:constructor-arg index="4"><beans:value>${api.common.login.oauth.grant_type.custom_password}</beans:value></beans:constructor-arg>
</beans:bean>
<!-- End Of Custom Grant Types -->
<beans:bean id="clientCredentialsTokenEndpointFilter" class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter">
<beans:property name="authenticationManager" ref="authenticationManager" />
</beans:bean>
<beans:bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.JdbcTokenStore">
<beans:constructor-arg ref="dataSource" />
</beans:bean>
<beans:bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
<beans:property name="tokenStore" ref="tokenStore" />
<beans:property name="supportRefreshToken" value="true" />
<beans:property name="clientDetailsService" ref="clientDetails" />
</beans:bean>
<beans:bean id="clientDetailsUserService" class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
<beans:constructor-arg ref="clientDetails" />
</beans:bean>
<beans:bean id="oauthAccessDeniedHandler" class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler" />
<beans:bean id="oauthAuthenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
<beans:property name="realmName" value="api" />
</beans:bean>
<beans:bean id="clientServiceImpl" class="com.xxxx.api.services.ClientServiceImpl">
<beans:property name="clientDetailsService" ref="clientDetails" />
</beans:bean>
你可以为客户和用户使用相同的AuthenticationManager
,但这只会让生活变得困难,特别是如果你使用ProviderManager
(像你是),因为客户端和用户凭据以相同的形式呈现 (UsernamePasswordAuthenticationToken
)。如果您坚持提供的默认行为,这是一个单独的 AuthenticationManager
框架为您创建的客户端,那就容易多了。所有示例和集成测试都使用这种双重身份验证管理器模式,因此有很多值得借鉴的地方。
我目前收到此错误:
org.springframework.dao.EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0
然而,它由 DaoAuthenticationProvider 处理,它抛出一个 InternalAuthenticationServiceException 异常。这绕过了 ProviderManager 的异常处理,ProviderManager 不会调用我的 customDaoProvider 来验证用户。
有 2 个 DaoAuthenticationProvider 注册到 ProviderManager。我的观察是,在 ProviderManager 的第一次迭代中,它 selects DefaultAuthenticationProvider。我认为这就是它的工作原理,然后 ProviderManager 会失败,在它的第二次迭代中 select 我的自定义 DaoAuthenticationProvider (customDaoProvider) 会验证用户凭据。事情发生在第一次迭代中,当它失败时,它会抛出 ProviderManager 会重新抛出的特定错误。
这是将捕获特定错误的 ProviderManager 内部:
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
} catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to invalid account status
throw e;
} catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
注意 InternalAuthenticationServiceException
的问题。在这里它重新抛出异常并停止身份验证过程。
我下面的整个security.xml(元素除外):
<http pattern="/oauth/token" create-session="stateless" authentication-manager-ref="authenticationManager" xmlns="http://www.springframework.org/schema/security">
<intercept-url pattern="/oauth/token" access="ROLE_FRONTEND" />
<anonymous enabled="false" />
<http-basic entry-point-ref="clientAuthenticationEntryPoint" />
<custom-filter ref="clientCredentialsTokenEndpointFilter" before="BASIC_AUTH_FILTER" />
<access-denied-handler ref="oauthAccessDeniedHandler" />
</http>
<authentication-manager alias="authenticationManager" xmlns="http://www.springframework.org/schema/security">
<sec:authentication-provider user-service-ref="clientDetailsUserService" />
<sec:authentication-provider ref="customDaoProvider">
</sec:authentication-provider>
</authentication-manager>
<beans:bean id="customUserDetailService" class="com.xxxx.api.services.UserDetailsServiceImpl">
<beans:property name="accountService" ref="userAccountServiceImpl"></beans:property>
</beans:bean>
<!-- DAO Providers -->
<beans:bean id="customDaoProvider" class="com.xxxx.api.daoauthproviders.CustomDaoAuthenticationProvider">
<beans:property name="userDetailsService" ref="customUserDetailService"/>
<beans:property name="passwordEncoder" ref="passwordEncoder" />
<beans:property name="accountService" ref="userAccountServiceImpl"></beans:property>
<beans:property name="clientService" ref="clientServiceImpl"></beans:property>
</beans:bean>
<!-- End of DAO Providers -->
<beans:bean id="clientDetails" class="org.springframework.security.oauth2.provider.JdbcClientDetailsService">
<beans:constructor-arg ref="dataSource" />
</beans:bean>
<beans:bean id="accessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased" xmlns="http://www.springframework.org/schema/beans">
<beans:constructor-arg>
<beans:list>
<beans:bean class="org.springframework.security.oauth2.provider.vote.ScopeVoter" />
<beans:bean class="org.springframework.security.access.vote.RoleVoter" />
<beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
</beans:list>
</beans:constructor-arg>
</beans:bean>
<oauth:authorization-server client-details-service-ref="clientDetails" token-services-ref="tokenServices">
<oauth:refresh-token />
<oauth:client-credentials/>
<oauth:custom-grant token-granter-ref="customPasswordGrant"/>
<oauth:custom-grant token-granter-ref="randomTokenGrant" />
</oauth:authorization-server>
<beans:bean id="restServicesSuccessHandler" class="com.xxxx.api.handlers.RestAuthenticationSuccessHandler" />
<oauth:resource-server id="resourceServerFilter" resource-id="api" token-services-ref="tokenServices" />
<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" />
<beans:bean id="clientAuthenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
<beans:property name="realmName" value="api/" />
</beans:bean>
<!-- Custom Grant Types -->
<beans:bean id="randomTokenGrant" class="com.xxxx.api.grants.RandomTokenGrant">
<beans:constructor-arg index="0"><beans:value>${api.common.login.oauth.token}</beans:value></beans:constructor-arg>
<beans:constructor-arg index="1"><beans:value>${user.login.username.param}</beans:value></beans:constructor-arg>
<beans:constructor-arg index="2"><beans:value>${user.login.password.param}</beans:value></beans:constructor-arg>
<beans:constructor-arg index="3" ref="authenticationManager"></beans:constructor-arg>
<beans:constructor-arg index="4" ref="tokenServices"></beans:constructor-arg>
<beans:constructor-arg index="5" ref="clientDetails"></beans:constructor-arg>
<beans:constructor-arg index="6"><beans:value>${api.common.login.oauth.grant_type.two_factor}</beans:value></beans:constructor-arg>
<beans:constructor-arg index="7" ref="userAccountServiceImpl"></beans:constructor-arg>
</beans:bean>
<beans:bean id="customPasswordGrant" class="com.xxxx.api.grants.CustomPasswordGrant">
<beans:constructor-arg index="0" ref="authenticationManager"></beans:constructor-arg>
<beans:constructor-arg index="1" ref="tokenServices"></beans:constructor-arg>
<beans:constructor-arg index="2" ref="clientDetails"></beans:constructor-arg>
<beans:constructor-arg index="3" ref="userAccountServiceImpl"></beans:constructor-arg>
<beans:constructor-arg index="4"><beans:value>${api.common.login.oauth.grant_type.custom_password}</beans:value></beans:constructor-arg>
</beans:bean>
<!-- End Of Custom Grant Types -->
<beans:bean id="clientCredentialsTokenEndpointFilter" class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter">
<beans:property name="authenticationManager" ref="authenticationManager" />
</beans:bean>
<beans:bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.JdbcTokenStore">
<beans:constructor-arg ref="dataSource" />
</beans:bean>
<beans:bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
<beans:property name="tokenStore" ref="tokenStore" />
<beans:property name="supportRefreshToken" value="true" />
<beans:property name="clientDetailsService" ref="clientDetails" />
</beans:bean>
<beans:bean id="clientDetailsUserService" class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
<beans:constructor-arg ref="clientDetails" />
</beans:bean>
<beans:bean id="oauthAccessDeniedHandler" class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler" />
<beans:bean id="oauthAuthenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
<beans:property name="realmName" value="api" />
</beans:bean>
<beans:bean id="clientServiceImpl" class="com.xxxx.api.services.ClientServiceImpl">
<beans:property name="clientDetailsService" ref="clientDetails" />
</beans:bean>
你可以为客户和用户使用相同的AuthenticationManager
,但这只会让生活变得困难,特别是如果你使用ProviderManager
(像你是),因为客户端和用户凭据以相同的形式呈现 (UsernamePasswordAuthenticationToken
)。如果您坚持提供的默认行为,这是一个单独的 AuthenticationManager
框架为您创建的客户端,那就容易多了。所有示例和集成测试都使用这种双重身份验证管理器模式,因此有很多值得借鉴的地方。