SKILL.md
只读
名称
laravel-security
描述
Laravel 安全最佳实践 — 包含身份验证、权限授权、Eloquent 操作安全、CSRF 与 XSS 防御、API 安全以及生产环境安全部署配置。
Laravel 安全最佳实践
全面实用的 Laravel 应用安全指南,帮助项目防御常见漏洞与安全风险。
触发条件
- 配置 Laravel 身份验证与权限授权(如 Sanctum、Passport、Jetstream、Breeze)
- 实现用户角色、权限控制(Permissions)与策略(Policies)
- 配置生产环境安全参数与环境变量
- 审查 Laravel 项目中的安全漏洞与隐患
- 将 Laravel 应用部署至生产环境
- 编写安全的 Eloquent 查询与数据库迁移脚本
生产环境安全配置
核心生产环境设置
// config/app.php
'env' => env('APP_ENV', 'production'),
'debug' => (bool) env('APP_DEBUG', false), // 致命隐患:生产环境绝不能设为 true
'key' => env('APP_KEY'), // 必须设置:php artisan key:generate
// config/session.php
'secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => true,
'same_site' => 'lax',
// 应用启动时校验 APP_KEY 是否存在
// bootstrap/app.php 或 ServiceProvider 中
if (empty(config('app.key'))) {
throw new RuntimeException('APP_KEY 未设置,请执行:php artisan key:generate');
}
环境变量文件安全
# 严禁将 .env 提交至版本控制
# .gitignore 默认已包含 .env
# 请改为使用带占位符的 .env.example
DB_PASSWORD=
APP_KEY=
SANCTUM_TOKEN_PREFIX=
# 应用启动时校验必需的环境变量
// 在 AppServiceProvider::boot() 中
$requiredKeys = ['app.key', 'database.connections.mysql.database', 'database.connections.mysql.username'];
foreach ($requiredKeys as $key) {
if (empty(config($key))) {
throw new RuntimeException("缺少必需的配置项: {$key}");
}
}
强制 HTTPS 传输
// AppServiceProvider::boot() 或中间件中
if (app()->environment('production')) {
URL::forceScheme('https');
request()->server->set('HTTPS', 'on');
}
// config/app.php 配置可信代理(负载均衡器)
// 请指定具体 IP 段 — 设为 * 会信任所有来源,导致 X-Forwarded-* 头部易被伪造
// AWS 示例: '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'
'trusted_proxies' => ['10.0.0.0/8', '172.16.0.0/12'],
// 在生产环境中通过中间件强制重定向至 HTTPS
// app/Http/Middleware/ForceHttps.php
public function handle($request, Closure $next)
{
if (!$request->secure() && app()->environment('production')) {
return redirect()->secure($request->getRequestUri());
}
return $next($request);
}
身份验证
Sanctum (API Token 身份验证)
// config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
env('APP_URL') ? ',' . parse_url(env('APP_URL'), PHP_URL_HOST) : ''
)));
'expiration' => 60 * 24, // Token 有效期(分钟),null 表示永不过期
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
// 签发带有权限能力 (Abilities) 的 Token
$token = $user->createToken('api-token', ['read', 'write'])->plainTextToken;
// 在路由中校验 Abilities 权限
Route::middleware('auth:sanctum')->group(function () {
Route::get('/orders', function () {
// 用户必须具备 'read' 权限
abort_unless(Auth::user()->tokenCan('read'), 403);
// ...
})->middleware('abilities:read');
Route::post('/orders', function () {
// 用户必须具备 'write' 权限
abort_unless(Auth::user()->tokenCan('write'), 403);
// ...
})->middleware('abilities:write');
});
密码安全
// config/hashing.php
// 默认哈希算法为 bcrypt,Argon2id 安全强度更高
'bcrypt' => [
'rounds' => env('BCRYPT_ROUNDS', 12), // 增加轮数提高哈希强度
],
'argon' => [
'memory' => 65536,
'threads' => 4,
'time' => 4,
],
// RegisterRequest 中的密码验证规则
public function rules(): array
{
return [
'password' => [
'required',
'confirmed',
Password::min(12)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised(), // 检查密码是否已在泄露库中(haveibeenpwned)
],
];
}
// 限制登录尝试频率(防爆破)
// App\Http\Controllers\Auth\AuthenticatedSessionController
protected function authenticated(Request $request, $user)
{
if ($user->wasRecentlyLockedOut()) {
// 向用户发送可疑登录提醒通知
$user->notify(new SuspiciousLoginNotification($request->ip()));
}
}
Session 会话管理
// config/session.php
'driver' => env('SESSION_DRIVER', 'database'), // 建议 database/redis > file
'lifetime' => env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
'encrypt' => env('SESSION_ENCRYPT', false),
// 用户登录成功后重新生成 Session ID
// App\Http\Controllers\Auth\AuthenticatedSessionController
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate(); // 关键安全步骤:防范 Session 固定攻击 (Session Fixation)
return redirect()->intended(RouteServiceProvider::HOME);
}
// 退出登录时注销 Session
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
权限授权
Gates (门控)
// App\Providers\AuthServiceProvider
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::define('update-post', function (User $user, Post $post): bool {
return $user->id === $post->user_id;
});
Gate::define('publish-post', function (User $user): bool {
return $user->role === 'editor' || $user->role === 'admin';
});
// 使用 before() 实现超级管理员全局越权放行
Gate::before(function (User $user, string $ability): ?bool {
if ($user->role === 'super-admin') {
return true; // 拥有所有权限
}
return null; // 降级回常规权限检查
});
}
// Controller 中使用
public function update(Request $request, Post $post): RedirectResponse
{
Gate::authorize('update-post', $post);
// 或使用: $this->authorize('update-post', $post);
// 或使用: abort_unless(Auth::user()->can('update-post', $post), 403);
// ...
}
Policies (策略类)
// App\Policies\PostPolicy
class PostPolicy
{
use HandlesAuthorization;
public function viewAny(?User $user): bool
{
return true; // 公开列表
}
public function view(?User $user, Post $post): bool
{
return $post->is_published || ($user && $user->id === $post->user_id);
}
public function create(User $user): bool
{
return $user->hasVerifiedEmail(); // 必须先通过邮箱验证
}
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id && $post->created_at->diffInDays(now()) <= 30;
}
public function restore(User $user, Post $post): bool
{
return $user->role === 'admin';
}
public function forceDelete(User $user, Post $post): bool
{
return $user->role === 'super-admin';
}
}
// 在 AuthServiceProvider 中注册
protected $policies = [
Post::class => PostPolicy::class,
];
// Controller 中使用
public function show(Post $post): View
{
$this->authorize('view', $post);
return view('posts.show', compact('post'));
}
// Blade 模板中使用
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">编辑</a>
@endcan
@cannot('update', $post)
<span>你没有权限编辑此文章</span>
@endcannot
中间件权限校验
// 在路由定义中使用中间件
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('can:update,post');
Route::get('/posts/create', [PostController::class, 'create'])
->middleware('can:create,App\Models\Post');
// 自定义角色授权中间件
// app/Http/Middleware/CheckRole.php
class CheckRole
{
public function handle(Request $request, Closure $next, string $role): mixed
{
if (!$request->user() || $request->user()->role !== $role) {
abort(403, '未授权。该区域需要角色权限:' . $role);
}
return $next($request);
}
}
// 在 Kernel 中注册
protected $routeMiddleware = [
'role' => \App\Http\Middleware\CheckRole::class,
];
// 路由组调用
Route::middleware(['auth', 'role:admin'])->group(function () {
Route::get('/admin', [AdminController::class, 'index']);
});
Eloquent 操作安全
批量赋值保护
// 危险做法:$guarded = [] 会导致所有字段都可以被批量赋值
// 生产环境中严禁使用 $guarded = []
// 正确做法:设置可批量赋值字段白名单
final class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
'phone',
'avatar',
];
// 严禁将 'role'、'is_admin'、'is_verified' 等敏感字段写入此处
}
// 正确做法:显式控制请求中被写入的模型字段
public function store(StoreUserRequest $request): RedirectResponse
{
$user = User::create($request->safe()->only([
'name', 'email', 'phone', 'avatar'
]));
// $request->safe() 仅获取验证通过的数据
// 在缺乏验证规则时单独使用 $request->only() 并不安全
}
// 危险做法:直接使用请求所有参数创建记录
User::create($request->all()); // 存在批量赋值攻击风险!
// 推荐做法:使用验证通过的数据进行记录创建
$user = User::create($request->validated()); // 仅传递校验通过的字段
SQL 注入防御
// 正确做法:Eloquent 会自动进行参数绑定预处理
User::where('email', $userInput)->first();
User::whereRaw('email = ?', [$userInput])->first();
// 正确做法:Query Builder 同样会自动进行参数绑定
DB::table('users')->where('email', $userInput)->first();
DB::select('SELECT * FROM users WHERE email = ?', [$userInput]);
// 危险做法:原生 SQL 语句直接内联拼接字符串
DB::select("SELECT * FROM users WHERE email = '{$userInput}'"); // 存在 SQL 注入漏洞!
User::whereRaw("email = '{$userInput}'")->first(); // 存在 SQL 注入漏洞!
// 危险做法:在 whereRaw/orderByRaw 中使用未转义的输入
User::orderByRaw($userInput); // 存在 SQL 注入漏洞!
User::groupByRaw($userInput); // 存在 SQL 注入漏洞!
// 危险做法:在 DB::statement 中直接拼接字符串
DB::statement("INSERT INTO users (email) VALUES ('{$userInput}')"); // 存在 SQL 注入漏洞!
属性类型转换
final class User extends Authenticatable
{
protected $casts = [
'email_verified_at' => 'datetime',
'is_admin' => 'boolean', // 转为布尔型可防止字符串注入
'settings' => 'array', // 自动处理 json_encode/json_decode
'metadata' => 'encrypted:array', // Laravel 11+ 加密类型转换
'password' => 'hashed', // Laravel 10+ 自动进行哈希加密
];
}
Model 安全策略
final class User extends Authenticatable
{
// 从 JSON / API 响应中隐藏敏感属性
protected $hidden = [
'password',
'remember_token',
'two_factor_secret',
'two_factor_recovery_codes',
];
// 仅追加安全的计算属性
protected $appends = ['full_name']; // 安全
// 严禁追加包含敏感信息的计算属性
}
final class Post extends Model
{
// 启用软删除全局作用域
use SoftDeletes;
// 禁止延迟加载以防御 N+1 查询(可选严格模式)
// AppServiceProvider::boot()
// Model::preventLazyLoading(!app()->isProduction());
}
CSRF 防御
默认防护机制
// Laravel 默认通过 VerifyCsrfToken 中间件开启 CSRF 防护
// app/Http/Kernel.php (protected $middlewareGroups['web'])
// 所有 POST/PUT/PATCH/DELETE 表单必须包含 @csrf
<form method="POST" action
<!-- truncated for translation batch; full body continues in source -->






