在Spring Security源码分析三:Spring Social实现QQ社交登录和Spring Security源码分析四:Spring Social实现微信社交登录这两章中,我们使用
Spring Social
已经实现了国内最常用的微信
社交登录。本章我们来简单分析一下Spring Social
在社交登录的过程中做了哪些事情?(微博
社交登录也已经实现,由于已经连续两篇介绍社交登录,所以不在单开一章节描述)
引言
OAuth2是一种授权协议,简单理解就是它可以让用户在不将用户名密码交给第三方应用的情况下,第三方应用有权访问用户存在服务提供商上面的数据。
Spring Social 基本原理
- 访问第三方应用
- 将用户请求导向服务提供商
- 用户同意授权
- 携带授权码返回第三方莹莹
- 第三方应用携带授权码到服务提供商申请令牌
- 服务提供商返回令牌
- 获取用户基本信息
- 根据用户信息构建
Authentication
放入SecurityContext中
如果在SecurityContext
中放入一个已经认证过的Authentication
实例,那么对于Spring Security
来说,已经成功登录
Spring Social
就是为我们将OAuth2
认证流程封装到SocialAuthenticationFilter
过滤器中,并根据返回的用户信息构建Authentication
。然后使用Spring Security
的验证逻辑从而实现使用社交登录。
启动logback断点调试;
-
ValidateCodeFilter
校验验证码过滤器 -
SocialAuthenticationFilter
社交登录过滤器 -
UsernamePasswordAuthenticationFilter
用户名密码登录过滤器 -
SmsCodeAuthenticationFilter
短信登录过滤器 -
AnonymousAuthenticationFilter
前面过滤器都没校验时匿名验证的过滤器 -
ExceptionTranslationFilter
处理FilterSecurityInterceptor
授权失败时的过滤器 -
FilterSecurityInterceptor
授权过滤器
本章我们主要讲解SocialAuthenticationFilter
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
判断是否授权
OAuth2AuthenticationService#getAuthToken
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
SocialAuthenticationFilter#doAuthentication
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;
}
}
SocialAuthenticationProvider#authenticate
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));
}
- 从SocialAuthenticationToken中获取providerId(表示当前是那个第三方登录)
- 从SocialAuthenticationToken中获取获取用户信息 即ApiAdapter设置的用户信息
- 从UserConnection表中查询数据
- 调用我们自定义的MyUserDetailsService查询
- 都正常之后返回已经认证的SocialAuthenticationToken
UserConnection表中是如何添加添加数据的?
JdbcUsersConnectionRepository#findUserIdsWithConnection
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
。