Spring Security原始碼分析六:Spring Social社交登入原始碼解析

鄭龍飛發表於2019-02-27

Spring Security原始碼分析三:Spring Social實現QQ社交登入Spring Security原始碼分析四:Spring Social實現微信社交登入這兩章中,我們使用Spring Social已經實現了國內最常用的QQ微信社交登入。本章我們來簡單分析一下Spring Social在社交登入的過程中做了哪些事情?(微博社交登入也已經實現,由於已經連續兩篇介紹社交登入,所以不在單開一章節描述)

引言

OAuth2是一種授權協議,簡單理解就是它可以讓使用者在不將使用者名稱密碼交給第三方應用的情況下,第三方應用有權訪問使用者存在服務提供商上面的資料。

Spring Social 基本原理

https://user-gold-cdn.xitu.io/2018/1/17/16102026080b0e2c?w=984&h=1341&f=png&s=45181
https://user-gold-cdn.xitu.io/2018/1/17/16102026080b0e2c?w=984&h=1341&f=png&s=45181

  1. 訪問第三方應用
  2. 將使用者請求導向服務提供商
  3. 使用者同意授權
  4. 攜帶授權碼返回第三方瑩瑩
  5. 第三方應用攜帶授權碼到服務提供商申請令牌
  6. 服務提供商返回令牌
  7. 獲取使用者基本資訊
  8. 根據使用者資訊構建Authentication放入SecurityContext中 如果在SecurityContext中放入一個已經認證過的Authentication例項,那麼對於Spring Security來說,已經成功登入

Spring Social就是為我們將OAuth2認證流程封裝到SocialAuthenticationFilter過濾器中,並根據返回的使用者資訊構建Authentication。然後使用Spring Security驗證邏輯從而實現使用社交登入。

啟動logback斷點除錯;

https://user-gold-cdn.xitu.io/2018/1/17/161020260896af75?w=1138&h=946&f=png&s=352446
https://user-gold-cdn.xitu.io/2018/1/17/161020260896af75?w=1138&h=946&f=png&s=352446

  1. ValidateCodeFilter校驗驗證碼過濾器
  2. SocialAuthenticationFilter社交登入過濾器
  3. UsernamePasswordAuthenticationFilter使用者名稱密碼登入過濾器
  4. SmsCodeAuthenticationFilter簡訊登入過濾器
  5. AnonymousAuthenticationFilter前面過濾器都沒校驗時匿名驗證的過濾器
  6. ExceptionTranslationFilter處理FilterSecurityInterceptor授權失敗時的過濾器
  7. 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;
		}		
	}
	
複製程式碼
  1. 判斷使用者是否允許授權
  2. 獲取系統的允許的社交登入配置資訊
  3. 獲取當前的社交登入資訊
  4. 判斷當前的資訊是否存在系統配置中
  5. 獲取處理社交的OAuth2AuthenticationService(用於獲取SocialAuthenticationToken
  6. 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;
		}
	}
複製程式碼
  1. 獲取code
  2. 判斷當前code是否存在值
  3. 如果不存在則將使用者導向授權的地址
  4. 如果存在則根據code獲取access_token
  5. 根據access_token返回使用者資訊(該資訊為Spring Social標準資訊模型)
  6. 使用使用者返回的資訊構建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));
	}
複製程式碼
  1. 從SocialAuthenticationToken中獲取providerId(表示當前是那個第三方登入)
  2. 從SocialAuthenticationToken中獲取獲取使用者資訊 即ApiAdapter設定的使用者資訊
  3. 從UserConnection表中查詢資料
  4. 呼叫我們自定義的MyUserDetailsService查詢
  5. 都正常之後返回已經認證的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();
    }
}
複製程式碼

時序圖

https://user-gold-cdn.xitu.io/2018/1/17/16102026115ed282?w=2164&h=2254&f=png&s=99832
https://user-gold-cdn.xitu.io/2018/1/17/16102026115ed282?w=2164&h=2254&f=png&s=99832

至於OAuth2AuthenticationService中獲取codeAccessToken,Spring Social已經我們提供了基本的實現。開發中,根據不通的服務提供商提供不通的實現,具體可參考以下類圖,程式碼可參考logback專案social包下面的類。

https://user-gold-cdn.xitu.io/2018/1/17/16102026116a43a7?w=1537&h=1194&f=png&s=88870
https://user-gold-cdn.xitu.io/2018/1/17/16102026116a43a7?w=1537&h=1194&f=png&s=88870

總結

以上便是使用Spring Social實現社交登入的核心類,其實和使用者名稱密碼登入,簡訊登入原理一樣.都有Authentication,和實現認證的AuthenticationProvider

相關文章