多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

PHP字符串比较函数strcmp()和strcasecmp()使用总结

PHP字符串比较函数strcmp()和strcasecmp()使用总结 前言strcmp()和strcasecmp()是一对孪生函数前者区分大小写后者不区分大小写其余行为一致都做二进制安全的字节比较。关于它们有三个反复出现的误解。第一个是返回值。手册明确只保证「小于零 / 等于零 / 大于零」这三种情况不保证具体的整数。所以if (strcmp($a, $b) -1)这种写法是不可靠的正确姿势是 0、 0、 0。第二个是「strcmp()能防时序攻击」。不能。它是普通的字节比较一发现不同就返回比较哈希值、校验签名这类场景要用hash_equals()。而校验用户密码连hash_equals()都不该直接用——应该用password_verify()。第三个是「strcmp()能比较数字大小」。也不能。它逐字节比strcmp(10, 9)的结果是负数因为字符1比9小。要按数值比就先转成数字要按「人能接受的顺序」比就用strnatcmp()。本文讲清这两个函数的签名、返回值的正确用法、字节比较带来的各种后果以及在现代 PHP 里该用哪些新函数替代它们。一、签名与返回值以 PHP 官方手册为准strcmp(string $string1, string $string2): intstrcasecmp(string $string1, string $string2): int两个函数的返回值语义完全相同返回值含义小于 0$string1小于$string2等于 0两个字符串相等大于 0$string1大于$string2注意这里刻意没有写「返回 -1」「返回 1」——手册的承诺只有符号。历史实现里有的直接返回两个字节的差值有的做了归一化具体数字不是可依赖的接口。稳妥写法一律是判断符号?php// 适用于 PHP 8.0// 相等判断必须是 0var_dump(strcmp(abc, abc) 0); // bool(true)var_dump(strcmp(abc, abd) 0); // bool(false)// 大小关系判断符号var_dump(strcmp(abc, abd) 0); // bool(true)var_dump(strcmp(abd, abc) 0); // bool(true)// 大小写小写字母的 ASCII 值更大var_dump(strcmp(a, A) 0); // bool(true)// strcasecmp忽略 ASCII 大小写var_dump(strcasecmp(Hello, hello) 0); // bool(true)var_dump(strcasecmp(Hello, World) 0); // bool(false)二、strcmp() 的比较规则strcmp()的比较规则可以用三句话概括二进制安全按字符串的实际长度逐字节比较内容中带\0也不会提前结束。按字节的数值大小比较本质上是比较字符的编码值所以小写字母大于大写字母数字字符之间也是按字符比。前缀相同的两个字符串短的更小。?php// 适用于 PHP 8.0// 数字字符比较不是数值比较var_dump(strcmp(10, 9) 0); // bool(true) 1 9var_dump(10 9); // bool(false) 这才是数值比较// 前缀相同时短的更小var_dump(strcmp(abc, abcd) 0); // bool(true)var_dump(strcmp(, ) 0); // bool(true)// 二进制安全中间的 NUL 字节照样参与比较var_dump(strcmp(a\0b, a\0c) 0); // bool(true)关于中文strcmp()比较的是 UTF-8 字节序列得到的是字节序既不是拼音序也不是笔画序。给中文做「按拼音排序」这种需求strcmp()帮不上忙需要 intl 扩展的Collator或者自己把中文转成拼音/编码后再排。这一点在建索引、做排序列表时是硬约束别指望靠strcmp()蒙对。三、strcasecmp() 的边界只折叠 ASCII 大小写strcasecmp()的「不区分大小写」仅限ASCII 的A到Z。也就是说它只把英文字母的大小写视为等价非 ASCII 字符中文、带重音符号的拉丁字母等完全按原样比较。?php// 适用于 PHP 8.0var_dump(strcasecmp(abc, ABC) 0); // bool(true) ASCII 折叠var_dump(strcasecmp(straße, STRASSE) 0); // bool(false) 非 ASCII 不做折叠var_dump(strcasecmp(中文, 中文) 0); // bool(true) 完全相同的字节序列// 需要多字节的忽略大小写比较时先统一转小写再比$s1 mb_strtolower(Hello 世界, UTF-8);$s2 mb_strtolower(hello 世界, UTF-8);var_dump(strcmp($s1, $s2) 0); // bool(true)要点是strcasecmp()不读取mb_internal_encoding()也不认识 UTF-8它就是一个逐字节的 ASCII 比较器。涉及到多字节文本的忽略大小写比较老老实实mb_strtolower()两边之后再strcmp()。四、在什么场景用它们以及现代替代方案场景一当排序回调。这是这两个函数在实战中最常见的用法。排序只需要返回值的符号所以strcmp()可以直接当回调传进去。?php// 适用于 PHP 8.0$fruits [banana, Apple, cherry];$a $fruits;usort($a, strcmp); // 区分大小写大写 A 的编码更小排在前面var_dump($a); // [Apple, banana, cherry]$b $fruits;usort($b, strcasecmp); // 忽略大小写var_dump($b); // [Apple, banana, cherry]$c $fruits;sort($c, SORT_STRING); // 等价于按字符串比较排序var_dump($c); // [Apple, banana, cherry]场景二比较相等。单纯判断两个字符串是否相等时比strcmp()更直接也没有函数调用开销。但要注意它和的差别在两侧都是「数字字符串」时会做数值比较而strcmp()永远按字节比。?php// 适用于 PHP 8.0var_dump(10 10.0); // bool(true) 松散比较按数字比var_dump(strcmp(10, 10.0) 0); // bool(false) 按字节比长度都不同var_dump(10 10.0); // bool(false) 严格比较类型与内容都要求一致这就是「判断字符串相等到底该用哪个」的答案普通业务判断用需要明确「按字节比较、不参与任何数值语义」时用strcmp(...) 0。绝对不要用比较两个来路不明的字符串数字字符串的松散比较会带来意外相等。场景三前缀、包含关系。这类需求现在有更直白的函数可用不需要再借道strcmp()需求现代写法PHP 8.0老写法是否以某串开头str_starts_with($s, $prefix)strncmp($s, $prefix, strlen($prefix)) 0是否包含某串str_contains($s, $needle)strpos($s, $needle) ! false是否以某串结尾str_ends_with($s, $suffix)手动substr()比较自然顺序比较strnatcmp($a, $b)无strcmp做不到按当前区域设置比较strcoll($a, $b)无场景四比较敏感数据。这里必须换函数。比较两个哈希值、两个签名、两个令牌时strcmp()会「一发现不同就返回」比较耗时会泄露「前多少个字节是对的」这就是时序攻击的入口。正确做法是用hash_equals()?php// 适用于 PHP 8.0hash_equals 自 PHP 5.6 起提供$expected hash_hmac(sha256, $message, $key);$received $_POST[signature] ?? ;if (!hash_equals($expected, $received)) {exit(签名校验失败\n);}而校验用户密码连hash_equals()都不该直接上手应该用password_verify()——它会从哈希串里读取盐值和算法参数自己完成比对?php// 适用于 PHP 8.0$hash password_hash(用户密码, PASSWORD_DEFAULT);if (password_verify($_POST[password] ?? , $hash)) {echo 密码正确\n;} else {echo 密码错误\n;}千万不要自己去比md5($input) $stored也不要自己用strcmp()拼比对逻辑。常见坑点❌ 写成if (strcmp($a, $b))以为返回真就是相等。✅ 返回0才是相等非零是不等条件应写成if (strcmp($a, $b) 0)。❌ 依赖具体返回值strcmp($a, $b) -1。✅ 手册只保证符号不同实现返回的数字可能不同判断 0、 0、 0。❌ 用strcmp()比较数字大小strcmp(10, 9)得到负数。✅ 它逐字节比按数值比要先转成数字按「符合直觉的顺序」比用strnatcmp()。❌ 用strcasecmp()比较含中文或带重音字母的字符串以为它会忽略大小写差异。✅ 它只折叠 ASCII 的 A 到 Z多字节文本先mb_strtolower($s, UTF-8)再strcmp()。❌ 用strcmp()或strcasecmp()校验密码或比较令牌。✅ 密码用password_verify()其他敏感数据的比对用hash_equals()。❌ 用strcmp()判断前缀strncmp($s, $prefix, strlen($prefix)) 0写起来容易错。✅ PHP 8.0 起直接用str_starts_with($s, $prefix)。❌ 认为strcmp($a, $b) 0和$a $b等价。✅ 对数字字符串两者结论可能相反例如10与10.0用相等、用strcmp()不等。❌ 向这两个函数传null常见于读配置项没读到的时候。✅ 参数是非可空stringPHP 8.1 起传null会触发废弃提示先做类型转换。总结对比项strcmp()strcasecmp()大小写区分不区分仅 ASCII比较单位字节二进制安全字节二进制安全返回值只保证符号小于 0 /0/ 大于 0同左数字字符串按字符比10 9同左中文按 UTF-8 字节序不是拼音序非 ASCII 不折叠判等写法strcmp($a, $b) 0strcasecmp($a, $b) 0典型用途排序回调、明确的字节比较忽略大小写的排序、键名匹配不该用的场合密码、令牌、签名比对同左strcmp()和strcasecmp()的核心是「按字节、看符号」返回值只判断符号不判断数值比较单位是字节所以不做数值比较、也不是多字节安全的。普通判等用就够了涉及密码用password_verify()涉及其他敏感数据用hash_equals()前缀和后缀判断在 PHP 8.0 以后交给str_starts_with()这类语义化函数。把这几次分流做对字符串比较这一类问题就基本清干净了。
返回列表