如何在 CodeIgniter 4 中防止用户多设备重复登录

来源:开发教程作者:上海网站建设头衔:草根站长
导读:本期聚焦于上海网站建设创作的《如何在 CodeIgniter 4 中防止用户多设备重复登录》,敬请观看详情。当同一个账号可以在不同设备上同时保持登录状态时,账户安全风险会明显上升。一旦某个设备丢失或被盗,攻击者可能会利用仍有效的会话继续操作。CodeIgniter 4 提供了灵活的会话与过滤器机制,开发者可以通过存储会话标识并实时校验来强制实现单设备登录。本文从用户表字段设计、登录与校验逻辑、过滤器实现、基于数据库会话驱动或 Redis 的多端踢出方案等多个角度,给出可直接落地的代码示例,并探讨令牌轮换、过期策略与安全注意事项,帮助你在不影响用户体验的前提下有效限制并发登录。

不少应用在默认情况下允许同一个账号在多台设备上同时保持登录状态,这虽然给用户带来了便利,但也埋下了安全隐患。例如某台设备被他人借用或丢失后,只要会话未过期,对方就能继续以合法身份访问账户数据。要解决这个问题,核心思路是在用户登录后生成一个唯一的会话标识并存入数据库,之后每次请求都校验当前会话携带的标识是否与数据库记录一致。如果发现不一致,说明该账号在其他设备触发了新的登录,当前会话应当被强制终止。CodeIgniter 4 提供的会话服务和过滤器机制可以很方便地实现这一逻辑。

如何在 CodeIgniter 4 中防止用户多设备重复登录

实现单设备登录的核心步骤

最直接且易于维护的做法是在用户表中增加一个字段,用来记录当前有效会话的令牌。当用户成功登录时,系统生成一个随机且足够长的字符串作为令牌,并同时写入数据库和当前会话中。之后每次请求,都通过过滤器对比会话中的令牌与数据库中的令牌。一旦二者不匹配,就立即销毁当前会话并跳转到登录页。

首先需要修改用户表结构。以 MySQL 为例,可以在迁移文件中添加一个 session_token 字段。该字段建议使用 VARCHAR(64) 类型,并设置默认值为 NULL,这样可以区分尚未登录的用户。同时为了查询效率,可以给这个字段加上索引,但考虑到它只在登录和校验时使用,高频更新,通常不建索引也可以接受。

<?php

namespace App\Database\Migrations;

use CodeIgniter\Database\Migration;

class AddSessionTokenToUsers extends Migration
{
    public function up()
    {
        $this->forge->addColumn('users', [
            'session_token' => [
                'type'       => 'VARCHAR',
                'constraint' => 64,
                'null'       => true,
                'default'    => null,
            ],
        ]);
    }

    public function down()
    {
        $this->forge->dropColumn('users', 'session_token');
    }
}

在登录控制器中,验证用户凭据通过后,生成一个新的随机令牌,更新数据库,并将用户 ID 和令牌写入会话。这里使用 bin2hex(random_bytes(32)) 生成 64 位十六进制字符串,足够安全且便于存储。同时,旧设备上的会话因为令牌未更新,会在下一次请求时被过滤器拦截并退出。

public function attemptLogin()
{
    $email = $this->request->getPost('email');
    $password = $this->request->getPost('password');
    $userModel = new \App\Models\UserModel();
    $user = $userModel->where('email', $email)->first();

    if ($user && password_verify($password, $user['password_hash'])) {
        $newToken = bin2hex(random_bytes(32));
        $userModel->update($user['id'], ['session_token' => $newToken]);
        session()->set('user_id', $user['id']);
        session()->set('session_token', $newToken);
        return redirect()->to('/dashboard')->with('success', '登录成功');
    }
    return redirect()->back()->with('error', '邮箱或密码错误');
}

这种方式的优点在于实现简单、逻辑清晰,而且不需要改动 CodeIgniter 的会话存储机制。缺点是每次请求都需要查询一次用户表,在高并发场景下可能会给数据库带来额外压力。不过对于大多数中小型应用来说,这种开销完全在可接受范围内。如果后续性能成为瓶颈,可以切换为从 Redis 或缓存中读取令牌,核心思想不变。

用过滤器校验会话令牌

为了确保令牌校验覆盖所有需要登录的页面,最合适的方式是实现一个自定义过滤器。CodeIgniter 4 的过滤器可以在控制器执行前运行,我们可以在 before 方法中读取会话里的 user_idsession_token,然后查询数据库中的最新令牌。如果不一致或者用户已被删除,就销毁会话并重定向。

创建过滤器文件 app/Filters/SingleSessionFilter.php。在 before 方法中,先判断会话中是否存在用户标识和令牌。若不存在,通常说明用户尚未登录,可以不做处理,交给后续的登录检查或者其他过滤器处理。若存在,则从数据库加载用户记录,并比较令牌。一旦发现不匹配,说明当前会话已经在别处被替换,立即销毁会话并跳转。

<?php

namespace App\Filters;

use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;

class SingleSessionFilter implements FilterInterface
{
    public function before(RequestInterface $request, $arguments = null)
    {
        if (!session()->has('user_id') || !session()->has('session_token')) {
            return;
        }

        $userModel = new \App\Models\UserModel();
        $user = $userModel->find(session()->get('user_id'));

        if (!$user || $user['session_token'] !== session()->get('session_token')) {
            session()->destroy();
            return redirect()->to('/login')->with('error', '您的账号已在其他设备登录,请重新登录');
        }
    }

    public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
    {
        // 无需处理
    }
}

接下来需要在 app/Config/Filters.php 中注册这个过滤器,并将其应用到需要保护的路由组。例如,可以创建一个名为 single-session 的别名,然后在路由文件中为所有需要登录的地址添加这个过滤器。也可以直接将其挂载到全局过滤器,但那样会影响到所有请求,包括静态资源,所以建议仅在需要认证的分组中使用。

// app/Config/Filters.php
public $aliases = [
    'csrf'     => \CodeIgniter\Filters\CSRF::class,
    'toolbar'  => \CodeIgniter\Filters\DebugToolbar::class,
    'honeypot' => \CodeIgniter\Filters\Honeypot::class,
    'single-session' => \App\Filters\SingleSessionFilter::class,
];

public $filters = [
    'single-session' => ['before' => ['dashboard', 'account/*']],
];

使用过滤器的方式让所有受保护的路由自动执行校验,代码集中且易于维护。如果后续需要调整校验逻辑,只需要修改过滤器即可,不会影响业务控制器。此外,这种方案也支持在过滤器中进行更细粒度的处理,比如记录日志、发送通知等。

基于会话驱动或 Redis 的更灵活方案

除了在用户表中增加字段,还可以利用 CodeIgniter 4 的数据库会话驱动来管理所有活动会话。这种方式需要在 app/Config/App.php 中将 sessionDriver 设置为数据库驱动,并确保会话表结构已经创建。然后通过会话表记录每个会话所属的用户 ID,登录后删除该用户的其他会话记录,从而实现单设备限制。这种方案的好处是不用修改用户表,并且可以统一管理会话状态。

实现时,在登录成功后获取当前会话 ID(session_id()),然后执行一条 SQL 语句,从会话表中删除所有属于该用户但会话 ID 不等于当前的记录。同时,需要把当前会话的用户 ID 写入会话表,以便后续识别。由于 CodeIgniter 默认的会话表结构不包含用户 ID 字段,需要手动添加该列,或者在 data 字段中存储用户 ID 并解析,但前者更直接。

public function login()
{
    // 假设已通过密码验证,$userId 为用户 ID
    $userId = $user['id'];
    $newSessionId = session_id();

    $db = \Config\Database::connect();
    $builder = $db->table('ci_sessions');
    // 删除其他会话
    $builder->where('user_id', $userId);
    $builder->where('id !=', $newSessionId);
    $builder->delete();

    // 更新当前会话的用户 ID
    $builder = $db->table('ci_sessions');
    $builder->where('id', $newSessionId);
    $builder->update(['user_id' => $userId]);
}

如果应用部署在分布式环境或者需要更高性能,可以考虑使用 Redis 存储登录令牌。将每个用户当前有效的令牌写入 Redis,键名设计为 user_token:{userId},值就是令牌字符串。登录时覆盖旧值,请求校验时从 Redis 读取并比较。这样避免了频繁查询关系型数据库,响应速度更快。但需要注意的是 Redis 数据可能因重启而丢失,因此必须设置合理的过期时间,并在 Redis 不可用时回退到数据库验证。

无论采用哪种方案,都应当考虑令牌轮换与过期策略。令牌不应无限期有效,可以结合会话过期时间,定期或在敏感操作前要求用户重新验证。另外,强制单设备登录可能会给用户带来不便,比如在不同设备间切换时需要反复登录。更好的做法是提供“记住此设备”的选项,或者在旧会话被踢出时给出友好提示,引导用户重新登录。安全性与用户体验需要平衡,但核心目标仍然是保护账户不被未授权访问。

CodeIgniter 4重复登录会话管理修改时间:2026-08-26 09:46:04

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。