爱奇艺是如何实现第三方登录无刷新更新页面的

2,157 阅读5分钟

需求背景

最近在研究 iqiyi 的登录流程,发现他们的第三方登录体验挺好的。

在目标页面(以下目标页面均指用户当前浏览的页面)点击登录,弹出登录弹框,选择第三方登录,比如 qq,微信,点击则新开页面。

在新页面中登录完毕,页面会自动关闭,且刚刚那个页面不刷新且自动进行状态变更

首先是页面自动关闭

这个很简单,通过 window.close() 即可实现

但是问题是,第三方登录页一般不会自己在代码中做这件事,毕竟一般登录后是重定向到某个页面(通过 ticket 写入 cookie )而不是关闭当前页面

那么如何关闭呢?其实也很简单,让目标页面执行登录页的关闭即可

newWin = window.open("//open.weixin.qq.com")
// 在某个时机
newWin.close()

至于这个时机,就是登录成功之后。

那么目标页面如何知道登录成功了?

你可能会想说可以利用轮询啊,长连接啊,再或者 websocket 咯

这个是可以实现,但是额外浪费了性能,其实在当前页面(登录完重定向的页面)通过 window.opener 操作目标页面

至于这东西是啥,大家应该都知道,不懂的看 MDN

所以这就要求,登录完我们最后重定向的页面除了写入 cookie 的作用,还有就是操作 window.opener

比如这是 iqiyi 第三方登录后重定向的页面 https://passport.iqiyi.com/oauth/closepage.php?fromurl=&from=29&isNew=0

其页面代码就是以下简单的几行

<html><head><script>

//成功调用
document.domain="iqiyi.com";
window.opener.lib.__callbacks__._oAuthSuccess('http://passport.iqiyi.com/pages/user/success.action');

</script>

</head><body></body></html>

这里需要将 domain 设为一致,才能操作相同 domain 的 window

是的, www.iqiyi.com 的 domain 也被改为了 "iqiyi.com" 了,同样的, iqiyi 的其他子域名比如 https://edu.iqiyi.com/ 等都改写了 domain,使得全站都能顺滑的实现无刷新登录

最后看看代码中的 window.opener.lib.__callbacks__._oAuthSuccess(xxx) ,其实作用也很简单,一是关闭新开页面,二是获取用户信息并更新状态

这样差不多就实现了我们的需求,另外还有一点没提到的是,写入 cookie 的时候,domain iqiyi 写的是二级域名 .iqiyi.com ,这样其他三级域名如 edu.iqiyi.com 才能共用 cookie

PS:在测试过程中发现有 iqiyi 有两个bug(截止 20/04/12),当然,普通用户使用一般不会出现。。

  • 若登录弹框关闭,第三方登录成功后不会自动关闭页面且不进行用户信息请求,这个是由于 _oAuthSuccess 其实是调用的登录窗口 iframe 中的方法
  • 手动调用 _oAuthSuccess 方法,头像变成登录后的默认头像,此时其实并没有登录

那以上方案有什么问题么?

除了全站需要改造 document.domain 外,正常来说是没有问题,但是可能会遇到不能修改 domain 的场景,你要是问我有哪些场景,emmmm...反正我就是为了说我的新方案的,新方案就是 postMessage

我现在能想到的一个,比如最后一个重定向的最后一个页面不是 .iqiyi.com域名,而是其他页面(可能是为了集团内多个产品复用吧),比如 util.baidu.com

www.iqiyi.com

newWin = window.open("//util.baidu.com")
window.addEventListener("message",(evt)=>{
    // 根据信息来源进行特殊处理,避免受其他 postMessage 影响
    if(evt.origin === "https://util.baidu.com"){
        console.log(evt.data)
        newWin.close()
        // or
        // evt.source.close()
    }
})

util.baidu.com

window.opener.postMessage("test","*") //所有

可以在 www.iqiyi.com 中看到输出,且 util.baidu.com 自动关闭

当然该方案也有不好的地方,就是 postMessage 使用不当会导致安全问题,主要有以下2点

  1. postMessage 第二个参数 targetOrigin 设置为 * 可能导致数据泄露
    比如使用了某个不安全的第三方登录页面,其对目标网站进行了重定向,比如变成 www.ihack.com ,第三方登录后通过 postMessage 传递隐私信息,由于设置的是 * ,导致 www.ihack.com 可以收到隐私信息。当然你后面发现这个 ihack 网站不正常也没用,因为数据已经泄露了。
    解决方案就是 targetOrigin 设置为特定的地址比如 edu.iqiyi.como ,这就需要我们将当前页面作为传递到重定向页面
    举例,像第三方登录页传递这样的参数 -- &redirect_uri=https://passport.iqiyi.com?targetOrigin=edu.iqiyi.com , 第三方登录后重定向到 passport 写入 cookie ,再次重定向到 https://util.baidu.com?targetOrigin=edu.iqiyi.com ,再当前页面读取 targetOrigin 参数并传递 postMessage 。整个就是这样的一个过程
    当然在本例中不需要传递隐私信息

  2. 接收 message 时请始终使用 origin 和 source 属性验证发件人的身份
    比如点击打开了用户评论的 url(某个黑客网站),其对目标页面发起 postMessage ,而我们如果在收到 message 时不进行验证就执行危险操作时,可能就会导致安全问题
    另外,如果回调执行安全性依赖于消息的话(比如执行 eval(msg.data)),即使检查通过,还需要对消息格式再一次检查,避免因信任网站收到跨站脚本攻击而导致本网站也遭到攻击 当然在本例中只是简单的关闭新开页面及发送请求,没有什么危险操作,检查下 origin 即可

总结

domain 适用于大部分情景,有全站改造 document.domain 的成本,且最后重定向页面需要是相同的 domain

postMessage 适用于所有情景,对于安全问题需要额外处理

两种方案各有千秋,看就具体的场景啦~

参考文档