在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<String> 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<S> 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<String> findUserIdsWithConnection(Connection<?> connection) {
ConnectionKey key = connection.getKey();
List<String> 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
。