述
上文中,整个QQ登录的流程的代码已经写完了,可以启动项目试一下, 点QQ登录,效果如下:
可以看下QQ的文档, 就是回调地址不合法
这只是一个问题,在运行代码的过程中还会有几个问题,下面来看一下
回调地址
上面的问题就是回调地址不对, 在QQ互联里面,需要在自己应用中去配置一下回调地址,这里要注意,我们QQ登录跳转的请求是 /auth/qq
然后这个地址和回调的地址是一个,就是说登录跳转了 /auth/qq
,那么回调地址也是 /auth/qq
我现在在QQ互联里面配的回调是 http://www.pinzhi365.com/qqLogin/callback.do
,那么我跳转认证和回调的地址都应该是 /qqLogin/callback.do
而不是 /auth/qq
但是在 social 中 /auth
这段地址是 SocialAuthenticationFilter
中写死的, 那我们就需要去把他的配置给覆盖掉,下面看下如何实现
在此之前,还有一件事,就是在本地开发环境中,需要把这个域名映射到本地的项目,这个需要在 host 文件中,加入以下内容1
127.0.0.1 www.pinzhi365.com
把项目的端口改成80
自定义social的url
我们之前在 SocialConfig
中配置了 SpringSocialConfigurer
这个类,然后在浏览器的配置中引入了, 点进去这个类看一下
它里面有一个 configure()
方法,在最后,把 SocialAuthenticationFilter
加到了security的过滤器链上面, 在此之前 调用了一个 postProcess()
方法,去做了一些操作, 我们可以重写这个方法,然后加一些默认的操作
新建一个 MySpringSocialConfigurer
继承 SpringSocialConfigurer
,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class MySpringSocialConfigurer extends SpringSocialConfigurer {
private String filterProcessesUrl;
public MySpringSocialConfigurer(String filterProcessesUrl) {
this.filterProcessesUrl = filterProcessesUrl;
}
@Override
protected <T> T postProcess(T object) {
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filterProcessesUrl);
return (T) filter;
}
}
这里首先重写了一个 postProcess()
方法, 这个object参数其实就是 social 的过滤器, 所以首先拿到父类的处理结果,然后再加入我们自己的 url ,从构造里面传过来
然后 SocialConfig
中,要把之前的 SpringSocialConfigurer
替换成我们自己的 MySpringSocialConfigurer
1 | @Configuration |
这个路径是通过配置传过来的, 在 SocialProperties
中需要加一个属性1
2
3
4/**
* social 要拦截的请求地址
*/
private String filterProcessesUrl = "/auth";
默认还是 /auth ,但是在我们上面配置的回调中,是 /qqLogin
所以需要在配置文件中加入以下配置:1
2
3
4
5
6
7
8core:
security:
social:
filterProcessesUrl: /qqLogin
qq:
app-id: xxx
app-secret: xxxxxxx
providerId: callback.do
先是 filterProcessesUrl
配置成 /qqLogin
然后 providerId
配置为 callback.do
, 这样回调配置就ok了, 在页面中的请求地址也是 /qqLogin/callback.do
1
<a href = '/qqLogin/callback.do'>QQ登录</a>
配置完上面的之后,启动项目,访问 www.pinzhi365.com/login.html ,然后使用QQ登录,效果如下:
现在,就是授权的界面,点击头像登录,这个时候会调回到登录页, 在控制台中会有这样的一段输出1
2019-08-19 18:00:03.851 INFO 10952 --- [p-nio-80-exec-4] c.s.e.web.controller.BrowserController : 引发跳转的请求是:http://www.pinzhi365.com/signin
这里可以看到,他并没有去获取到QQ的用户信息,而是到了一个/signup的请求
登录流程
下面先不去管QQ登录, 先看一下social的登录流程
总体的流程和之前的登录是一样的, 首先是拦截器拦截特定的登录请求, 然后通过 ConnectionFactory
创建一个没有经过认证的 Authentication
对象, 然后就是通过 AuthenticationProvider
去验证,去调用 SocialUserDetailsService
然后校验,最后返回一个已认证的 Authentication
在这个过程中 SocialAuthenticationService
会完成整个 OAuth2 的流程, 他会去调用我们的 ConnectionFactory
让然后里面有 ServiceProvider
等,最后拿到第三方的用户信息之后, 创建出来一个 Connection
对象, 封装成一个 SocialAuthenticationToken
回到上面的问题中, 我们在网站授权完成之后,条转到的是一个 /signup 的请求, 也就是再授权完成回调的时候出的问题,这个时候 OAuth2 的流程还没有走完, 也就是说问题出在 SocialAuthenticationService
中
看一下 OAuth2AuthenticationService
的源码, 这个类是继承 SocialAuthenticationService
的
这个 getAuthToken()
方法就是去获取第三方的授权的, 首先它会判断请求中有没有code这个参数, 因为我们系统中发起第三方登录请求和回调的地址都是一样的 , 所以有 code 参数的话,就是回调的,没有的话就是我们发起登录的
上面是没有 code 参数的处理, 最后会抛出一个异常, social 捕获到这个异常之后,就会重定向到QQ的授权页面
下面的是有 code 参数的处理, 这里会调用我们的 ConnectionFactory
创建 Connection
最后返回 SocialAuthenticationToken
对象
我们授权之后,QQ重定向回来,也就是说, 上面的问题是创建 SocialAuthenticationToken
的过程中出现了问题
在回调的时候打个断点看一下出的问题
如图这里的异常是:1
Could not extract response: no suitable HttpMessageConverter found for response type [interface java.util.Map] and content type [text/html]
这里 QQ 返回的信息的 content type
是 [text/html] 的, 这里是使用 ConnectionFactory
去调用 OAuthOperations
的 exchangeForAccess()
方法出现的问题, 这里的 OAuthOperations
我们使用的是默认的 OAuth2Template
看一下 OAuth2Template
中的 exchangeForAccess()
方法,如下:
这个方法最后调用了一个 postForAccessGrant()
方法
这个方法就是用他自己的 RestTemplate
调用第三方的接口,然后将返回来的数据转成一个 Map 对象,这就需要返回来的数据是一个json格式的数据,contentType 是 application/json ,但是我们上面QQ返回来的是 text/html 的, 所以就会报错,捕获到异常之后返回一个空
在上层的 SocialAuthenticationFilter
中,判断token是否为空, 是空的话也返回空,如下图:
最终,会调用 social 的失败处理器,会重定向到一个默认的登录页面
这个默认的页面就是 /signup
问题定位到之后,看一下如何解决
自定义 OAuth2Template
先看一下 OAuth2Template
中的 createRestTemplate()
方法, 如下:
这里其实就是他自己的 RestTemplate 没有加处理 text/html 的 converter,那么我们需要写一个自定义的 RestTemplate ,让他可以去处理 text/html 的返回
还有一个问题, 他的 RestTemplate 拿到返回值以后,会转成一个Map类型的数据,然后调用了一个 extractAccessGrant()
方法,组成一个 AccessGrant
对象返回,代码如下:
但是在QQ的返回,其实是下面这样的一个字符串:1
access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
然后我们需要把这个拆出来.组合成一个 AccessGrant
对象返回去
最后再看一下 exchangeForAccess()
方法
这里有个属性是 useParametersForClientAuthentication
,只有这个属性是true的时候,请求参数里面才会带上 client_id
和 client_secret
这两个参数是QQ要求必传的,所以我们还需把 useParametersForClientAuthentication
设置成true
好了,要做的功能就是这几个,然后新建一个 QQOAuth2Template
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34@Slf4j
public class QQOAuth2Template extends OAuth2Template {
public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
// 设置 useParametersForClientAuthentication 为true
setUseParametersForClientAuthentication(true);
}
@Override
protected RestTemplate createRestTemplate() {
// 拿到父类创建的结果
RestTemplate restTemplate = super.createRestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}
@Override
protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
log.info("获取accessToken的响应:{}", responseStr);
// 返回格式是 access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
// 拆分组装成 AccessGrant
String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&");
String accessToken = StringUtils.substringAfterLast(items[0], "=");
Long expiresIn = new Long(StringUtils.substringAfterLast(items[1], "="));
String refreshToken = StringUtils.substringAfterLast(items[2], "=");
return new AccessGrant(accessToken, null, refreshToken, expiresIn);
}
}
- restTemplate 中,添加了处理 text/html 的处理器
- 重写了
postForAccessGrant()
自定义解析QQ的返回值 - 构造中设置
useParametersForClientAuthentication
为true
ServiceProvider 修改
在我们的 QQServiceProvider
中把之前的 OAuth2Template
替换成我们自己的1
2
3
4public QQServiceProvider(String appId, String secret) {
super(new QQOAuth2Template(appId, secret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
this.appId = appId;
}
最后启动项目,用QQ登录, 控制台输出如下:1
2
3
4
5
6
7
8
9
10
11
12
13
142019-08-19 22:48:35.253 INFO 3160 --- [p-nio-80-exec-2] c.s.e.c.a.social.qq.api.QQImpl : QQ获取用户信息结果:{
"ret": 0,
"msg": "",
"is_lost":0,
"nickname": "周。",
"gender": "男",
"province": "浙江",
"city": "杭州",
// ... 省略部分信息
}
2019-08-19 22:48:35.282 INFO 3160 --- [p-nio-80-exec-3] c.s.e.c.v.filter.ValidateCodeFilter : 进入验证码校验的filter.....
2019-08-19 22:48:35.296 INFO 3160 --- [p-nio-80-exec-6] c.s.e.c.v.filter.ValidateCodeFilter : 进入验证码校验的filter.....
2019-08-19 22:48:35.316 INFO 3160 --- [p-nio-80-exec-6] c.s.e.web.controller.BrowserController : 引发跳转的请求是:http://www.pinzhi365.com/signup
这里这次会跳转到一个 /signup
的请求上去,就是默认的注册请求,这个之后再说
总结
回顾下登录的流程:
- 访问第三方登录的请求,没有携带code参数
- social 会重定向到第三方的授权页面
- 用户授权完毕之后, 第三方会回调原地址, 并且携带code参数
- social 检测到了code 参数之后, 调用 QQImpl 去交换 accessToken
- 走自定义的
OAuthTemplate
去拿到token - 组装返回一个
AccessGrant
对象 - 获取用户的信息,组装成未认证的
Authentication
对象 - 最后就是 security 的认证流程了