JdbcUsersConnectionRepository不起作用的问题解决方案
注意:关键源码位置有 “ //###################关键看这里##############”标注
依赖版本:(该问题只是针对我使用的Spring Boot版本,所以这里有必要列出版本号供参考)
<!-- Spring Maven 依赖管理 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Brussels-SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
问题
当你继承SocialConfigurerAdapter类并复写了getUsersConnectionRepository(),运行程序时发现你期待的JdbcUsersConnectionRepository类并没有起作用,系统还是默认使用InMemoryUsersConnectionRepository。
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired(required = false)
private ConnectionSignUp connectionSignUp;
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,
connectionFactoryLocator, Encryptors.noOpText());
if (connectionSignUp != null) {
repository.setConnectionSignUp(connectionSignUp);
}
return repository;
}
}
当你使用已经注册的QQ用户在扫码登陆之后还会调到登陆页面,这是由于你配置的JdbcUsersConnectionRepository 没有生效,系统默认使用InMemoryUsersConnectionRepository来获取用户的userId,由于从内存中无法获取相应的userId,导致鉴权失败,如果你系统配置了signupUrl ,那么程序就会执行跳转。
执行流程分析,有助于了解原因
1、当请求到达三方登陆过滤器时。Authentication success = getAuthenticationManager().authenticate(token);通过这行代码可以看出,过滤器获取到一个AuthenticationManager的实现类(org.springframework.security.authentication.ProviderManager)用来鉴权。
public class SocialAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private Authentication doAuthentication(SocialAuthenticationService<?> authService, HttpServletRequest request, SocialAuthenticationToken token) {
try {
if (!authService.getConnectionCardinality().isAuthenticatePossible()) return null;
token.setDetails(authenticationDetailsSource.buildDetails(request));
//###################关键看这里##############
Authentication success = getAuthenticationManager().authenticate(token);
Assert.isInstanceOf(SocialUserDetails.class, success.getPrincipal(), "unexpected principle type");
updateConnections(authService, token, success);
return success;
} catch (BadCredentialsException e) {
// connection unknown, register new user?
//###################关键看这里##############
if (signupUrl != null) {
// store ConnectionData in session and redirect to register page
sessionStrategy.setAttribute(new ServletWebRequest(request), ProviderSignInAttempt.SESSION_ATTRIBUTE, new ProviderSignInAttempt(token.getConnection()));
throw new SocialAuthenticationRedirectException(buildSignupUrl(request));
}
throw e;
}
}
}
2、ProviderManager会查找容器中所有的AuthenticationProvider实现,并为三方登陆选择合适的类处理(org.springframework.social.security.SocialAuthenticationProvider)
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
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;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
}
3、SocialAuthenticationProvider中的toUserId()方法我们可以看出他调用了UsersConnectionRepository的一个实现类来获取userId。
关键就在这里,系统默认使用的InMemoryUsersConnectionRepository是无法获取到用户ID,所以就会抛出UsernameNotFoundException异常,当SocialAuthenticationFilter 捕获到这个异常时,就会执行页面跳转。
public class SocialAuthenticationProvider implements AuthenticationProvider {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(SocialAuthenticationToken.class, authentication, "unsupported authentication type");
Assert.isTrue(!authentication.isAuthenticated(), "already authenticated");
SocialAuthenticationToken authToken = (SocialAuthenticationToken) authentication;
String providerId = authToken.getProviderId();
Connection<?> connection = authToken.getConnection();
//###################关键看这里##############
String userId = toUserId(connection);
if (userId == null) {
throw new BadCredentialsException("Unknown access token");
}
UserDetails userDetails = userDetailsService.loadUserByUserId(userId);
if (userDetails == null) {
throw new UsernameNotFoundException("Unknown connected account id");
}
return new SocialAuthenticationToken(connection, userDetails, authToken.getProviderAccountData(), getAuthorities(providerId, userDetails));
}
protected String toUserId(Connection<?> connection) {
//###################关键看这里##############
List<String> userIds = usersConnectionRepository.findUserIdsWithConnection(connection);
// only if a single userId is connected to this providerUserId
return (userIds.size() == 1) ? userIds.iterator().next() : null;
}
}
解决办法
问题的根源找到了,只要将容器中的UsersConnectionRepository实现替换成JdbcUsersConnectionRepository即可。回到最初的配置类上,我们来看看解决办法,其实很简单,多加两个注解: @Primary和@Bean,关于注解的作用可以自行百度。
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired(required = false)
private ConnectionSignUp connectionSignUp;
@Primary
@Bean
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,
connectionFactoryLocator, Encryptors.noOpText());
if (connectionSignUp != null) {
repository.setConnectionSignUp(connectionSignUp);
}
return repository;
}
}