在Spring Security源码分析三:Spring Social实现QQ社交登录和Spring Security源码分析四:Spring Social实现微信社交登录这两章中,我们使用
Spring Social
已经实现了国内最常用的微信
社交登录。本章我们来简单分析一下Spring Social
在社交登录的过程中做了哪些事情?(微博
社交登录也已经实现,由于已经连续两篇介绍社交登录,所以不在单开一章节描述)
OAuth2是一种授权协议,简单理解就是它可以让用户在不将用户名密码交给第三方应用的情况下,第三方应用有权访问用户存在服务提供商上面的数据。
Authentication
放入SecurityContext中
SecurityContext
中放入一个已经认证过的Authentication
实例,那么对于Spring Security
来说,已经成功登录Spring Social
就是为我们将OAuth2
认证流程封装到SocialAuthenticationFilter
过滤器中,并根据返回的用户信息构建Authentication
。然后使用Spring Security
的验证逻辑从而实现使用社交登录。
启动logback断点调试;
ValidateCodeFilter
校验验证码过滤器SocialAuthenticationFilter
社交登录过滤器UsernamePasswordAuthenticationFilter
用户名密码登录过滤器SmsCodeAuthenticationFilter
短信登录过滤器AnonymousAuthenticationFilter
前面过滤器都没校验时匿名验证的过滤器ExceptionTranslationFilter
处理FilterSecurityInterceptor
授权失败时的过滤器FilterSecurityInterceptor
授权过滤器本章我们主要讲解SocialAuthenticationFilter
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//#1.判断用户是否允许授权
if (detectRejection(request)) {
if (logger.isDebugEnabled()) {
logger.debug("A rejection was detected. Failing authentication.");
}
throw new SocialAuthenticationException("Authentication failed because user rejected authorization.");
}
Authentication auth = null;
//#2.获取所有的社交配置providerId(本项目中三个:qq,weixin,weibo)
Set authProviders = authServiceLocator.registeredAuthenticationProviderIds();
//#3.根据请求获取当前的是那种类型的社交登录
String authProviderId = getRequestedProviderId(request);
//#4.判断是否系统中是否配置当前社交providerId
if (!authProviders.isEmpty() && authProviderId != null && authProviders.contains(authProviderId)) {
//#5.获取当前社交的处理类即OAuth2AuthenticationService用于获取Authentication
SocialAuthenticationService> authService = authServiceLocator.getAuthenticationService(authProviderId);
//#6.获取SocialAuthenticationToken
auth = attemptAuthService(authService, request, response);
if (auth == null) {
throw new AuthenticationServiceException("authentication failed");
}
}
return auth;
}
private Authentication attemptAuthService(final SocialAuthenticationService> authService, final HttpServletRequest request, HttpServletResponse response)
throws SocialAuthenticationRedirectException, AuthenticationException {
//获取SocialAuthenticationToken
final SocialAuthenticationToken token = authService.getAuthToken(request, response);
if (token == null) return null;
Assert.notNull(token.getConnection());
//#7.从SecurityContext获取Authentication判断是否认证
Authentication auth = getAuthentication();
if (auth == null || !auth.isAuthenticated()) {
//#8.进行认证
return doAuthentication(authService, request, token);
} else {
//#9.返回当前的登录账户的一些信息
addConnection(authService, request, token, auth);
return null;
}
}
OAuth2AuthenticationService
(用于获取SocialAuthenticationToken
)SecurityContext
获取Authentication
判断是否授权public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException {
//#1. 获取code
String code = request.getParameter("code");
//#2. 判断code值
if (!StringUtils.hasText(code)) {
//#3.如果code不存在则抛出SocialAuthenticationRedirectException
OAuth2Parameters params = new OAuth2Parameters();
params.setRedirectUri(buildReturnToUrl(request));
setScope(request, params);
params.add("state", generateState(connectionFactory, request));
addCustomParameters(params);
throw new SocialAuthenticationRedirectException(getConnectionFactory().getOAuthOperations().buildAuthenticateUrl(params));
} else if (StringUtils.hasText(code)) {
try {
//#4.如果code存在则根据code获得access_token
String returnToUrl = buildReturnToUrl(request);
AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
// TODO avoid API call if possible (auth using token would be fine)
//#5.用access_token获取用户的信息并返回spring Social标准信息模型
Connection connection = getConnectionFactory().createConnection(accessGrant);
//#6.使用返回的用户信息构建SocialAuthenticationToken
return new SocialAuthenticationToken(connection, null);
} catch (RestClientException e) {
logger.debug("failed to exchange for access", e);
return null;
}
} else {
return null;
}
}
code
code
是否存在值code
获取access_token
access_token
返回用户信息(该信息为Spring Social
标准信息模型)SocialAuthenticationToken
private Authentication doAuthentication(SocialAuthenticationService> authService, HttpServletRequest request, SocialAuthenticationToken token) {
try {
if (!authService.getConnectionCardinality().isAuthenticatePossible()) return null;
token.setDetails(authenticationDetailsSource.buildDetails(request));
//#重点熟悉的AuhenticationManage
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;
}
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//#1.一些判断信息
Assert.isInstanceOf(SocialAuthenticationToken.class, authentication, "unsupported authentication type");
Assert.isTrue(!authentication.isAuthenticated(), "already authenticated");
SocialAuthenticationToken authToken = (SocialAuthenticationToken) authentication;
//#2.从SocialAuthenticationToken中获取providerId(表示当前是那个第三方登录)
String providerId = authToken.getProviderId();
//#3.从SocialAuthenticationToken中获取获取用户信息 即ApiAdapter设置的用户信息
Connection> connection = authToken.getConnection();
//#4.从UserConnection表中查询数据
String userId = toUserId(connection);
//#5.如果不存在抛出BadCredentialsException异常
if (userId == null) {
throw new BadCredentialsException("Unknown access token");
}
//#6.调用我们自定义的MyUserDetailsService查询
UserDetails userDetails = userDetailsService.loadUserByUserId(userId);
if (userDetails == null) {
throw new UsernameNotFoundException("Unknown connected account id");
}
//#7.返回已经认证的SocialAuthenticationToken
return new SocialAuthenticationToken(connection, userDetails, authToken.getProviderAccountData(), getAuthorities(providerId, userDetails));
}
public List findUserIdsWithConnection(Connection> connection) {
ConnectionKey key = connection.getKey();
List localUserIds = jdbcTemplate.queryForList("select userId from " + tablePrefix + "UserConnection where providerId = ? and providerUserId = ?", String.class, key.getProviderId(), key.getProviderUserId());
//# 重点conncetionSignUp
if (localUserIds.size() == 0 && connectionSignUp != null) {
String newUserId = connectionSignUp.execute(connection);
if (newUserId != null)
{
createConnectionRepository(newUserId).addConnection(connection);
return Arrays.asList(newUserId);
}
}
return localUserIds;
}
因此我们自定义MyConnectionSignUp
实现ConnectionSignUp
接口后,Spring Social
会插入数据后返回userId
@Component
public class MyConnectionSignUp implements ConnectionSignUp {
@Override
public String execute(Connection> connection) {
//根据社交用户信息,默认创建用户并返回用户唯一标识
return connection.getDisplayName();
}
}
至于OAuth2AuthenticationService
中获取code
和AccessToken
,Spring Social
已经我们提供了基本的实现。开发中,根据不通的服务提供商提供不通的实现,具体可参考以下类图,代码可参考logback项目social
包下面的类。
以上便是使用Spring Social
实现社交登录的核心类,其实和用户名密码登录,短信登录原理一样.都有Authentication
,和实现认证的AuthenticationProvider
。