初始化

This commit is contained in:
2026-08-08 18:27:38 +08:00
parent efc150c937
commit 060eb79c09
336 changed files with 24613 additions and 15 deletions
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class Admin
{
public function handle(Request $request, Closure $next): Response
{
if (! $request->user()) {
return redirect()->route('admin.login');
}
if (! $request->user()->isAdmin()) {
abort(403, '需要管理员权限');
}
return $next($request);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* 权限点门禁:can:articles.moderate
* 传入多个权限点时,拥有任一即通过(OR 语义)。
*/
class Can
{
public function handle(Request $request, Closure $next, string ...$permissions): Response
{
$user = $request->user();
if (! $user) {
abort(403, '需要登录');
}
foreach ($permissions as $permission) {
if ($user->canPerm($permission)) {
return $next($request);
}
}
abort(403, '无访问权限');
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* 角色门禁:role:super_admin,admin,forum_admin,staff
* 用于「能否进入后台」这类按角色层级的判断。
*/
class Role
{
public function handle(Request $request, Closure $next, string ...$roles): Response
{
$user = $request->user();
if (! $user || ! in_array($user->role, $roles, true)) {
abort(403, '无访问权限');
}
return $next($request);
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* 统一下发 HTTP 安全响应头(纵深防御)。
*
* 设计说明(零构建架构约束):
* - 本站前台使用 Tailwind Play CDNcdn.tailwindcss.com)与内联 tailwind.config
* 因此 script-src / style-src 必须放行该 CDN 与 'unsafe-inline'。
* - 若日后将 Tailwind 改为构建产物并为内联脚本加 nonce,可移除 'unsafe-inline' 收紧 CSP。
* - HSTS 仅在「生产环境 + 已启用 HTTPS」时下发,避免本地 http 下浏览器记忆错误。
*/
class SecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// 禁止 MIME 嗅探(防止被当作可执行内容)
$response->headers->set('X-Content-Type-Options', 'nosniff');
// 防点击劫持(现代浏览器看 frame-ancestors,旧浏览器看 X-Frame-Options
$response->headers->set('X-Frame-Options', 'DENY');
// Referrer 泄露控制
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
// 关闭不必要的浏览器敏感能力
$response->headers->set(
'Permissions-Policy',
"geolocation=(), camera=(), microphone=(), payment=(), usb=(), interest-cohort=()"
);
// 内容安全策略:限制脚本/样式/连接/表单来源,缩小 XSS 影响面
$csp = implode('; ', [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com",
"style-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com",
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
]);
$response->headers->set('Content-Security-Policy', $csp);
// HSTS:仅生产 + HTTPS 时下发
if ($request->isSecure() && app()->environment('production')) {
$response->headers->set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
}
// 禁止代理/浏览器缓存动态页面(含 CSRF token 的表单页)。
// 否则 nginx/浏览器缓存的旧 HTML 会携带过期 _token,提交即触发 419 Page Expired。
// 静态资源由 web 服务器直接服务、不经过本中间件,不受影响。
$response->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate, private');
$response->headers->set('Pragma', 'no-cache');
$response->headers->set('Expires', '0');
return $response;
}
}