本文目录导读:

- 核心策略:登录成功后强制更换 Session ID(最重要)
- 配置层面:修改配置文件(全局加固)
- 高级防护:绑定用户特征(防session劫持)
- 设置会话生命周期与空闲超时
- 应对未登录访问(防止非登录态下的固定)
- 框架版本差异说明
- 总结防线(实战步骤清单)
ThinkPHP 框架本身在会话管理上提供了一定的安全机制,但默认配置并不保证完全免疫会话固定攻击,你需要在项目中主动配置和编码来强化防护。
会话固定攻击的核心是:攻击者先获取/创建一个有效的会话ID,然后诱骗受害者使用这个ID进行登录,从而窃取受害者的登录状态。
以下是针对 ThinkPHP 项目(以 ThinkPHP 6/8 为例,5.x 亦有类似配置)的全面防护方案:
核心策略:登录成功后强制更换 Session ID(最重要)
这是防止会话固定的根本措施,ThinkPHP 提供了 Session::regenerate() 方法。必须在用户登录成功、赋予权限之前调用。
操作位置: 登录控制器方法中。
<?php
declare(strict_types=1);
namespace app\controller;
use think\facade\Session;
use think\Request;
class Login
{
public function doLogin(Request $request)
{
// 1. 验证用户名密码(伪代码)
$user = $this->validateUser($request->param('username'), $request->param('password'));
if ($user) {
// !!!关键步骤 !!!
// 必须在写入用户数据之前,销毁旧会话ID,生成新ID
// 这会强制浏览器更新Cookie,旧的固定ID将失效
Session::regenerate();
// 2. 写入用户登录状态
Session::set('user_id', $user['id']);
Session::set('user_role', $user['role']);
return json(['status' => 1, 'msg' => '登录成功']);
}
return json(['status' => 0, 'msg' => '登录失败']);
}
}
注意: 若使用原生 PHP 会话,等价写法是
session_regenerate_id(true),但 TP 封装后的Session::regenerate()更统一。
配置层面:修改配置文件(全局加固)
在 config/session.php 配置文件中,通过调整参数增加攻击难度。
return [
// 会话ID长度(增加复杂度,防止暴力猜测)
'id_length' => 32,
// 会话ID字符集(建议包含多种字符)
'id_salt' => 'YourRandomStringHere!@#', // 增加ID的随机性
// 会话过期时间(缩短有效窗口,建议30分钟)
'expire' => 1800, // 30分钟
// Cookie 安全属性(重要)
'cookie' => [
// 只允许通过HTTPS传输会话ID,无则留空,有则设为true
'secure' => env('app_debug', false) ? false : true,
// 禁止JavaScript读取Cookie(防止XSS盗取)
'httponly' => true,
// 设置SameSite属性(防止CSRF和部分会话固定变种)
'samesite' => 'Lax', // 可选 'Strict' 或 'Lax'
],
// 会话使用前缀(避免与其他应用冲突)
'prefix' => 'tp_',
// 严格模式:如果客户端传递了未初始化的会话ID,则将其丢弃
'strict_sessions' => true,
];
高级防护:绑定用户特征(防session劫持)
即使攻击者获取了会话ID,如果服务端校验了客户端的特征信息(如 User-Agent、IP),攻击者也无法直接使用。注意: 建议绑定User-Agent,不建议绑定IP(因为手机网络切换IP会导致用户掉线)。
<?php
declare(strict_types=1);
namespace app\middleware;
use think\facade\Session;
use think\Request;
class SessionBind
{
public function handle($request, \Closure $next)
{
// 检查用户是否已登录
if (Session::has('user_id')) {
// 绑定User-Agent(浏览器指纹)
$bindUa = Session::get('bind_ua');
if ($bindUa !== null) {
// 如果User-Agent变了,销毁会话,要求重新登录
if ($bindUa !== md5($request->header('user-agent'))) {
Session::clear();
return redirect('/login')->with('error', '安全校验失败,请重新登录');
}
} else {
// 首次登录时写入绑定信息(建议在登录成功时写入,而非此处)
Session::set('bind_ua', md5($request->header('user-agent')));
}
}
return $next($request);
}
}
建议在 登录成功 时直接写入绑定信息:
// 登录成功后追加这段代码
Session::set('bind_ua', md5($request->header('user-agent')));
设置会话生命周期与空闲超时
即使攻击者拿到了ID,如果长时间未操作,会话应自动过期。
// 登录成功后
session([
'expire' => 1800 // 30分钟无操作则过期
]);
应对未登录访问(防止非登录态下的固定)
如果用户未登录,尽量不要依赖 Session 中的用户数据,或者对未登录的 Session 也设置较短的过期时间。
框架版本差异说明
- ThinkPHP 5.x:
Session::regenerate()在think\facade\Session中存在,配置位于application/config.php或extra/session.php。 - ThinkPHP 6/8: 配置位于
config/session.php,中间件机制更完善(可参考上文中间件写法)。
总结防线(实战步骤清单)
- 登录处: 调用
Session::regenerate()并立即写入用户信息。 - 退出处: 调用
Session::clear()后,再次调用Session::regenerate()防止会话变异。 - Cookie设置: 确保
httponly设为true。 - 强验证: 若业务涉及敏感操作,建议对绑定User-Agent进行校验。
- 日志监控: 检测同一Session在不同IP下频繁登录的异常行为并强制下线。
如果你正在用的是 PHP 8 和 ThinkPHP 8,建议开启框架自带的 Session 初始化中间件,确保所有配置项都被正确加载。