EP165. “$wpdb 查询:get_results 与 prepare 防注入”
🔒 登录后可标记已读让 /pet-adoption 页面模板真正查询 wp_pets 表、把结果渲染成表格。先在 Adminer 数据库管理界面里练习几句基础 SQL(WHERE/ORDER BY/LIMIT/只选部分列),再搬到 PHP 里用 $wpdb->get_results() 执行查询。这一讲的重点是安全意识:只要 SQL 语句里有任何一部分来自用户输入(比如 URL 参数),就必须改用 $wpdb->prepare() 生成语句——用 %s/%d 占位符代替直接拼接用户提供的值,从根本上避免 SQL 注入攻击。
涉及文件
wp-content/plugins/new-database-table/inc/template-pets.php(修改)
代码实现
Adminer 界面里练习的 SQL(不是写进代码,只是熟悉语法):
SELECT * FROM wp_pets WHERE species = 'cat' AND birthyear > 2017 ORDER BY birthyear DESC LIMIT 100
SELECT petname, birthyear FROM wp_pets
inc/template-pets.php:查询表数据并渲染成表格:
<?php
get_header(); ?>
<div class="page-banner">
<div class="page-banner__bg-image" style="background-image: url(<?php echo get_theme_file_uri('/images/ocean.jpg'); ?>);"></div>
<div class="page-banner__content container container--narrow">
<h1 class="page-banner__title">Pet Adoption</h1>
<div class="page-banner__intro">
<p>Providing forever homes one search at a time.</p>
</div>
</div>
</div>
<div class="container container--narrow page-section">
<p>This page took <strong><?php echo timer_stop();?></strong> seconds to prepare. Found <strong>x</strong> results (showing the first x).</p>
<?php
global $wpdb;
$tablename = $wpdb->prefix . 'pets';
$ourQuery = $wpdb->prepare("SELECT * FROM $tablename LIMIT 100");
$pets = $wpdb->get_results($ourQuery);
?>
<table class="pet-adoption-table">
<tr>
<th>Name</th>
<th>Species</th>
<th>Weight</th>
<th>Birth Year</th>
<th>Hobby</th>
<th>Favorite Color</th>
<th>Favorite Food</th>
</tr>
<?php
foreach($pets as $pet) { ?>
<tr>
<td><?php echo $pet->petname; ?></td>
<td><?php echo $pet->species; ?></td>
<td><?php echo $pet->petweight; ?></td>
<td><?php echo $pet->birthyear; ?></td>
<td><?php echo $pet->favhobby; ?></td>
<td><?php echo $pet->favcolor; ?></td>
<td><?php echo $pet->favfood; ?></td>
</tr>
<?php }
?>
</table>
</div>
<?php get_footer(); ?>
关键改动点:
- 先在数据库管理界面(Adminer/phpMyAdmin)里熟悉基础 SQL 语法:
WHERE 列名 = '值'筛选、AND叠加多个条件、ORDER BY 列名 ASC/DESC排序、LIMIT 数字限制返回条数、SELECT 列名1, 列名2只选部分列而不是SELECT *全部列——这些练习不会体现在最终代码里,纯粹是先在图形界面里试出语法再搬进 PHP,比直接对着代码编辑器猜语法更直观 global $wpdb:$wpdb是 WordPress 提供的全局数据库操作对象,任何函数里想用它都要先用global关键字声明才能访问到$wpdb->get_results($SQL语句):执行一段 SQL 查询语句,返回一个数组,数组的每一项默认是一个对象(不是关联数组),所以后面渲染表格时要用$pet->petname这种「对象属性访问」语法(箭头->),而不是$pet['petname']$wpdb->prepare($SQL模板, ...值)——防 SQL 注入的关键工具:- 只要 SQL 语句里任何一部分来自用户可控的输入(最常见的是 URL 查询参数,比如
?species=dog),就绝对不能直接把这个值拼接进 SQL 字符串——恶意用户可能会在这个参数里塞入精心构造的 SQL 片段(即 SQL 注入攻击),篡改整条查询甚至破坏数据库 - 正确做法:SQL 语句里该有动态值的地方,用占位符代替——
%s表示这里会替换成一个字符串,%d表示会替换成一个数字(整数) - 第一个参数永远是「完全由你自己写死、绝对安全」的 SQL 模板字符串(带着
%s/%d占位符);第二个及之后的参数依次对应每个占位符实际要代入的值——prepare()会自动帮这些值做必要的转义处理,再把它们安全地嵌入最终的 SQL 字符串里 - 举例:
$wpdb->prepare("SELECT * FROM wp_pets WHERE species = %s AND birthyear > %d LIMIT 10", array('hamster', 2018))——hamster替换第一个%s,2018替换%d prepare()本身不会真的去查询数据库,它只是返回一段处理好、可以安全使用的 SQL 字符串,真正执行查询还是要把这段字符串传给get_results()- 这一讲最终的查询(
SELECT * FROM $tablename LIMIT 100)其实完全不含任何动态/用户输入的值,理论上不需要用prepare()包一层也是安全的——但代码里依然保留了这个用法,是为了给下一讲「根据 URL 参数动态拼查询条件」提前搭好架子
- 只要 SQL 语句里任何一部分来自用户可控的输入(最常见的是 URL 查询参数,比如
$tablename = $wpdb->prefix . 'pets':表名前缀依然动态获取,不硬编码wp_,跟 EP164 建表时的做法一致- 用双引号而不是单引号包住带变量的字符串:PHP 里想在字符串内部直接插值一个变量(比如
"SELECT * FROM $tablename LIMIT 100"里的$tablename),必须用双引号——单引号字符串不会做变量替换,会把$tablename原样当成字面文字输出 - 渲染表格:保留表头
<tr>(写死的列名标题),把原本表示「一行数据」的那个<tr>模板行整体放进foreach($pets as $pet) { ... }循环里,每次循环用当前$pet对象的各个属性替换掉原本的占位横杠
Hook / Function 速查
| 名称 | 类型 | 用途 |
|---|---|---|
$wpdb->get_results($SQL) | $wpdb 方法 | 执行 SQL 查询,返回结果数组(每项默认是对象) |
$wpdb->prepare($SQL模板, ...值) | $wpdb 方法 | 用 %s/%d 占位符安全拼接动态值,返回处理好的 SQL 字符串,防止 SQL 注入 |
%s / %d(prepare 占位符) | SQL 占位符语法 | 分别表示这里会安全代入一个字符串 / 一个数字 |
常见坑
- 把用户输入(尤其是 URL 参数)直接拼接进 SQL 字符串再执行——存在 SQL 注入风险,恶意用户可以借此篡改查询甚至破坏数据
- 用单引号包住需要插值变量的 SQL 字符串——PHP 不会对单引号字符串里的
$变量做替换,会把变量名原样当文字输出,查询会出错 - 把
get_results()返回的每一项当成关联数组用$pet['petname']访问——默认返回的是对象,要用$pet->petname这种箭头语法 - 误以为
$wpdb->prepare()会直接执行查询——它只是生成安全的 SQL 字符串,还需要传给get_results()/query()之类的方法才会真正执行
[截图:前台 /pet-adoption 页面渲染出的宠物数据表格,含 Name/Species/Weight/Birth Year 等列]
延伸 / 后续讲座会用到
下一讲要学怎么把 URL 参数(比如 ?favColor=green)动态拼进查询条件,这时候 prepare() 就真正派上用场了。
Sources
Udemy:
- Become a WordPress Developer: Unlocking Power With Code — Section 27, EP165