Spring Security源碼分析六:Spring Social社交登陸源碼解析

Spring Security源碼分析三:Spring Social實現QQ社交登陸Spring Security源碼分析四:Spring Social實現微信社交登陸這兩章中,咱們使用Spring Social已經實現了國內最經常使用的QQ微信社交登陸。本章咱們來簡單分析一下Spring Social在社交登陸的過程當中作了哪些事情?(微博社交登陸也已經實現,因爲已經連續兩篇介紹社交登陸,因此不在單開一章節描述)java

引言

OAuth2是一種受權協議,簡單理解就是它可讓用戶在不將用戶名密碼交給第三方應用的狀況下,第三方應用有權訪問用戶存在服務提供商上面的數據。git

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驗證邏輯從而實現使用社交登陸。github

啓動logback斷點調試;spring

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會插入數據後返回userIdsession

@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包下面的類。 ide

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源碼分析

相關文章
相關標籤/搜索