场景:产品经理通过后台系统插入一个字母组成的优惠码(例如:coupon)之后,再从后台通过关键字coupon去查找这个优惠码,查不到,同时影响线上使用。
假设表名:exchange_code.
假设列名:code , varchar类型
症状1:通过Sequel Pro按字段名等等条件搜索coupon,搜索不出来这条记录。
等价于SQL: SELECT * from exchange_cdoe where code = 'coupon';
症状2:通过contains条件搜索,能找到这条记录。
等价于SQL: SELECT * from exchange_cdoe where code contains 'coupon';
症状3:通过 length 函数,计算一下code 长度, 发现长度并不等于coupon的长度6.
看上去是6个字母,却表现出上面的情况。
后面团队想到办法,把里面的内容复制出来,转化为 unicode。
发现了里面包含 unicode \200b 这个不可见字符。
coupon
http://tool.chinaz.com/tools/unicode.aspx (从左至右复制上面这一行字符,去这个网址转换unicode试试)
问题复现
利用java 复现问题
@Test
public void testBlog(){
// 字母m 对应unicode u6d
String unicode = "\\u6d\\u200b\\u6d";
System.out.println("unicode:"+unicode);
String string = unicode2String(unicode) ;
System.out.println("string:"+string);
}
/**
* unicode 转字符串
*/
public static String unicode2String(String unicode) {
StringBuffer string = new StringBuffer();
String[] hex = unicode.split("\\\\u");
for (int i = 1; i < hex.length; i++) {
// 转换出每一个代码点
int data = Integer.parseInt(hex[i], 16);
// 追加成string
string.append((char) data);
}
return string.toString();
}
输出结果:
unicode:\u6d\u200b\u6d
string:mm
输出结果中mm之间有一个不可见不占位的 \u200b 特殊字符,类似的 \u200c 也是一样的效果。
这些字符的产生 也许是因为”实际上当前版本的统一码并未完全使用这16位编码,而是保留了大量空间以作为特殊使用或将来扩展。” — https://zh.wikipedia.org/wiki/Unicode 。
问题预防:
通过正则表达式校验数据合法性(例如,只允许数字和大小写字母)。
@Test
public void testBlog(){
// 字母m 对应unicode \\u6d
String unicode = "\\u6d\\u200b\\u6d";
System.out.println("unicode:"+unicode);
String string = unicode2String(unicode) ;
System.out.println("string:"+string);
Pattern pattern = Pattern.compile("[a-zA-Z0-9.]+");
System.out.println("pass? "+pattern.matcher(string).matches());
}
输出结果:
unicode:\u6d\u200b\u6d
string:mm
pass? false
=== 程序员日常小故事 之 隐形的字符===
产品W:“怎么回事啊,这条记录我刚插入的,后台怎么查不出来啊”
程序员T:“我看看怎么回事,之前都是好的啊”
产品H:“这个功能模块怎么又出问题了?”
程序员Z:“我看看啊,数据库里去查一下,怎么数据库里按字段查,也查不出来啊。 ”
某某程序员:“按ID查。”
某某程序员:“查一下这个字段的长度”
某某程序员:“奶奶的,怎么和看上去的长度不一样”
某某程序员:“看看字符串的unicode编码”
某某程序员:“复制到Java里面转成unicode看看”
某某程序员:“奶奶的怎么有不可见字符”
程序员T:“产品W,你是怎么输入不可见字符的…”
产品W:“我是正常操作的呀”