整体流程
这个流程与SpringSecrity的核心原理是一致的:
- 拿一个过滤器去拦截特定的url请求,
- 然后把做身份验证所需要的各种信息包装到一个Authentication的实现中,(此时设置未为未认证的)
- 然后到这个Authentication交给AuthenticationManger,AuthenticationManger根据传递进来的Authentication类型的不同,选择一个合适的Provider来处理传递进来的校验信息(support方法)
- 处理过程中会调用我们自己写的userDeataiService来获取业务系统中的用户信息,然后把信息封装到userDeatils接口的实现中,
- 然后经过一系列的检查与样验,如果都通过了,
- 就把用户信息放到这个Authentication里面,然后把这个Authentication标记为经过认证的,
- 然后把它放到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.配置过滤器
- 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);
查看这个类的源码,主要干了几件事
- 生成SocialAuthenticationFilter,并添加各种必要的url
- 把过滤器加入过滤器链中
- 添加SocialAuthenticationProvider用来做认证
- 在添加到过滤链之前,有一个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;
}
}
这个方法主要做了两件事:
- 获得token
- 拿着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码客户端怎么处理?
- 如哪里来接收这个code? 怎么接收?
找了很多次,也没有找到针对这个的code - 哪里来拼接这个Url?
一开始感觉应该是resetTemlate中应该有,但是发现只有发送的request的,并没有针对回应的。 - 哪里发送这个个带code的url?
如果只从oatuh2的流程上来讲,感觉最应该出现在这里,但是resetTemlate中只有拿token换acess的,并没有拿code的方法。
查了大量资料,虽然没有找到答案,但感觉找到了真相。应该是服务器端得到code之后,以固定的格式放在redirect_uri后面,然后服务端发送请求,请求的url就是redirect,这个时候就类似于一个客户端发送一个http请求,我们hostname来接收请求,这样我们的程序就会自动对这个请求进行一系列处理,整个流程就串了起来。
突然感觉设计好巧妙,如果再写方法再去拼接,与这个比起来,确实有点Lower了。
如何做认证
- 拿到code换取token,即AccessGrant,包括token,过期时间等
- 根据AccessGrant拿到connection,connection对象封闭了服务商的连接信息,如wechat,qq,weibo是不同的服务商,因此连接信息肯定不一样。
- 创建一个未被认证的SocialAuthenticationToken
super.setAuthenticated(false);
- 做认证
SocialAuthenticationFilter#doAuthentication
- 设置一些必要的detail之后,设置这个为认证过的authentcion对象
auth.setAuthenticated(true);
- 返回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
它的主要流程
this.autowireBeanFactory.initializeBean
- invokeAwareMethods
- applyBeanPostProcessorsBeforeInitialization
- invokeInitMethods
- applyBeanPostProcessorsAfterInitialization
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;
}