csrf中间件完整示例代码

zhezhebie

中间件代码:

# app\middleware\CsrfTokenCheck.php
<?php

namespace plugin\acms\app\middleware;

use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;

class CsrfTokenCheck implements MiddlewareInterface
{
    /**
     * 排除的应用
     * @var array
     */
    protected $excludedApps = [];

    /**
     * 构造函数
     */
    public function __construct($excludedApps = [])
    {
        $this->excludedApps = $excludedApps;
    }
    // 需要验证 CSRF 的 HTTP 方法
    protected $methodsToVerify = ['POST', 'PUT', 'PATCH', 'DELETE'];

    // 在 CsrfTokenCheck 中间件中添加:
    public function process(Request $request, callable $handler): Response
    {
        // 当前请求的应用属于排除列表,则忽略
        if (in_array($request->app, $this->excludedApps)) {
            return $handler($request);
        }
        if (in_array($request->method(), ['POST', 'PUT', 'PATCH', 'DELETE'])) {
            $token = $request->header('X-CSRF-TOKEN') ?? '';
            $sessionToken = $request->session()->get('csrf_token');

            if (empty($token) || !hash_equals($sessionToken, $token)) {
                return json(['code' => 419, 'msg' => 'CSRF token invalid'], 320);
            }
        }

        return $handler($request);
    }
}

layout代码:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@yield('title', 'CMS系统 - 内容管理系统')</title>
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <link href="/app/user/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.2.1/css/all.min.css" rel="stylesheet">
    <!-- Markdown解析库 -->
    <link href="/app/acms/css/github-markdown.min.css" rel="stylesheet">
    <link href="/app/acms/css/github.min.css" rel="stylesheet">
    <link href="/app/acms/css/pagination.css" rel="stylesheet">
    @yield('additional-styles')
</head>
<body>
    <!-- 导航栏 -->
    <nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
        <div class="container">
            <a class="navbar-brand fw-bold" href="/app/acms">CMS系统</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNavDropdown">
                <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                    <li class="nav-item">
                        <a class="nav-link {{ strpos(request()->uri(), '/app/acms') === 0 && request()->uri() === '/app/acms' ? 'active' : '' }}" href="/app/acms">首页</a>
                    </li>
                    @isset($categoryTree)
                    @foreach($categoryTree as $navCategory)
                    @if(isset($navCategory['children']) && count($navCategory['children']) > 0)
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle {{ (isset($category) && $category->id == $navCategory['id']) || (isset($article) && $article->category_id == $navCategory['id']) ? 'active' : '' }}" href="/app/acms/list?category_id={{$navCategory['id']}}" id="navbarDropdown{{$navCategory['id']}}" role="button" data-toggle="dropdown" aria-expanded="false">
                            {{$navCategory['name']}}
                        </a>
                        <ul class="dropdown-menu" aria-labelledby="navbarDropdown{{$navCategory['id']}}">
                            @foreach($navCategory['children'] as $child)
                            <li><a class="dropdown-item" href="/app/acms/list?category_id={{$child['id']}}">{{$child['name']}}</a>
                            </li>
                            @endforeach
                        </ul>
                    </li>
                    @else
                    <li class="nav-item">
                        <a class="nav-link {{ (isset($category) && $category->id == $navCategory['id']) || (isset($article) && $article->category_id == $navCategory['id']) ? 'active' : '' }}" href="/app/acms/list?category_id={{$navCategory['id']}}">{{$navCategory['name']}}</a>
                    </li>
                    @endif
                    @endforeach
                    @endisset
                </ul>
                <form class="d-flex" action="/app/acms/list" method="GET">
                    <div class="input-group">
                        <input class="form-control" type="search" name="keyword" placeholder="搜索文章..." aria-label="搜索" value="{{ $params['keyword'] ?? '' }}">
                        <button class="btn btn-primary" type="submit">
                            <i class="fas fa-search"></i>
                        </button>
                    </div>
                </form>
                <div class="d-flex align-items-center ms-3">
                    @if(session('user'))
                    <div class="nav-item dropdown">
                        <a class="dropdown-toggle text-secondary" href="#" role="button" data-toggle="dropdown">
                            <img src="{{ session('user.avatar') }}" class="rounded me-2" height="40px" width="40px" />{{ session('user.nickname') }}
                        </a>
                        <ul class="dropdown-menu dropdown-menu-end">
                            <li><a class="dropdown-item" href="/app/user">会员中心</a></li>
                            <li>
                                <hr class="dropdown-divider">
                            </li>
                            <li><a class="dropdown-item" href="/app/user/logout">退出</a></li>
                        </ul>
                    </div>
                    @else
                    <a href="/app/user/login" class="btn btn-primary me-2">登录</a>
                    @if(($setting['register_enable'] ?? true))
                    <a href="/app/user/register" class="btn btn-outline-primary">注册</a>
                    @endif
                    @endif
                </div>
            </div>
        </div>
    </nav>

    <!-- 内容区域 -->
    <div class="container my-4">
        <div class="row">
            <!-- 主内容区 -->
            <div class="col-lg-8">
                @yield('content')
            </div>

            <!-- 侧边栏 -->
            <div class="col-lg-4">
                @section('sidebar')
                <!-- 分类列表 -->
                @isset($categories)
                <div class="card mb-4">
                    <div class="card-header bg-white">
                        <h5 class="mb-0">分类列表</h5>
                    </div>
                    <div class="card-body">
                        <ul class="list-unstyled">
                            @foreach($categories as $sidebarCategory)
                            <li class="sidebar-item">
                                <a href="/app/acms/list?category_id={{$sidebarCategory->id}}" class="text-decoration-none text-dark {{ (isset($category) && $category->id == $sidebarCategory->id) || (isset($article) && $article->category_id == $sidebarCategory->id) ? 'fw-bold text-primary' : '' }}">
                                    <i class="fa fa-folder-open"></i>
                                    {{$sidebarCategory->name}}
                                    <span class="badge bg-light text-dark ms-2"><i class="fa fa-file-alt me-1"></i>{{$sidebarCategory->articles_count ?? 0}}</span>
                                </a>
                            </li>
                            @endforeach
                        </ul>
                    </div>
                </div>
                @endisset

                <!-- 标签云 -->
                @isset($tags)
                <div class="card">
                    <div class="card-header bg-white">
                        <h5 class="mb-0">标签云</h5>
                    </div>
                    <div class="card-body">
                        <div class="tag-cloud">
                            @foreach($tags as $sidebarTag)
                            <a href="/app/acms/list?tag_id={{$sidebarTag->id}}" class="tag text-decoration-none">
                                <i class="fa fa-tag"></i> {{$sidebarTag->name}}
                                @if(isset($sidebarTag->articles_count))
                                <span style="font-size:0.8em;opacity:0.85;">({{$sidebarTag->articles_count}})</span>
                                @endif
                            </a>
                            @endforeach
                        </div>
                    </div>
                </div>
                @endisset
                @show
            </div>
        </div>
    </div>

    <script src="/app/user/js/jquery.min.js"></script>
    <script src="/app/user/js/bootstrap.bundle.min.js"></script>
    <!-- Markdown解析库 -->
        if (typeof jQuery !== 'undefined') {
            $(document).ajaxSend(function(event, xhr, options) {
                // 仅对 POST/PUT/PATCH/DELETE 方法添加 CSRF Token
                if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(options.type)) {
                    xhr.setRequestHeader('X-CSRF-TOKEN', $('meta[name="csrf-token"]').attr('content'));
                }
            });
        }

    </script>
    @yield('scripts')
</body>
</html>

中间件配置:

<?php

use plugin\acms\app\middleware\CsrfTokenCheck;
use plugin\admin\api\Middleware as AdminMiddleware;
use plugin\user\api\Middleware as UserMiddleware;
return [
    '' => [
        new UserMiddleware(['admin']),
        new CsrfTokenCheck(['admin']),
    ],
    'admin' => [
        AdminMiddleware::class
    ]
];

效果截图:

截图

139 3 1
3个评论

10bang

少了方法csrf_token()的示例

zhezhebie

webman官网论坛再次编辑有bug,截图

<?php
if (!function_exists('csrf_token')) {
    function csrf_token()
    {
        $token = md5(uniqid('', true));
        \request()->session()->set('csrf_token', $token);
        return $token;
    }
}

if (!function_exists('csrf_field')) {
    function csrf_field()
    {
        return '<input type="hidden" name="_token" value="' . csrf_token() . '">';
    }
}
  • 暂无评论
路过人间

这种对传统模板页面,这样做没问题,如果是spa单页或前后端分离的项目,建议将csrf_token 写到cookie或header头部,然后每次请求自动带着走。我自己是产生csrf和校验csrf是分开的:

/**
 * CSRF Token 生成 / 同步中间件(SPA 友好版)
 *
 * 设计说明(非常重要):
 *
 * 1. CSRF Token 是「会话级别」的安全标识,而不是一次性验证码
 * 2. 在 SPA 场景下,Token 必须在 Session 生命周期内保持稳定
 * 3. 本中间件【只负责】:
 *    - 在 Session 中生成 Token(若不存在)
 *    - 将 Token 同步到 Cookie(XSRF-TOKEN)
 *
 * 4. 本中间件【不负责】:
 *    - 校验 CSRF(应由独立的 Verify 中间件完成)
 *    - 每次请求刷新 Token(禁止!会导致并发请求失败)
 *
 * 前后端 CSRF 工作流:
 *
 * 请求前:
 *   - 后端生成 CSRF Token(仅首次)
 *   - 写入 Cookie:XSRF-TOKEN(非 HttpOnly)
 *   - Axios 从 Cookie 读取,并写入 Header:X-CSRF-TOKEN
 *
 * 请求中:
 *   - 后端校验 Header 中的 X-CSRF-TOKEN
 *   - 与 Session 中的 csrf_token 比对
 *
 * 失效时:
 *   - 返回 419(或 403)
 *   - 前端可选择重新拉取 Token 或引导重新登录
 */
  • 暂无评论

zhezhebie

680
积分
0
获赞数
0
粉丝数
2023-03-30 加入
🔝