springsocial/oauth2—绑定和解绑处理【QQ绑定异常,微信解绑302/ERR_TOO_MANY_REDIRECTS】

  • Post author:
  • Post category:其他


项目源码地址

https://github.com/nieandsun/security



注意:

源码中已经包含了微信登陆功能☺☺☺



1 事先声明

之前在《

springsocial/oauth2—第三方登陆之QQ登陆7【注册逻辑之mysql里rank为关键字问题解决+springsocial源码解读③】

》这篇文章里我讲到过一种绑定的应用场景,即登陆者第一次用QQ登陆一个网站,假如这个网站没有在程序里“偷偷地”为登陆者创建一个用户并将该用户和QQ的关联关系插入到userconnection表的话,按照springsocial/springsecurity的默认逻辑来讲会跳到一个注册/绑定页面。假如登陆者在这个网站里曾经注册过用户的话,那么他就可以直接输入自己的用户名+密码并点击绑定,来绑定已有账户和QQ之间的关系。

但是本文讲的绑定和解绑并不适用于这个场景,因为本文讲的绑定和解绑是在用户已经登陆的情况下(即用户信息已经存在于session中),对QQ,微信等进行绑定和解绑,而上面的场景下session里并没有用户信息。

这里简单畅想一下我说的这种场景的绑定的实现步骤:

  • 在进入绑定页面前session里已经存了封装了QQ信息的Connection对象
  • 输入用户名+密码后,点击绑定按钮
  • 点击绑定按钮后第一个要做的事其实是用户名+密码登陆
  • 登陆成功后应该在走登陆成功事件之前从session里取出Connection对象和用户名或用户id
  • 利用ProviderSignInUtils工具类将Connection对象+用户名或用户id建立关系并插入到userConnection表 —》完成绑定工作



2 ConnectController简介

ConnectController是springsocial为我们提供的一个专门用于处理绑定和解绑的Controller类。在该类里主要定义了如下几个方法:

  • 获取当前用户所有第三方账号的绑定状态的方法
  • 将当前用户与第三方账号进行绑定的方法
  • 解绑的方法

这个Controller有如下几个特点:

  • (1)该Controller里的方法只提供了数据,并没有提供视图,比如说下面的方法为

    获取当前用户所有第三方账号绑定状态的Controller方法

    ,它虽然向Model对象里写了数据并指定了返回视图的名称 —》connect/status,但并没提供该视图。
	@RequestMapping(method=RequestMethod.GET)
	public String connectionStatus(NativeWebRequest request, Model model) {
		setNoCache(request);
		processFlash(request, model);
		Map<String, List<Connection<?>>> connections = connectionRepository.findAllConnections();
		//将providerIds放到model中---Set<String>
		model.addAttribute("providerIds", connectionFactoryLocator.registeredProviderIds());	
		//将Connection放到model中	
		model.addAttribute("connectionMap", connections);
		//下面返回视图名其实是connect/status----但是springsocial并没有提供该名称的视图
		//需要我们自己写代码实现该视图,并返回给前端特定的数据
		return connectView();
	}
  • (2) 该Controller提供的方法都是基于session的 。
  • (3) 该Controller提供的方法如解绑

    貌似

    只能用form请求(我试着用ajax请求搞了好久,一直没成功!!!)

基于以上3个特点,其实我感觉在真实的项目里,为了灵活应对项目需求,我们完全可以模仿这个ConnectController来写一个适用于我们的项目的Controller类来处理绑定和解绑的业务。但本文仅仅抛砖引玉的讲讲如何利用好springsocial提供的这个ConnectController类来实现绑定和解绑的工作。



3 获取当前用户所有第三方账号的绑定状态



3.1 当前用户所有第三方账号绑定状态绑定状态的处理视图

如2中所说,其实ConnectController提供的

获取当前用户所有第三方账号绑定状态

的方法已经为我们封装好了数据,我们只要写一个视图,将数据接收并返回给浏览器就好了,示例代码如下:

package com.nrsc.security.core.social;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.nrsc.security.utils.ResultVOUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.Connection;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.view.AbstractView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author : Sun Chuan
 * @date : 2019/9/17 23:22
 * Description: 获取当前用户所有第三方账号的绑定状态的视图
 */
@Component("connect/status")
@Slf4j
public class NrscConnectionStatusView extends AbstractView {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    protected void renderMergedOutputModel(Map<String, Object> map, HttpServletRequest request,
                                           HttpServletResponse response) throws Exception {
        //取出所有的Connection对象
        Map<String, List<Connection<?>>> connections = (Map<String, List<Connection<?>>>) map.get("connectionMap");
        //取出所有的providerId
        Set<String> providerIds = (Set<String>) map.get("providerIds");
        log.info("providerIds:{}",providerIds);

        //封装当前用户的第三方账号绑定状态 key为providerId , value为true或false
        Map<String, Boolean> result = new HashMap<>();
        for (String key : connections.keySet()) {
            result.put(key, CollectionUtils.isNotEmpty(connections.get(key)));
        }
        //将当前用户的第三方账号绑定状态返回给浏览器
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(ResultVOUtil.success(result)));
    }
}



3.2 测试

如下图,userconnection表里有如下两条数据(nrsc绑定了QQ,yoyo绑定了微信)。

在这里插入图片描述

若以用户nrsc进行登陆时,调用www.pinzhi365.com/connect,则得到结果如下,证明我们配置的视图起作用了。

在这里插入图片描述



4 将当前用户与第三方账号进行绑定



4.1 绑定微信

需要一个post请求,请求url为/connect/{providerId},以微信为例页面可按照如下方式发送请求

  <form action="/connect/weixin" method="post">
      <button type="submit">绑定微信</button>
  </form>

当ConnectController接收到该post请求后会拼接一个重定向到微信授权页面的url,并根据该url重定向到微信授权页面。用户在授权页面进行扫码授权后微信会利用url中的redirect_uri(回调地址)发送一个get请求回调我们的项目(接收这个回调的Controller也在ConnectController中,有兴趣的可以跟一下源码)—》 接着我们的项目会再去请求微信获取微信用户信息并将其封装成一个Connection对象 —》 然后将Connection对象和session中的用户信息进行关联,并将该关联关系存到userconnection表里(其实这一步已经完成了绑定) —》 接着调用一个视图,视图名为connect/+providerId+Connected(当然spingsocial没提供该视图)。


下面提供一种各个第三方账号统一使用一个绑定和解绑逻辑的方法

  • 绑定成功和绑定失败的处理逻辑
package com.nrsc.security.core.social;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.nrsc.security.utils.ResultVOUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.view.AbstractView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

/**
 * @author : Sun Chuan
 * @date : 2019/9/17 23:41
 * Description: 由于微信,QQ,微博等解绑和绑定都想用这个视图,但providerId不可能一样
 * 所以这里不能直接用@Component注解将其写死
 */
public class NrscConnectView extends AbstractView {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    protected void renderMergedOutputModel(Map<String, Object> map, HttpServletRequest request,
                                           HttpServletResponse response) throws Exception {
        response.setContentType("application/json;charset=UTF-8");
        //如果map(Model对象)里有Connection对象,则表示绑定,否则表示解绑
        if (map.get("connections") == null) {
            response.getWriter().write(objectMapper.writeValueAsString(ResultVOUtil.success("解绑成功")));
        } else {
            response.getWriter().write(objectMapper.writeValueAsString(ResultVOUtil.success("绑定成功")));
        }
    }
}
  • 微信绑定和解绑指定使用上面的逻辑
    /***
     * connect/weixinConnected 绑定成功的视图
     * connect/weixinConnect 解绑成功的视图
     *
     * 两个视图可以写在一起,通过判断Model对象里有没有Connection对象来确定究竟是解绑还是绑定
     */
    @Bean({"connect/weixinConnect", "connect/weixinConnected"})
    //下面的注解的意思是当程序里有名字为weixinConnectedView的bean
    // 我写的默认的weixinConnectedView这个bean不会生效,也就是你可以写一个更好的bean来覆盖掉我的
    @ConditionalOnMissingBean(name = "weixinConnectedView")
    public View weixinConnectedView() {
        return new NrscConnectView();
    }

请读者自行测试。



4.2 QQ绑定 — redirect uri is illegal(100010)错误分析

在进行完微信绑定之后,我试着写了QQ绑定的代码,发现点击QQ绑定时

又出现了 redirect uri is illegal(100010)错误

,追踪源代码发现在进行QQ绑定时拼接的跳向QQ授权页面的url如下:



https://graph.qq.com/oauth2.0/authorize?client_id=100550231&response_type=code&redirect_uri=http%3A%2F%2Fwww.pinzhi365.com%2Fconnect%2Fcallback.do&state=f0a0f313-310a-4c31-8365-2e903740445c



即redirect_uri解码后其实为 :

http://www.pinzhi365.com/connect/callback.do

,但是对于QQ来说,QQ互联上明确说了,其回调域名并不是简单的/www.pinzhi365.com而是http://www.pinzhi365.com/connect/callback.do 这样一整串 — 》 具体规则可以参考我的文章《

springsocial/oauth2—第三方登陆之QQ登陆4【redirect uri is illegal(100010)错误解决方式】

》,当然也可以直接参考

QQ互联官网

但是这个项目在QQ上配置的redirect_uri为

http://www.pinzhi365.com/qqLogin/callback.do

,因此报

redirect uri is illegal(100010)错误

就很好理解了,其实大家可以试一下将QQ绑定拼接的url中的redirect_uri换成

http://www.pinzhi365.com/qqLogin/callback.do

是可以跳转到QQ授权页面的,但是这样QQ回调就无法回调到ConnectController里的方法了。

那该怎么解决这个问题呢?我觉得有如下两种方法:

  • 自己徒手写代码实现绑定的逻辑
  • 在QQ互联上多配置一个redirect_uri —》 下图是QQ互联上关于域名配置的介绍,可以看到redirect_uri 是可以配置多个的。

在这里插入图片描述

到这里不知道大家会不会想那微信为啥没有这个问题呢???

其实很简单,因为微信中让用户指定的不是完整的回调地址,而是域名因此无论是下面这种地址,



https://open.weixin.qq.com/connect/qrconnect?client_id=wxd99431bbff8305a0&response_type=code&redirect_uri=http%3A%2F%2Fwww.pinzhi365.com%2Fconnect%2Fweixin&state=ebb12821-f2bd-4903-b609-9403f429e2ac&appid=wxd99431bbff8305a0&scope=snsapi_login


还是


https://open.weixin.qq.com/connect/qrconnect?client_id=wxd99431bbff8305a0&response_type=code&redirect_uri=http%3A%2F%2Fwww.pinzhi365.com%2Fconnect1111111111111%2Fweixin&state=ebb12821-f2bd-4903-b609-9403f429e2ac&appid=wxd99431bbff8305a0&scope=snsapi_login


都是可以访问到微信的授权页面的。



5 解绑

微信解

绑貌似比较简单

,只需要发送一个url为/connect/{provideId}的delete请求就可以了,但是实际操作后发现还是有不少坑的。



5.1 在页面利用ajax发送delete请求 — 报ERR_TOO_MANY_REDIRECTS错误

我首先想到的是用ajax发送delete请求,于是写了如下页面:

<button onclick="deleteConnect()">微信解绑--无法完成</button>
 <!-- 这样发的delete请求有问题-->
 <script type="text/javascript">
      function deleteConnect() {
          console.log("1111111111111111")
          $.ajax({
              url: "/connect/weixin",
              type: "delete",
              success: function (res) {
              }
          })
      }
  </script>

但是发送请求后跟进到源码里发现会进入一个死循环,最后页面会报出如下错误:

在这里插入图片描述



5.2 利用restlet_client插件发送delete请求 — 302异常

利用restlet_client插件发送delete请求,跟进源码发现虽然不会进入死循环,但是会报302异常,效果如下:

在这里插入图片描述



5.3 使用form形式发送delete请求 — 终于进入解绑成功后的逻辑

其实5.1和5.2这两种方式都已经把userconnection表中的绑定信息给删掉了,但是就是无法跳转到我指定的解绑成功的视图(视图请看本文4.1),直到用了如下方式发送delete请求

 <form action="/connect/weixin" method="post">
     <input id="method" type="hidden" name="_method" value="delete"/>
     <button>微信解绑</button>
 </form>

简单展示一下解绑成功后页面的显示情况:

在这里插入图片描述



版权声明:本文为nrsc272420199原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。