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

相关文章
相关标签/搜索