@greensea
2015-01-04T03:16:10.000000Z
字数 3289
阅读 1708
所谓跨域登录指的是,在 youxiamotors.com 域名下登录一次之后,就可以自动在 weiting.fm 等游侠其他域名下登录。
具体来说,当用户在 youxiamotors.com 登录之后,再访问 weiting.fm,就会发现自己已经登录到 weiting.fm 了。本文档将对这一过程的实现进行说明。
为了实现跨域登录,我们需要实现跨域 Cookies 和 AJAX 请求。在默认情况下,Cookies 和 AJAX 是不能跨域访问的。为了实现跨域访问,HTTP 规范中引入了两个 HTTP 头,分别是 Access-Control-Allow-Origin 和 Access-Control-Allow-Credentials。
在发起 AJAX 请求时,如果服务器返回的 HTTP 头中包含了 Access-Ctontrol-Allow-Origin 字段,且值为当前发起 AJAX 请求的域名,那么我们就能够进行跨域 AJAX 访问。
举例来说,我们在 https://weiting.fm 网址下,用 AJAX 请求一个 位于 https://youxiamotors.com 下的 URL,默认情况下是不能成功获取响应的。但如果我们在 youxiamotors.com 侧返回这样一个 HTTP 头:
Access-Control-Allow-Origin: https://weiting.fm
那么我们就能成功获取 youxiamotors.com 的响应了。
使用 Access-Control-Allow-Origin 头虽然可以成功完成 AJAX 请求,但是 AJAX 在发送请求的时候,默认是不会带上客户端的 Cookies 的。而用户身份信息是保存在 Cookies 中的,如果不发送 Cookies,服务器就无法判断用户是否已经登录了。为了让浏览器在跨域请求时也发送 Cookies,我们需要明确地要求浏览器发送 Cookies。
在发送 AJAX 请求的时候,我们需要使用 withCredentials 参数。在 jQuery 中,具体做法如下:
$.ajax({
xhrFields: {
withCredentials: true
},
/** 其他参数... **/
})
这样浏览器在进行跨域请求的时候,就会发送目标域名下的 Cookies。
仅仅告诉浏览器发送用户 Cookies 还不够,我们还需要在服务器上使用 Access-Control-Allow-Credentials 头,用来明确地告诉浏览器可以使用服务器返回的(包含机密内容的)信息。
服务器端需要输出下面的 HTTP 头:
Access-Control-Allow-Credentials: true
目前我们已经拥有了进行跨域登录所需的全部知识,现在开始实现游侠的跨域登录。
首先,用户需要在 youxiamotors.com 域名下进行登录,登录成功后,youxiamotors.com 会给用户返回一个 Cookies,用以记录用户登录信息。如果客户端能够提供这个 Cookies,我们就认为这个客户端是已经登录的用户。
此时,用户仅仅在 youxiamotors.com 域名下是已登录状态,在 weiting.fm 域名下仍然是未登录状态。
当用户访问 weiting.fm 时,如果用户未登录,则页面会发起一个 AJAX 请求,目标页面是 youxiamotors.com/user/login/ajax/corstoken.html。该页面会检查用户是否已经在 youxiamotors.com 登录了,如果已登录,则返回一个 token。
token 中包含当前已登录用户的编号,token 有效期和签名。
紧接着,weiting.fm 的页面将这个 token 发送到 weiting.fm 下的一个页面,该页面会检查 token 是否有效(是否过期、是否被使用过、签名是否正确),如果 token 有效,就到数据库中查找对应编号的用户,并将此用户设为已登录(给客户端返回一个身份 Cookies,表明当前客户端已登录)。
weiting.fm 的页面在发现登录成功后,修改页面上响应的信息(比如,将用户名显示在网页上等)。至此,一次跨域登录就完成了。
上文中提到的登录 token,是实现跨域登录的关键,其中包含三个信息:
为了防止重放攻击,服务端会记录每一个 token 的使用情况。当一个 token 被使用过后,就将这个 token 标记为“已使用”。具体的代码实现是:
假设 token 为 AAA,有效期为 360 秒,那么当这个 token 被使用过之后,我们在 redis 中增加一个名为 token_used_AAA 的 KEY,有效期为 360 秒。
在使用一个 token 前,应该检查这个 token 是否已经被使用过——只要到 redis 中查询即可。
由于每一个 token 都有一个签名,故伪造 token 是不可能的。
我们使用 HMAC-SHA256 对 token 进行签名。服务器上保存这一个 sign_ken,作为签名密钥。
如果攻击者想要获取 token,则必须获取 youxiamotors.com 下的用户 Cookies。在默认情况下,浏览器会对跨站 Cookies 进行保护,攻击者无法获取用户 Cookies,故无法获取登录 token。
目前的代码中实现了一个 CORS Helper,用于实现跨域登录。
该助手的设计思想是,提供生成 token 和校验 token 的方法,用户可以在 token 中添加自己希望传输给另一个域名的数据(如已登录的用户编号)。
下面描述一个具体的实现,用户已登录 youxiamotors.com,未登录 weiting.fm。现在用户访问了 weiting.fm 页面,我们需要完成一次自动跨域登录。
$.ajax({
xhrFields: {
withCredentials: true
},
/** 其他参数 **/
});
Access-Control-Allow-Credentials:true
Access-Control-Allow-Origin:https://weiting.fm
/** 其他 HTTP 头和内容 **/
服务端的代码类似:
$cors = importHelper('CORS');
$token = $cors->encode($_SESSION['user']['user_id']); /// 生成一个 token,并附带已登录用户的 user_id 数据
$cors->allowOrigin(); /// 输出 Access-Control-Allow-* 头
echo json_encode(array('token' => $token));
weiting.fm 将得到的登录 token 使用普通的 AJAX 请求发送到 weiting.fm/corslogin.php
页面
corslogin.php 页面检查 token 是否合法,若合法,则从中提取用户编号,将该用户设为已登陆。服务端代码类似:
$cors = importHelper('CORS');
$user_id = $cors->decode($_POST['token']); /// 检查 token 是否有效,并从中提取出携带的数据
if ($user_id) {
$cors->mark($_POST['token']); /// 将这个 token 标记为“已使用”
/// 代码:将编号为 $user_id 的用户设置为已登录
}
else {
/// token 不合法(过期、签名错误等)
}