springsecurity -spring social 源码学习

整体流程

在这里插入图片描述
这个流程与SpringSecrity的核心原理是一致的:

  1. 拿一个过滤器去拦截特定的url请求,
  2. 然后把做身份验证所需要的各种信息包装到一个Authentication的实现中,(此时设置未为未认证的)
  3. 然后到这个Authentication交给AuthenticationManger,AuthenticationManger根据传递进来的Authentication类型的不同,选择一个合适的Provider来处理传递进来的校验信息(support方法)
  4. 处理过程中会调用我们自己写的userDeataiService来获取业务系统中的用户信息,然后把信息封装到userDeatils接口的实现中,
  5. 然后经过一系列的检查与样验,如果都通过了,
  6. 就把用户信息放到这个Authentication里面,然后把这个Authentication标记为经过认证的,
  7. 然后把它放到SecurityContext里面,完成整个登陆。

SpringSocial中特殊的地方
1) 在由过滤器封装校验信息Authentication给AuthenticationManger的时候用到了SocialAuthenticaionService这个接口,在我们例子里用到的具体实现是Oauth2AuthenticaitonSerivce,蓝色部分是springsocial封装好的,不需要去动的。橘色的是们自己写的。蓝色部分在执行过程中会调用 我们自己写的代码,如Oauth2AuthenticaitonSerivce会执行整个Oauth流程,在执行过程中会调用ConnectionFactory, ConnectionFactory会拿到ServiceProvider,ServiceProvider的Oauth2Operations也会帮助完成整个流程。完成整个流程后会获得服务提供商的信息,并把这些信息封装到Connection中,Connection会被封装为SocialAuthenticaitonToken, SocialAuthenticaitonToken中包含Connection信息,之后SocialAuthenticaitonToken交给AuthenticationManger,AuthenticationManger会选择SocialAuthenticaitonProvider来处理token. SocialAuthenticaitonProvider在处理过程中会根据connection的信息使用JdbcUserConnectionRepository到数据中查出一个userid,SocialUserDetailService会根据userid查询到SocialUserDetails,然后把用户信息放到SecurityContext中,最后放入session中

1.配置过滤器

  1. spring通过SpringSocialConfigurer这个类来加载对应的fitler
public void configure(HttpSecurity http) throws Exception {		
		ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
		UsersConnectionRepository usersConnectionRepository = getDependency(applicationContext, UsersConnectionRepository.class);
		SocialAuthenticationServiceLocator authServiceLocator = getDependency(applicationContext, SocialAuthenticationServiceLocator.class);
		SocialUserDetailsService socialUsersDetailsService = getDependency(applicationContext, SocialUserDetailsService.class);
		
		SocialAuthenticationFilter filter = new SocialAuthenticationFilter(
				http.getSharedObject(AuthenticationManager.class), 
				userIdSource != null ? userIdSource : new AuthenticationNameUserIdSource(), 
				usersConnectionRepository, 
				authServiceLocator);
		
		RememberMeServices rememberMe = http.getSharedObject(RememberMeServices.class);
		if (rememberMe != null) {
			filter.setRememberMeServices(rememberMe);
		}
		
		if (postLoginUrl != null) {
			filter.setPostLoginUrl(postLoginUrl);
			filter.setAlwaysUsePostLoginUrl(alwaysUsePostLoginUrl);
		}
		
		if (postFailureUrl != null) {
			filter.setPostFailureUrl(postFailureUrl);
		}

		if (signupUrl != null) {
			filter.setSignupUrl(signupUrl);
		}

		if (connectionAddedRedirectUrl != null) {
			filter.setConnectionAddedRedirectUrl(connectionAddedRedirectUrl);
		}

		if (defaultFailureUrl != null) {
			filter.setDefaultFailureUrl(defaultFailureUrl);
		}
		
		http.authenticationProvider(
				new SocialAuthenticationProvider(usersConnectionRepository, socialUsersDetailsService))
			.addFilterBefore(postProcess(filter), AbstractPreAuthenticatedProcessingFilter.class);

查看这个类的源码,主要干了几件事

  1. 生成SocialAuthenticationFilter,并添加各种必要的url
  2. 把过滤器加入过滤器链中
  3. 添加SocialAuthenticationProvider用来做认证
  4. 在添加到过滤链之前,有一个Postprocess方法,可以对现在的fiter做一些定制化的内容

流程分析

主要分析这个SocialAuthenticationFilter

这个类的父类AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter.dofilter
->SocialAuthenticationFilter#attemptAuthentication
->attemptAuthService

private Authentication attemptAuthService(final SocialAuthenticationService<?> authService, final HttpServletRequest request, HttpServletResponse response) 
			throws SocialAuthenticationRedirectException, AuthenticationException {

		final SocialAuthenticationToken token = authService.getAuthToken(request, response);
		if (token == null) return null;
		
		Assert.notNull(token.getConnection());
		
		Authentication auth = getAuthentication();
		if (auth == null || !auth.isAuthenticated()) {
			return doAuthentication(authService, request, token);
		} else {
			addConnection(authService, request, token, auth);
			return null;
		}		
	}	

这个方法主要做了两件事:

  1. 获得token
  2. 拿着token去做认证
    OAuth2AuthenticationService#getAuthToken

如何获得token

public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException {
		String code = request.getParameter("code");
		if (!StringUtils.hasText(code)) {
			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 {
				String returnToUrl = buildReturnToUrl(request);
				AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
				// TODO avoid API call if possible (auth using token would be fine)
				Connection<S> connection = getConnectionFactory().createConnection(accessGrant);
				return new SocialAuthenticationToken(connection, null);
			} catch (RestClientException e) {
				logger.debug("failed to exchange for access", e);
				return null;
			}
		} else {
			return null;
		}

如果没有授权码,则跳到服务商提供的指定的url来重定向,对应Oauth2标准流程的第一步
从代码逻辑上来说,这里是在父类的dofitler对这些异常进行统一处理的,即AbstractAuthenticationProcessingFilter#doFilter中调用了unsuccessfulAuthentication来进行重定向

public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
		if (failed instanceof SocialAuthenticationRedirectException) {
			response.sendRedirect(((SocialAuthenticationRedirectException) failed).getRedirectUrl()); 
			return;
		}
		delegate.onAuthenticationFailure(request, response, failed);
	}

从代码可以看到,这里使用的是request中的redirecturl,重定向后这一次请求的调用链就结束了。

重定向到指定的uri后,需要用户来进行登陆授权,进行下一个请求的fitler过滤器链。

?需要注意的是,当用户进行手动授权后返回的url中是携带code以及scope参数的,以前一直没有理解这个点,不清楚服务器返回的code码客户端怎么处理?

  1. 如哪里来接收这个code? 怎么接收?
    找了很多次,也没有找到针对这个的code
  2. 哪里来拼接这个Url?
    一开始感觉应该是resetTemlate中应该有,但是发现只有发送的request的,并没有针对回应的。
  3. 哪里发送这个个带code的url?
    如果只从oatuh2的流程上来讲,感觉最应该出现在这里,但是resetTemlate中只有拿token换acess的,并没有拿code的方法。

查了大量资料,虽然没有找到答案,但感觉找到了真相。应该是服务器端得到code之后,以固定的格式放在redirect_uri后面,然后服务端发送请求,请求的url就是redirect,这个时候就类似于一个客户端发送一个http请求,我们hostname来接收请求,这样我们的程序就会自动对这个请求进行一系列处理,整个流程就串了起来。
突然感觉设计好巧妙,如果再写方法再去拼接,与这个比起来,确实有点Lower了。

如何做认证

  1. 拿到code换取token,即AccessGrant,包括token,过期时间等
  2. 根据AccessGrant拿到connection,connection对象封闭了服务商的连接信息,如wechat,qq,weibo是不同的服务商,因此连接信息肯定不一样。
  3. 创建一个未被认证的SocialAuthenticationTokensuper.setAuthenticated(false);
  4. 做认证SocialAuthenticationFilter#doAuthentication
  5. 设置一些必要的detail之后,设置这个为认证过的authentcion对象auth.setAuthenticated(true);
  6. 返回Authentication对象,认证结束。

如何自定义

自定义processurl

在SpringSocialConfigurer#configure中可以看到,这里配置了SocialAuthenticationFilter并做了一些初始化配置,最重要的是最后一句,添加这个过滤器到链中,不过有个postProcess方法

http.authenticationProvider(
				new SocialAuthenticationProvider(usersConnectionRepository, socialUsersDetailsService))
			.addFilterBefore(postProcess(filter), AbstractPreAuthenticatedProcessingFilter.class);

如果我们定义一个子类,去重写这个postProcess,就可以实现对filter的定制

	protected <T> T postProcess(T object) {
		SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
		filter.setFilterProcessesUrl(filterProcessesUrl);
		if (socialAuthenticationFilterPostProcessor != null) {
			socialAuthenticationFilterPostProcessor.process(filter);
		}
		return (T) filter;
	}
?默认拦截的是哪个Uri

通过debug可以看到这个是AutowiredBeanFactoryObjectPostProcessor
它的主要流程

  1. this.autowireBeanFactory.initializeBean
    1. invokeAwareMethods
    2. applyBeanPostProcessorsBeforeInitialization
    3. invokeInitMethods
    4. applyBeanPostProcessorsAfterInitialization
  2. this.autowireBeanFactory.autowireBean(object)
怎么自定义?

对html的处理

默认的RestTemplate中并没有对TEXT/HTML进行处理

protected RestTemplate createRestTemplate() {
		ClientHttpRequestFactory requestFactory = ClientHttpRequestFactorySelector.getRequestFactory();
		RestTemplate restTemplate = new RestTemplate(requestFactory);
		List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>(2);
		converters.add(new FormHttpMessageConverter());
		converters.add(new FormMapHttpMessageConverter());
		converters.add(new MappingJackson2HttpMessageConverter());
		restTemplate.setMessageConverters(converters);
		restTemplate.setErrorHandler(new LoggingErrorHandler());
		if (!useParametersForClientAuthentication) {
			List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
			if (interceptors == null) {   // defensively initialize list if it is null. (See SOCIAL-430)
				interceptors = new ArrayList<ClientHttpRequestInterceptor>();
				restTemplate.setInterceptors(interceptors);
			}
			interceptors.add(new PreemptiveBasicAuthClientHttpRequestInterceptor(clientId, clientSecret));
		}
		return restTemplate;
	}

解决方案:
重写这个方法,添加能处理text/html的hander

@Override
	protected RestTemplate createRestTemplate() {
		RestTemplate restTemplate = super.createRestTemplate();
		restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
		return restTemplate;
	}

添加必要的参数client_id,client_secret

默认是不添加的,如果需要添加,可以在子类的构造方法中把这个参数设置为true

public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
		super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
		setUseParametersForClientAuthentication(true);
	}

最后应用自定义的template
由于这个OAuth2Template是服务提供商的对象,因此需要把它添加到服务商的构造函数中

public QQServiceProvider(String appId, String appSecret) {
		super(new QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
		this.appId = appId;
	}
	
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值