流程
1.用户通过点击“记住密码”,提交登陆表单传递给服务器。
2.服务器生成唯一标识token,保存到数据库
3.写cookie,设置失效时间(这样token就保存到了客户端)
4.如果用户登陆失效,服务器取客户端token进行校验
5.token与服务器保存的token一致,且没有失效,则判定用户登录成功
6.token与服务器保存的token不一致或超过失效时间,则判定用户需要重新登录
方案一
用用户id来作为token,如remember_user=1337,这样等于把用户id暴露了出来。 这样一来,可以不断尝试不同的用户id来登录其他人的账号,甚至可以登录管理员账号,相当危险!!
方案二
使用一个随机字符串作为token。
function generateInsecureToken($length = 20)
{
$buf = '';
for ($i = 0; $i < $length; ++$i) {
$buf .= chr(mt_rand(0, 255));
}
return bin2hex($buf);
}
mt_rant其实并不安全,如果你要生成随机数,可以使用以下几个方案
1.RandomLib[^1]
2.random_bytes($length) (PHP 7, or available in PHP 5 via random_compat[^2])
3.Raw bytes read from /dev/urandom
4.mcrypt_create_iv($length, MCRYPT_DEV_URANDOM)
5.openssl_random_pseudo_bytes($length)
可以这么写
function generateToken($length = 20)
{
return bin2hex(random_bytes($length));
}
这个方案会有时序攻击的风险。 如果你的token=WBWgm2oMFxsiGRGQNJ6n8gtN3gOuQ2wjN8ZRjZtU0Mn 如果你是通过数据库来校验
SELECT * FROM auth_tokens WHERE token = 'WBWgm2oMFxsiGRGQNJ6n8gtN3gOuQ2wjN8ZRjZtU0Mn';
或者通过从数据库取出token,在进程中做简单校验
$getToken = $_GET[‘token’];
if ($getToken == $token) {
//success
} else {
//fail
}
这时有人按以下步骤修改他的cookie
1.第一次把第一个字节从W改为X
2.第二次把最后一个字节从n改为o 第二次的校验时间要比第一次的校验时间要短,这样用户可以不断修改尝试来确认更改的字符是否正确,就是一次时序攻击。参考It’s All About Time. 这个时间差异仅在纳秒级有意义。 这不是一个简单或容易的攻击,但是我们写一个存在风险的进程是完全没有意义的。
方案三
在token中不留下任何有用的信息(甚至服务器时间)给攻击者。 使用selector:validator来取代单一的token,通过selector在数据库中去取出validator,这样可以防止不可避免的时序风险。
从数据库取出token,进程中使用hash_equals()来做校验,因为hash_equals()能够防止时序攻击。
生成token
if ($login->success && $login->rememberMe) { // However you implement it
$selector = base64_encode(random_bytes(9));
$authenticator = random_bytes(33);
setcookie(
'remember',
$selector.':'.base64_encode($authenticator),
time() + 864000,
'/',
'yourdomain.com',
true, // TLS-only
true // http-only
);
$database->exec(
"INSERT INTO auth_tokens (selector, token, userid, expires) VALUES (?, ?, ?, ?)",
[
$selector,
hash('sha256', $authenticator),
$login->userId,
date('Y-m-dTH:i:s', time() + 864000)
]
);
}
验证
if (empty($_SESSION['userid']) && !empty($_COOKIE['remember'])) {
list($selector, $authenticator) = explode(':', $_COOKIE['remember']);
$row = $database->selectRow(
"SELECT * FROM auth_tokens WHERE selector = ?",
[
$selector
]
);
if (hash_equals($row['token'], hash('sha256', base64_decode($authenticator)))) {
$_SESSION['userid'] = $row['userid'];
// Then regenerate login token as above
}
}
实际例子
这里没有把uc记住登陆接口用异步请求去处理,主要因为我们系统并不都在同个主域名下,所以如果要实现全系统统一记住登陆处理,只能跳转到uc,在uc设置cookie。
1.记住登录
sequenceDiagram
participant 系统A
participant UC
系统A-->>UC: 请求异步登录接口校验
UC->>系统A: 返回登录校验结果
Note left of 系统A: 失败则结束流程
系统A-->>UC: 跳转到记住登录接口
Note right of UC: 1.生成token
2.写入数据库
3.设置cookie
UC->>系统A: 跳转回系统A首页
Note left of 系统A: 完成
2.校验登录
sequenceDiagram
participant 系统A
participant UC
系统A-->>UC: 跳转到校验记住登录接口
Note right of UC: 校验cookie及有效期
Note right of UC: 校验失败
UC->>系统A: 跳转回系统A登录页
Note left of 系统A: 完成
Note right of UC: 校验成功
1.生成新token
2.写入数据库
3.设置cookie
UC->>系统A: 跳转回系统A首页
Note left of 系统A: 完成
3.移除记住登录
sequenceDiagram
participant 系统A
participant UC
系统A-->>UC: 跳转到移除记住登录接口
Note right of UC: 1.删除cookie
2.删除数据库token
UC->>系统A: 跳转回系统A首页
Note left of 系统A: 完成
资源