FastAdmin作为一款优秀的后台管理框架,默认提供了会员系统的单点登录功能。但在实际开发中,我们经常会遇到前后端分离的项目架构,比如前端使用Vue或React开发,后端仅提供API接口。这种情况下,官方手册提供的配置方式就显得力不从心了。
我最近就遇到了这样一个项目:前端是Vue3+TypeScript开发的管理系统,后端使用FastAdmin提供API接口。按照官方文档配置后,发现会员账号居然可以在多个设备同时登录,这显然不符合业务需求。经过排查发现,问题出在Token管理机制上。
在前后端分离架构中,传统的session机制不再适用,取而代之的是基于Token的身份验证。FastAdmin默认的Token管理存在一个关键缺陷:当用户登录时,系统会生成新的Token,但不会清除旧的Token。这就导致同一个账号可以生成多个有效的Token,自然就能在多个客户端同时保持登录状态。
这个问题在纯后端渲染的项目中可能不明显,因为浏览器会自动管理cookie。但在API接口场景下,每个请求都需要显式携带Token,旧Token如果不及时清除就会一直有效。我在测试时就发现,用同一账号在手机和电脑上登录后,两个设备都能正常操作,这显然是个安全隐患。
解决这个问题的关键就在app/common/library/Auth.php文件中。这个文件负责处理用户认证的核心逻辑,其中的login方法正是我们需要修改的地方。具体位置大概在234行左右,在用户密码验证通过后,直接登录会员之前。
原始代码是这样的:
php复制if ($user->password != $this->getEncryptPassword($password, $user->salt)) {
$user->save(['loginfailure' => $user->loginfailure + 1, 'loginfailuretime' => time()]);
$this->setError('Password is incorrect');
return false;
}
// 直接登录会员
return $this->direct($user->id);
我们需要在直接登录会员之前,先清除该用户已有的所有Token。改造后的代码应该像这样:
php复制if ($user->password != $this->getEncryptPassword($password, $user->salt)) {
$user->save(['loginfailure' => $user->loginfailure + 1, 'loginfailuretime' => time()]);
$this->setError('Password is incorrect');
return false;
}
// 清除之前的token,实现单点登录
Token::clear($user->id);
// 直接登录会员
return $this->direct($user->id);
这行Token::clear($user->id)就是实现单点登录的关键。它会清除该用户之前生成的所有Token,确保同一时间只有一个有效的登录会话。我在实际项目中测试发现,添加这行代码后,当用户在第二个设备登录时,第一个设备的Token就会立即失效,完美实现了单点登录的需求。
可能你会好奇Token::clear方法内部是怎么工作的。我特意去查看了FastAdmin的源代码,发现它的实现其实很直观:
php复制public static function clear($user_id)
{
$tokens = self::where('user_id', $user_id)->select();
foreach ($tokens as $token) {
$token->delete();
}
return true;
}
这个方法会查询数据库中该用户的所有Token记录,然后逐个删除。由于FastAdmin的Token验证是实时查询数据库的,所以一旦Token记录被删除,对应的登录会话就会立即失效。
仅仅改造Auth.php文件还不够,在前后端分离项目中,我们还需要注意以下几点:
我在项目中是这样处理前端请求的:
javascript复制// axios拦截器示例
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
在实际实施过程中,可能会遇到一些意外情况。这里分享几个我踩过的坑:
问题1:Token清除后,其他设备的用户操作会突然中断
问题2:高并发场景下可能出现Token竞争
问题3:移动端APP的特殊情况
虽然这个方案解决了单点登录的问题,但在用户量大的情况下,频繁操作Token表可能会成为性能瓶颈。可以考虑以下优化措施:
我优化后的Token::clear方法大概长这样:
php复制public static function clear($user_id)
{
return self::where('user_id', $user_id)->delete();
}
有些业务场景可能需要更灵活的登录控制,比如:
这种情况下,我们可以扩展Token表,添加device_type等字段,然后改造clear方法:
php复制public static function clear($user_id, $device_type = null)
{
$query = self::where('user_id', $user_id);
if ($device_type) {
$query->where('device_type', $device_type);
}
return $query->delete();
}
这样在登录时就可以根据设备类型选择性清除Token,实现更精细化的控制。