Spring Security源码分析六:Spring Social社交登录源码解析

2018/01/16 Spring Security

Spring Security源码分析三:Spring Social实现QQ社交登录Spring Security源码分析四:Spring Social实现微信社交登录这两章中,我们使用Spring Social已经实现了国内最常用的QQ微信社交登录。本章我们来简单分析一下Spring Social在社交登录的过程中做了哪些事情?(微博社交登录也已经实现,由于已经连续两篇介绍社交登录,所以不在单开一章节描述)

引言

OAuth2是一种授权协议,简单理解就是它可以让用户在不将用户名密码交给第三方应用的情况下,第三方应用有权访问用户存在服务提供商上面的数据。

Spring Social 基本原理

http://dandandeshangni.oss-cn-beijing.aliyuncs.com/github/Spring%20Security/Spring-Social-Sequence.png

  1. 访问第三方应用
  2. 将用户请求导向服务提供商
  3. 用户同意授权
  4. 携带授权码返回第三方莹莹
  5. 第三方应用携带授权码到服务提供商申请令牌
  6. 服务提供商返回令牌
  7. 获取用户基本信息
  8. 根据用户信息构建Authentication放入SecurityContext中 如果在SecurityContext中放入一个已经认证过的Authentication实例,那么对于Spring Security来说,已经成功登录

Spring Social就是为我们将OAuth2认证流程封装到SocialAuthenticationFilter过滤器中,并根据返回的用户信息构建Authentication。然后使用Spring Security验证逻辑从而实现使用社交登录。

启动logback断点调试;

http://dandandeshangni.oss-cn-beijing.aliyuncs.com/github/Spring%20Security/Spring-Security-chain.png

  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();
    }
}

时序图

http://dandandeshangni.oss-cn-beijing.aliyuncs.com/github/Spring%20Security/Spring-Social-Sequence%20Diagram0.png

至于OAuth2AuthenticationService中获取codeAccessToken,Spring Social已经我们提供了基本的实现。开发中,根据不通的服务提供商提供不通的实现,具体可参考以下类图,代码可参考logback项目social包下面的类。 http://dandandeshangni.oss-cn-beijing.aliyuncs.com/github/Spring%20Security/Spring-Social-Class%20Diagram0.png

总结

以上便是使用Spring Social实现社交登录的核心类,其实和用户名密码登录,短信登录原理一样.都有Authentication,和实现认证的AuthenticationProvider

Show Disqus Comments

Search

    Post Directory