在上一节,我们的SpringSocialQQ登录成功跑起来了,跳到QQ登录的网站上,但是报错了,我们来一点一点解决问题,直到成功登录
Redirect Url Illegal
Redirect Url是什么
上一节我们开发完,来到这个页面,首先我们配合之前的流程图分析一下完整的QQ登录流程:
用户点击QQ登录 ——》用户扫码或者账号密码登录 ——》登录成功后QQ调用应用回调地址
,将授权码给我们应用 ——》应用拿着授权码请求QQ获取accesstoken
——》换到accesstoken
后再根据它获取openId
——》然后通过openId获取用户信息
这里Redirect Url
就是回调地址
同时我们还要明确redirect uri
不是随意生成的,而是在我们的系统在QQ上进行注册时需要填写的必填信息之一,它的填写有一定的规则,可参考QQ互联官网。这里要注意,官网上明确说明了域名要包含http://
部分。
Redirect Url Illegal的原因
为什么我们会出现这个问题呢?我们先看看请求路径
https://graph.qq.com/oauth2.0/show?which=error&display=pc&error=100010&client_id=100550231&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fauth%2Fqq&state=8f3b0533-e5a7-4dfe-a2b8-94f0119b0ad2
这里重要的参数就是:redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fauth%2Fqq
其实可以很容易地猜出来就是localhost:8080/auth/qq,
这肯定不可能是我们在QQ互联上配置的redirect_uri
,因此当我们的项目拿着这个redirect_uri
去请求QQ的接口时就报出了redirect uri is illegal(100010)
的错误。
同时我们也可以看出redirect_uri
在我们项目中的生成规则是:
1 | IP+端口号(或者域名+端口)+/auth/qq |
其中/auth和/qq我们在上篇文章中讲过它俩分别由SocialAuthenticationFilter
和providerId
控制。
Redirect Url Illegal的解决方式
这里我在QQ互联上配置的地址和回调域如上图,所以我们就要把项目中的配置对应的进行修改
修改访问域名和端口
因为我们是本机测试,所以只需修改host即可
1 | 127.0.0.1 www.pinzhi365.com |
端口修改:
1 | 80 = |
这里为什么改80
就不解释了
修改SocialFilter拦截的路径为qqLogin
我们上一节的最后了解到,SocialAuthenticationFilter
的默认拦截路径为/auth
,如果我们想修改他,应该怎么办呢?SocialAuthenticationFilter
是由SpringSocialConfigurer
构建的,所以我们进源码看看
1 |
|
这里我们可以看到SpringSocialConfigurer
将SpringSocialConfigurer
添加到Security拦截器链上之前,做了一件事postProcess()
,所以我们重新这个方法即可,后置处理,将Filter的拦截路径修改一下
创建MySpringSocialConfigurer
1 | public class MySpringSocialConfigurer extends SpringSocialConfigurer { |
修改SocialConfig
1 |
|
这里我是把拦截路径设置成可配置的,这里要在application.properties
中配置一下
1 | /qqLogin = |
修改providerId
因为ProviderId
之前写在QQProperties
中,所以只需在application.properties
中配置一下
1 | callback.do = |
修改QQ登录按钮请求路径
1 | <a href="/qqLogin/callback.do">QQ登录</a> |
启动测试
成功访问
SignIn错误
这里我们点击登录后,会发现跳转到了认证页面
服务费输出跳转路径为http://www.pinzhi365.com/signin
,又因为我们没有signin
这个页面,并且没有对请求授权,所以会被之前写的Security认证拦截,因为没有登录,如果登录了或者授权放行了,应该会报出404错误
SignIn错误原因源码分析
SpringSocial认证流程
首先我们先看一下SpringSocial
的拦截器执行流程图,这里相较前面学习的认证流程原理其实就多了三个组件:
ConnectionFactory
:需要我们自己实现,在流程里的作用就是通过授权码换取令牌(Connection
)Connection
:需要我们自己实现,令牌,被封装到SocialAuthorizationToken
中保存SocialUserDetailsService
:需要我们自己实现,根据userId
去数据库获取用户信息
通过这个流程,我们可以发现最重要的就是ConnectionFactory
前面的那个组件SocialAuthorizationService
,他是请求第三方服务,获取授权码并使用授权码获取令牌的重要步骤~
我们进入源码一点点断点查看,为何会出现这个错误
AbstractAuthenticationProcessingFilter
首先查看AbstractAuthenticationProcessingFilter
:
1 | public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) |
其实这个代码我们之前都看过,最重要的就是attemptAuthentication()
方法,调用实现类的认证方法进行认证。
SocialAuthenticationFilter
进入到SocialAuthenticationFilter
的attemptAuthentication()
方法
1 | public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { |
断点进去看,可以发现就是我们之前实现的QQ登录的Connection
以及ServiceProvider
OAuth2AuthenticationService
接着我们进入到getAuthToken()
方法中,实现类为OAuth2AuthenticationService
,这段代码非常关键了!!!
1 | public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException { |
这里因为我们的请求路径和第三方回调路径都是一样的,所以会使用授权码来判断:
- 第一次请求,抛出重定向异常,错误处理类就会重定向到第三方认证页面
- 回调请求,使用授权码获取令牌
往下走的时候,走到exchangeForAccess()
即使用授权码获取令牌的时候报错,报了RestClientException
异常
然后return null;
我们知道,如果返回null
,那么肯定是认证失败,会进入失败处理类,这里Social失败处理类是SocialAuthenticationFailureHandler
实现类是SimpleUrlAuthenticationFailureHandler
这里就看到默认重定向URL/signin
的地址了,这也是为什么转跳/signin
的原因
RestClientException异常
找到了转跳/signin
的原因了,接着要看为啥抛出RestClientException
异常了
这里我们直接查看报错方法exchangeForAccess()
,我们使用的是默认的实现类即OAuth2Template
1 | public AccessGrant exchangeForAccess(String authorizationCode, String redirectUri, MultiValueMap<String, String> additionalParameters) { |
这里在代码中可以看到,使用RestTemplate
进行请求获取令牌,然后我们再看看报错信息
1 | Could not extract response: no suitable HttpMessageConverter found for response type [interface java.util.Map] and content type [text/html] |
意思是响应无法进行数据处理,我们看看RestTemplate
的创建方法
1 | protected RestTemplate createRestTemplate() { |
它只加了如下三个HttpMessageConverter
,这三个HttpMessageConverter
并不能处理contentType
为 [text/html]
的返回信息,因此走到上面的postForAccessGrant
方法会抛出RestClientException
异常。
SiginIn错误的解决方式
既然默认的OAuth2Template
使用有问题,那么我们就继承它重新他的方法
1 | public class QQOAuth2Template extends OAuth2Template { |
这里实现好之后,我们还需要将ServiceProvider
中OAuth2Operation
的实现方式改为我们自己的QQOAuth2Template
1 | public QQServiceProvider(String appId, String appSecret) { |
只需这两步,就完成了SignIn
错误的修改
重启测试
控制台正确打印信息,说明获取用户信息成功
然后发现页面又又又返回401需要认证了,查看控制台
成功获取用户信息后,请求signup
这个路径了,但是我们并没有配置过这个请求,所以返回401需要认证
SignUp错误
在之前,我们已经获取到QQ的用户信息,即Connection
对象,那我们是哪里出了错呢?
SignUp错误原因源码分析
之前返回了401
,说明没有认证,就说明认证的地方出了错,我们又知道SpringSecurity
的认证是由Provider
完成的,所以我们应该查看SocialAuthorizationProvider
的源码,跟一下报错信息
通过源码查看,其实很简单
- 首先获取刚刚封装到
Token
中的Connection
信息,即保存的用户信息 - 然后拿着这个用户信息向数据库查询,查关联我们应用的用户
- 问题!我们没有注册逻辑,所以表里肯定没有映射,即查出来的是null
- 因为null,所以抛出
BadCredentialsException
异常
抛出的异常肯定是最上层捕获,我们去SocialAuthorizationFilter
看看
1 | private String signupUrl = "/signup"; |
这里的源码就清晰明了了,捕获异常后判断signupUrl
是否存在,默认为/signup
,所以是存在的,然后抛出重定向异常,让SecurityExceptionFilter
进行重定向处理,转到/signup
页面,因为我们没有这个页面并且没有做授权,所以出现了上面401问题
SignUp问题解决方式
解决方式很简单,只需写一个注册页面,然后再SocialConfig
中配置一下即可,这里我们把注册页面弄成可配置的
创建注册页面
1 |
|
在配置类中添加signup配置
1 | public class BrowserProperties { |
这里是在之前创建的BrowserProperties
中配置,上层是SecurityProperties
,不了解的可以看一下之前SpringSecurity
表单登录的笔记。
配置SpringSocialConfig
在SocialConfig
类中:
1 |
|
添加一个signupUrl()
方法,设置为我们的注册页面路径
这里还有一个要配置的就是添加认证信息
1 | http. |
这时可以启动测试
可以看到,跳转到了标准的注册页面,接着我们写一个自定义的注册页面,并在application.properties
中设置
配置自定义注册页
1 |
|
1 | # 注册页面配置 |
成功配置
剩下的问题
到此,第三方QQ登录的基本逻辑问题已经完成了,剩下还有一些问题我们之后解决,如接下来的注册逻辑,这里不只是简单的向我们的用户表插入一条数据,还包括向userconnection
表保存应用于第三方QQ用户的映射数据