内核框架开发指南

更新于 2026-11-08 15:32:09 24 次浏览

本指南面向 BlogKit 的深度二次开发者,讲解 core/ 内核的实现原理与源码结构。模板标签在主题中的用法见 05-主题开发指南,插件如何使用内核能力见 06-插件开发指南,系统目录全貌见 03-目录与文件说明。

一、内核总览:请求生命周期

BlogKit 是无框架的原生 PHP 应用,不依赖 Composer,也没有依赖注入容器。一个 HTTP 请求的完整处理链路如下:

浏览器请求
   |
   v
入口文件(index.php / admin.php / api.php)
   |  require core/bootstrap.php
   v
bootstrap.php 统一引导
   |  输出缓冲 -> 常量定义 -> 安装检查 -> 自动加载 -> 核心类加载
   |  -> Config 初始化 -> 时区 -> 会话 -> 滑动过期
   |  -> 维护/站点状态检查 -> CSRF Token -> 安全响应头
   v
入口文件各自的处理逻辑
   |- index.php : 静态资源服务 -> 记住我自动登录 -> Router::dispatch()
   |- admin.php : IP 白名单 -> action 路由表 -> 登录/权限校验 -> 控制器
   `- api.php   : system_init 钩子 -> api_dispatch 钩子(插件接管或 404)
   |
   v
控制器(app/Controllers/) -> 模型(app/Models/) -> Database(PDO)
   |
   v
Template 模板引擎渲染 -> 响应输出

三个入口的分工:

入口面向对象路由机制职责要点
index.php前台访客Router 类,配置驱动的路由表另负责 /themes/*/assets/* 与 /admin/assets/* 静态资源直接输出(含 304 协商缓存)
admin.php管理后台?action= 参数 + $controllerMap 映射表登录校验、角色权限、操作级权限、插件后台页面分发均在此完成
api.php开放 API无内置路由核心不实现 API 逻辑,由 openapi 插件通过 api_dispatch 钩子接管;无插件接管时返回 404 JSON 提示

内核源码分为两块:core/lib/ 是功能类库(路由、数据库、钩子、插件、缓存、安全等),core/template/ 是模板引擎的组件拆分(编译器、解析器、继承、条件等)。所有类均由 core/lib/autoload.php 注册的自动加载器按需载入。

二、启动引导:bootstrap.php 逐段讲解

core/bootstrap.php 是所有入口的公共引导文件,按源码中的分节顺序执行:

  1. 输出缓冲(第 0 节):ob_start() 开启输出缓冲,保证后续任何位置都能设置 HTTP 头,避免 headers already sent 错误。
  2. 系统常量(第 1 节):定义路径类常量 ROOT_PATH、CORE_PATH、APP_PATH、STORAGE_PATH、LOG_PATH、UPLOADS_PATH 与安装锁路径 INSTALL_LOCK。
  3. 安装检查(第 2 节):install.lock 不存在时 302 跳转到 /install.php,未安装的系统无法进入业务流程。
  4. 自动加载(第 3 节):引入 core/lib/autoload.php。该文件注册 spl_autoload_register('autoload'),加载策略分三层:
    • PSR-4 命名空间映射:BlogKit\Core\、BlogKit\Exception\、BlogKit\Template\、BlogKit\Models\、BlogKit\Services\、BlogKit\Controllers\Front\、BlogKit\Controllers\Admin\、BlogKit\Middleware\ 分别映射到 core/lib/、core/lib/exceptions/、core/template/、app/ 下对应目录;
    • 无命名空间传统类名扫描:按 core/lib/、core/template/、app/Models/ 等固定路径逐个尝试 {类名}.php;
    • 插件智能扫描:类名匹配 /(.+?)(Plugin|Extension|Hook)$/i 时,到 plugins/{小写前缀}/ 目录查找同名类文件。

该文件末尾还会补齐路径常量的兜底定义,并引入 core/constants.php(业务常量:文章状态、用户角色、缓存 TTL、HTTP 状态码、CSRF 令牌有效期 CSRF_TOKEN_LIFETIME 等 20 组)。

  1. 核心类加载(第 4 节):显式 require Config、Database、Model、Router、Security、ExceptionHandler、functions.php(全局辅助函数)与 Template。
  2. 系统初始化(第 5 节):Config::init() 先加载 core/config/ 下的文件配置(database.php、system.php、version.php),再从 bk_config 表读入全部运行时配置。数据库配置键为下划线命名(如 cache_type),加载时会自动注册一份点语法别名(cache.type),新旧代码风格均可读取;debug_、cache_ 前缀的键同理。随后按 site_timezone 设置时区,并调用 ExceptionHandler::register() 注册统一异常处理器(须在 Config::init() 之后,以便读取 debug 配置)。
  3. 会话(第 6-7 节):会话文件存放于 storage/sessions/,Cookie 名固定为 BKSID(独立于默认 PHPSESSID,避免同域部署的其他系统登录互踢),Cookie 参数为 HttpOnly、SameSite=Strict、HTTPS 下 Secure。会话采用 30 分钟滑动过期:last_activity 超时后清除登录态并标记 session_expired。
  4. 站点状态(第 7.1-7.2 节):maintenance_mode 开启时,非管理员访问返回 503 维护页;site_status 为 closed 时非管理员同样 503,为 private 时未登录访客跳转登录页。
  5. CSRF Token(第 8 节):会话中无令牌时调用 Security::generateCsrfToken() 生成(详见第八节)。
  6. 安全响应头(第 9 节):统一输出 X-Frame-Options: DENY、X-Content-Type-Options: nosniff、X-XSS-Protection、Referrer-Policy 等响应头;HTTPS 下追加 HSTS。CSP 策略因主题模板存在大量内联样式与脚本,使用 'unsafe-inline';同时每次请求生成随机 nonce 存入 $_SESSION['csp_nonce'] 并注入模板变量 csp_nonce。

三、路由系统

3.1 URL 到 action 的映射规则

前台路由集中在 core/lib/Router.php。核心是 $routeDefinitions 路由表,每行格式为:

// [路由名, 配置键, 默认 URL 模式, {id} 参数正则, 控制器, 方法]
['home_article', 'rewrite_article', 'article/{id}', '([a-zA-Z0-9_-]+)', 'Home', 'article'],

匹配流程(resolveRoute 方法):

  1. rewrite.enabled 开启时,优先读取配置键对应的自定义伪静态规则(如 rewrite_article),未设置则用默认模式;
  2. 提取模式中的 {xxx} 占位符,查内置占位符正则映射表(支持 {id}、{slug}、{year}、{month}、{category_slug}、{username} 等数十种);{id} 特殊处理——优先使用路由行自带的参数正则(如分类路由用它兼容 slug 风格);未知占位符按 ([^\/]+) 处理;
  3. 对 URL 做全串正则匹配,全部捕获组按顺序作为位置参数传入控制器方法;
  4. 无占位符的路由做精确匹配。

在此之前,构造函数还有两条快速通道:/index.php/profile 直达 Auth::profile;?c=控制器&m=方法&id=值 的传统查询串格式直接映射(仅 id 作为位置参数,其余查询参数留在 $_GET 中由方法自行读取)。

dispatch() 分发时的完整顺序:

  1. 插件前台路由接管:Plugin::matchFrontRoute($method, $path) 按「先注册先得」顺序匹配已启用插件在 plugin.json routes 中声明的路由,命中即由插件控制器处理(插件侧写法见 06-插件开发指南 5.3 节);
  2. 核心路由:加载 app/Controllers/Front/{Controller}Controller.php,实例化并调用方法;
  3. 404 兜底:未命中时由 HomeController::notFound() 渲染 404 页。

若以上均未命中,Router 还有一条兜底解析路径:按 / 切分 URL,第一段作控制器、第二段作方法、其余作参数——这保证了路由表遗漏时 index.php/控制器/方法 形式的 URL 仍可用。

3.1.1 路由表分组一览

$routeDefinitions 按控制器分组,当前共 24 条路由(摘录结构,完整清单见源码):

分组代表路由默认模式说明
Home 内容页home_articlearticle/{id}{id} 正则兼容数字 ID 与 slug(含大写字母/下划线)
Home 列表页home_category / home_tag / home_pagecategory/{id} 等分类、标签、独立页面
Home 功能页home_tags / home_search / home_archivestags、search、archives无参数精确匹配
RSSrssrss另有 /feed 别名在 resolveRoute 中硬编码
Auth 认证auth_login / auth_register / auth_logout 等login、register、logout 等含找回密码、用户主页 user/{id}
个人中心子页profile_articles / profile_comments 等profile/articles 等九个子页均为无参数精确匹配

/profile 本身有特殊保护:无论其自定义重写规则如何设置,核心始终兜底将其映射到 Auth::profile,防止被站长误配置覆盖。

3.2 伪静态与 Router 的关系

伪静态分两层:

  • 单条路由自定义:后台「系统设置 - 伪静态设置」(RewriteSettings 控制器)保存的 rewrite_* 配置项,开启 rewrite_enabled 后逐条覆盖路由表默认模式。例如把 rewrite_article 设为 archives/{year}/{postname},即可用日期式 URL 访问文章;
  • 全局重写规则:rewrite.rules 配置数组支持 :参数 风格的通用重写(applyRewriteRules 方法),作为路由表未命中时的补充。

Nginx/Apache 只需把非静态请求统一转发到 index.php,URL 是否「伪静态」完全由 Router 层的配置决定。

3.3 如何新增一条路由

以新增 demo/{id} 映射到 Home::demo() 为例:

// 1. core/lib/Router.php 的 $routeDefinitions 数组中追加一行
//    配置键 rewrite_demo 允许站长在后台自定义该路由的伪静态形式
['home_demo', 'rewrite_demo', 'demo/{id}', '(\d+)', 'Home', 'demo'],

// 2. app/Controllers/Front/HomeController.php 中新增方法
//    $id 由路由占位符 {id} 按捕获顺序传入
public function demo($id = 0) {
    $articleModel = new ArticleModel();
    $article = $articleModel->find((int)$id);
    // ... 渲染模板或输出数据
}

要点:路由行的参数正则只约束 {id} 占位符;若使用 {slug} 等其他占位符,正则取自内置映射表;参数可以有多个(如 {year}/{month} 会按顺序传入两个方法参数)。

四、数据库层

4.1 Database 类:PDO 封装

core/lib/Database.php 是单例模式的 PDO 封装。连接参数来自 core/config/database.php,连接选项为:异常模式报错、默认 FETCH_ASSOC 取值、关闭模拟预处理(EMULATE_PREPARES => false,SQL 注入防护的基础);字符集优先 utf8mb4,服务器不支持时自动降级 utf8。连接失败只记录日志并向页面输出通用错误信息(API 请求输出 JSON),不暴露任何数据库细节。

$db = Database::getInstance();

// 查询构建:全部使用预处理参数绑定
$row = $db->fetch("SELECT * FROM {$db->table('article')} WHERE id = ?", [$id]);
$list = $db->fetchAll("SELECT * FROM {$db->table('article')} WHERE status = ?", [1]);

// 结果缓存:第三参传 true 开启,第四参为过期秒数
// 缓存键为 db_fetch_ / db_fetchall_ 前缀 + SQL 与参数的 md5,需 cache.enabled 开启
$hot = $db->fetchAll("SELECT * FROM {$db->table('article')} ORDER BY views DESC LIMIT 10", [], true, 600);

// 写操作:表名自动加系统前缀,写后自动清理 db_* 缓存
$newId = $db->insert('article', ['title' => '标题', 'status' => 1]);
$db->update('article', ['status' => 2], ['id' => $newId]);
$db->delete('article', ['id' => $newId]);

// SQL 表达式:update 的值以 SQL: 开头时不作绑定,直接进入 SQL(值必须可信)
$db->update('article', ['views' => 'SQL:views + 1'], ['id' => $newId]);

// 事务
$db->beginTransaction();
try {
    $db->insert('comment', ['content' => '内容', 'article_id' => 1]);
    $db->update('article', ['comment_count' => 'SQL:comment_count + 1'], ['id' => 1]);
    $db->commit();
} catch (Exception $e) {
    $db->rollback();
}

调试支持:debug.enabled 或 debug_slow_query_log 开启时,每次查询会记录耗时到内存查询日志(Database::getQueryLog() 读取,调试面板消费),超过 debug_slow_query_time 秒的查询写入慢查询日志。

// 查询日志与慢查询示例(需开启调试配置)
Database::clearQueryLog();                          // 清空本请求的查询日志
$stmt = $db->query("SELECT * FROM {$db->table('config')}");
foreach (Database::getQueryLog() as $entry) {
    // $entry 包含 sql / params / execution_time(毫秒) / timestamp
}

注意:query($sql, $params, $cache, $expire) 签名中的缓存参数在本方法内不生效,结果缓存实际由 fetch/fetchAll 实现。另外任何写操作(insert/update/delete)都会通过 Cache::deleteByPattern('db_*') 清空全部数据查询缓存,是「整表失效」而非「单键失效」策略。

4.2 Model 基类:CRUD 封装

core/lib/Model.php 是所有业务模型的基类,构造时传入表名(不带前缀):

class ArticleModel extends Model {
    public function __construct() {
        parent::__construct('article'); // 实际表名为 bk_article
    }
}

// 基类提供的通用方法
$model = new ArticleModel();
$model->find($id);                     // 按主键查单条(可指定主键名 $primaryKey)
$model->all();                         // 全表
$model->get(['status' => 1], ['id' => 'DESC'], 10, 0); // 条件/排序/limit/offset
$model->count(['status' => 1]);        // 计数
$model->insert($data);                 // 委托 Database::insert
$model->update($data, $where);         // 委托 Database::update
$model->delete($where);                // 委托 Database::delete

使用约束:get()/count() 的条件数组只支持等值匹配,全部走预处理绑定;排序子句是字符串拼接,$orderBy 的键名必须来自受控白名单(禁止直接透传用户输入);limit/offset 以绑定参数方式进入 SQL。复杂查询(联表、聚合、IN 条件)应在业务模型子类中用 $this->db->fetchAll() 手写 SQL 完成,基类不做查询构建器式的链式封装。

五、钩子系统

5.1 注册与触发原理

钩子系统由两个类协作完成,形成「静态门面 + 注册表」结构:

  • core/lib/Hook.php:静态门面。定义了覆盖用户、文章、评论、页面、分类、标签、系统、上传、通知、邮件、后台、API、模板、安全、SEO、主题、插件、支付等领域的钩子名常量,并提供三个触发入口;
  • core/lib/Plugin.php:注册表实际持有者。addHook($hook, callable $callback, $priority = 10) 把回调按钩子名与优先级存入 hooks[$hook][$priority][] 数组并 ksort 排序,优先级数值越小越先执行。

数据流:内核代码调用 Hook::trigger(...) -> Plugin::triggerHook(...) -> 按优先级遍历回调并传入 $data -> 返回执行结果;整个过程记录钩子执行日志(受日志配置 enabled 与 enabled_categories 中 hook 分类的双重开关控制)。插件在 init() 中调用 $this->addHook() 完成订阅(插件侧写法见 06-插件开发指南 5.1 节)。

5.1.1 钩子命名规范与常量清单

钩子名采用 业务域_对象_动作_阶段 的下划线命名,常量名与字符串值一一对应,便于检索:

前缀(业务域)示例常量示例钩子名
USER_ / ADMIN_USER_LOGIN_AFTER / ADMIN_LOGIN_BEFOREuser_login_after / admin_login_before
ARTICLE_ / COMMENT_ARTICLE_CREATE_BEFORE / COMMENT_ADD_AFTERarticle_create_before / comment_add_after
PAGE_ / CATEGORY_ / TAG_PAGE_VIEW_BEFORE 等page_view_before 等
SYSTEM_SYSTEM_INIT / SYSTEM_CACHE_CLEARsystem_init / system_cache_clear
TEMPLATE_TEMPLATE_RENDER_BEFORE / TEMPLATE_ASSIGNtemplate_render_before / template_assign
PLUGIN_ / THEME_PLUGIN_ACTIVATE_AFTER / THEME_ACTIVATE_BEFOREplugin_activate_after / theme_activate_before
FILE_ / EMAIL_ / CACHE_FILE_UPLOAD_AFTER / EMAIL_SEND_ERROR / CACHE_SET同名转换

完整的常量定义见 core/lib/Hook.php;哪些钩子在核心代码中实际被触发(而非仅定义),以 06-插件开发指南 5.1 节的「已验证触发的钩子清单」为准,监听优先选择该清单内的钩子。

5.2 三种触发方式

// 1. 普通触发:回调返回值按字符串拼接,返回拼接结果
Hook::trigger(Hook::ARTICLE_PUBLISH_AFTER, $article);

// 2. 带返回值触发:收集所有回调的返回值为数组(api.php 用它判断插件是否接管请求)
$results = Hook::triggerWithReturn('api_dispatch');
if (!empty($results)) {
    exit; // 有插件接管,本次请求已由插件处理完毕
}

// 3. 过滤器:$data 依次传入各回调,每个回调的返回值作为下一个回调的输入
$content = Hook::filter('article_content_render', $content);

配套方法:Hook::hasCallback($hookName) 判断某钩子是否有监听者。此外 core/lib/HookTrigger.php 提供了按业务语义命名的静态快捷封装(如 HookTrigger::systemInit()),内核控制器中多用它触发钩子。

5.3 钩子在内核与插件间的桥梁作用

内核通过钩子把扩展点暴露给插件,典型桥接模式有三类:

  • 数据注入:模板引擎构造时通过 Hook::triggerWithReturn(Hook::TEMPLATE_NOTIFICATION_COUNT, $userId) 向通知插件询问未读数,通过 Hook::TEMPLATE_PLUGIN_FLAGS 收集各插件的功能标记并注入为模板变量——内核只定义「问题」,答案由启用中的插件给出,插件未安装时取默认值(0 或 false);
  • 流程接管:api.php 的 api_dispatch 钩子允许插件整体接管请求;front_profile_page、front_interaction_action 等钩子的回调返回 true 即跳过核心默认处理;
  • 结果过滤:article_content_render、search_results_filter、admin_config_collect_custom 等过滤器钩子让插件修改内核产出的数据。

各入口中,只有 api.php 在引导阶段主动触发 system_init 钩子;前台与后台依赖具体业务位置(如模板变量注入、控制器动作前后)触发各自的钩子。

六、模板引擎内部

6.1 组件拆分

core/template/ 下,Template.php 是协调器,其余为职责单一的子组件:

文件职责
Template.php变量管理、站点/主题配置加载、子组件编排、渲染入口
TemplateParser.php全部标签的解析管线(30 余个解析阶段按固定顺序执行)
TemplateCompiler.php编译结果缓存(读写与失效判断)
TemplateInheritance.php模板继承与 {block} 块
TemplateCondition.php复杂条件标签
TemplateUrl.php{url:...} 类 URL 生成
TemplateOutput.php输出辅助
TemplateFilter.php变量过滤器
TemplateExtension.php扩展标签注册

Template 构造函数完成几件关键初始化:模板根目录默认取 themes/{default_theme}/templates/(无该子目录时回退主题根目录),搜索路径包含模板根目录与其下 common/;随后注入一批框架级模板变量——config(全部站点配置)、登录用户的 total_notifications(经 template_notification_count 钩子询问插件)、插件功能标记(interaction_enabled、notification_enabled 等)、csrf_token(受 security_csrf_protection 开关控制)、csp_nonce。

6.2 渲染流程与标签解析管线

$template = new Template();          // 默认指向当前主题模板目录
$template->assign('articles', $list);
$template->display('index.html');    // 等价于 echo $template->render('index.html')

render() 的执行路径:loadTemplate($name) 按「模板名 -> 模板名.html -> common/模板名 -> common/模板名.html」的顺序在搜索路径中定位文件;命中后(编译缓存未命中时)先做继承解析(parseInheritance + parseBlocks),再交给 TemplateParser::parse()。

标签管线按固定顺序执行约 30 个解析阶段,可归为六组:

顺序解析组覆盖标签(举例)
1结构与复用{cache} 区块、{macro} 宏、{assign}、{include}
2内置与安全{year}、{current_url}、{csrf_field}、{plugin_hook}
3配置与条件{config}、{site.xxx}、{if} 及其变体(in_array/isset/empty/equal/变量比较)
4循环与代码{loop}、{loop.xxx} 再访问、{php}
5页面组件{field}、{pagination}、{breadcrumb}、{url:xxx}、{top_badge}、{func:xxx}
6收尾变量输出 `{$var

管线顺序意味着后解析的标签不会出现在先解析阶段产生的文本中,扩展新标签时应参考 05-主题开发指南 的标签清单并在解析器中选择合适的插入位置。

6.3 编译缓存机制

编译缓存由 TemplateCompiler 负责,开关逻辑在 Template 构造函数中:后台「缓存功能开关」cache_enabled 为 1 且 template_cache.enabled(默认开启)时启用。机制细节:

  • 缓存目录:storage/cache/templates/;缓存文件名为 md5(模板文件绝对路径) . '.php';
  • 有效性判断:缓存文件的 mtime 必须晚于或等于模板文件 mtime(isCompiled 比较),因此编辑模板文件保存后,下一次请求即自动重新编译,无需手动清理;
  • 缓存内容是「解析后的模板文本」而非 PHP 代码,运行时不再执行标签替换,直接进入后续输出环节;
  • cache_enabled 关闭时每次请求都实时解析全部标签,便于主题调试。

另有与编译缓存并行的区块缓存:模板中 {cache ttl="600" key="sidebar"}...{/cache} 包裹的片段,其渲染结果以 md5(key).html 形式写入同一目录,TTL 内直接复用,适合侧栏等查询密集区块。

缓存失效途径有三:模板文件修改(mtime 比较自动失效)、后台清空缓存操作(调用 TemplateCompiler::clear() 删除目录下全部 .php 编译文件)、cache_enabled 开关切换。模板标签语法的完整用法见 05-主题开发指南。

七、缓存系统

7.1 驱动类型与配置

core/lib/Cache.php 为单例缓存门面,支持两种驱动:

  • file(默认):每条缓存一个 .cache 文件,内容为 serialize 后的数组(值、过期时间、创建时间、压缩标记);后台开启 cache_compression 后使用 gzip 压缩存储;
  • redis:cache_type 配置为 redis 时启用,连接参数在 redis 配置组(host/port/password/timeout);连接失败自动降级为文件缓存并记录错误。

主要配置键与后台设置的对应关系:

配置键含义默认值
cache_enabled缓存总开关(关闭时数据缓存与模板编译缓存均不生效)0
cache_type驱动类型 file/redisfile
cache_path缓存目录storage/cache
cache_expire默认过期秒数3600
cache_size_limit文件缓存容量上限(MB)100
cache_key_prefix缓存键前缀blog_
cache_compression是否 gzip 压缩0
cache_history_days监控统计数据保留天数7

7.2 缓存键规范与失效

所有键经 generateCacheKey() 归一化:最终键 = 前缀 + 命名空间_ + md5(完整键)。键中含冒号(如 article:list:home)时,冒号前的部分会转为下划线形式作为命名空间前缀,便于按模式批量清理。推荐的键写法是「业务域:对象:标识」,例如 article:detail:123。

Cache::setCache('article:detail:123', $article, 600);          // 设置
$article = Cache::getCache('article:detail:123');               // 读取(未命中返回 null)
Cache::deleteCache('article:detail:123');                       // 单键删除

// 标签失效:写入时附带标签,之后可整组失效
Cache::setCache('article:detail:123', $article, 600, ['article']);
Cache::invalidateTag('article');                                // 删除所有带该标签的缓存

文件缓存的过期采用惰性删除:读取时发现已过期即删除文件并返回未命中。容量超过 cache_size_limit 时按 LRU 策略(按文件 mtime 从旧到新)清理最旧文件,清理策略固定为 LRU,后台展示的清理策略项仅为说明。数据库查询缓存使用 db_ 开头的键,任何数据写操作会触发 deleteByPattern('db_*') 全量清理(见第四节)。缓存命中/写入统计在开启监控(cache_monitoring_enabled)时周期性写入 bk_cache_stats 表,供后台缓存页查看命中率与健康状态;插件可订阅 cache_clear_before/after 等钩子参与缓存生命周期。

7.3 批量操作、统计与健康检查

Cache 门面除单键操作外还提供成组能力(均受 cache_batch_operation 开关控制):

方法用途
batchSet($items)批量写入,$items 格式为 ['键' => ['value' => 值, 'expire' => 秒]]
batchGet($keys, $default = null)批量读取,返回 键 => 值 数组
batchDelete($keys)批量删除
deleteByPattern($pattern)按模式删除(仅文件驱动,glob 匹配 键*.cache)
getStats()命中/未命中/写入/删除计数、命中率、缓存目录体积
getHealthStatus()驱动连通性、缓存目录可写性、磁盘占用、容量使用率的综合健康报告

监控开启时,统计快照会写入 bk_cache_stats 表(首次使用时自动建表,含 hits/misses/sets/deletes/cache_size/hit_rate 字段),并按 cache_history_days 清理过期记录;该表数据供后台缓存管理页绘制命中率趋势。上述全部静态便捷方法(Cache::getCache/setCache/deleteCache/clearCache 等)内部均转发到单例实例,插件与业务代码可任意选用。

八、安全组件

core/lib/Security.php 提供贯穿全站的安全原语,各入口与中间件在其上组装防护。

8.1 CSRF Token 的生成与校验

  • 生成(generateCsrfToken()):令牌在 CSRF_TOKEN_LIFETIME(7200 秒,定义于 core/constants.php)内复用——同一页面渲染多个表单不会互相作废;生成优先使用 random_bytes(32)(CSPRNG),降级 openssl_random_pseudo_bytes,最后兜底 uniqid + mt_rand;存入 $_SESSION['csrf_token'] 与 $_SESSION['csrf_token_time']。bootstrap 在会话就绪后统一生成;
  • 校验(validateCsrfToken($token, $expiration = null)):依次执行——后台 security_csrf_protection 开关为 0 时直接放行(默认 1 开启);空值与非字符串拒绝;会话无令牌拒绝;hash_equals 常时比较防时序攻击;过期检查;
  • 输出(getCsrfField()):返回 <input type="hidden" name="csrf_token" value="...">,开关关闭时返回空字符串;模板中亦可使用 {csrf_field} 标签。

8.2 输入过滤与校验

  • filterXss($input, $flags, $encoding):对字符串做 htmlspecialchars(默认 ENT_QUOTES | ENT_SUBSTITUTE,UTF-8),数组与对象递归处理;受 xss_protection 配置开关控制;
  • sanitizeInput($input):先 trim 再递归 XSS 过滤;
  • 校验族:isValidEmail/isValidUrl/isValidIp/isValidInt/isValidFloat/isValidLength 基于 filter_var 实现;
  • filterSql() 已标记弃用并触发 E_USER_DEPRECATED,SQL 注入防护的正确途径是 PDO 预处理(见第四节,EMULATE_PREPARES 已关闭);
  • getClientIp() 按 HTTP_CF_CONNECTING_IP、X-Forwarded-For 等头优先级取值并做 IP 格式校验,多 IP 时取第一个——部署在不可信代理之后时需注意该头可被伪造。

8.3 安全响应头与会话防护的内部要点

bootstrap 每次请求统一输出以下响应头:

响应头取值防护目标
X-Frame-OptionsDENY点击劫持(禁止被 iframe 嵌套)
X-Content-Type-OptionsnosniffMIME 嗅探
X-XSS-Protection1; mode=block旧浏览器 XSS 过滤器
X-Permitted-Cross-Domain-Policiesnone跨域策略文件加载
X-Download-Optionsnoopen下载文件直接打开
Referrer-Policystrict-origin-when-cross-origin引用来源泄露
Strict-Transport-Securitymax-age=31536000; includeSubDomains(仅 HTTPS)强制 HTTPS
Content-Security-Policy脚本/样式 'unsafe-inline' 等指令资源加载白名单

会话与请求判别类辅助方法:会话 Cookie 属性(HttpOnly/SameSite=Strict/HTTPS Secure)、30 分钟滑动过期均实现于 core/bootstrap.php(见第二节第 7、10 步);Security::isAjaxRequest() 依据 X-Requested-With 头判断、isPostRequest()/isGetRequest() 判断请求方法,控制器中常用于分支处理。后台另有 admin_ip_whitelist_enabled 开关控制的 IP 白名单(core/lib/IpWhitelist.php,支持可信代理配置),在 admin.php 入口于路由分发前拦截。表单处理的完整实践(token 注入、POST 校验、失败处理)见 06-插件开发指南 的示例代码。

九、源码导读地图

按「想改什么功能,先看哪个文件」整理的速查表(路径均相对系统根目录):

目标源码位置
调整引导流程、会话、安全响应头core/bootstrap.php
新增/修改前台路由、伪静态占位符core/lib/Router.php($routeDefinitions 与 resolveRoute)
后台新增功能页、权限码、操作级校验admin.php($controllerMap、SUB_ACTION_PERMISSION_MAP)+ 对应 app/Controllers/Admin/ 控制器
数据库连接、查询缓存策略、慢查询日志core/lib/Database.php
通用 CRUD 封装调整core/lib/Model.php
新增钩子定义点、调整触发语义core/lib/Hook.php(常量)+ core/lib/HookTrigger.php(快捷封装)
插件加载、生命周期、前台/后台路由接管、ACLcore/lib/Plugin.php;插件基类在 core/lib/PluginBase.php
模板编译缓存策略core/template/TemplateCompiler.php + core/template/Template.php(构造函数开关逻辑)
新增/调整模板标签解析core/template/TemplateParser.php(parse() 管线中定位插入点)
模板继承与块机制core/template/TemplateInheritance.php
缓存驱动、键规范、标签失效、监控core/lib/Cache.php
CSRF、XSS 过滤、输入校验core/lib/Security.php
运行时配置加载与别名机制core/lib/Config.php + core/config/ 目录
统一异常处理与 404/403 页core/lib/ExceptionHandler.php + core/lib/exceptions/
业务常量(状态码、TTL、上传限制等)core/constants.php
自动加载规则core/lib/autoload.php
全局辅助函数core/lib/functions.php
API 入口逻辑与插件接管点api.php
主题静态资源服务与 304 缓存index.php(serveStaticFile())

相关文档:主题开发指南、插件开发指南、目录与文件说明、API 文档