PathVariable

当我们在 Spring Boot 应用中,因为一些强制性的原因,不得不选用 PathVariable 定义的时候,可能会遇到如下问题,假设这个 URI 设计成 api/domain/resource/{name}。

  • 当 name 是形如 “/文件夹/文件夹/资源” 的时候,由于 Spring Boot 会将 / 用作路由分隔符,这里会出现不能预料的问题
  • name 是自定义编码的内容的时候,会不得不用上特殊字符

具体原因是 Spring Boot 的路由解析严格按照了 RFC 3986 标准设计,这套标准中只有 a..z 和 A..Z 和 0..9 和 -_.~ 以及部分保留字符 ! * ’ ( ) ; : @ & = + $ , / ? # [ ] 被认可。那么当业务中我们需要在路由里面包含了特殊字符的时候,为了不影响路由解析,最常见的做法就是给特殊字符进行编码。常见的有 URL Encode 和 Base64,这两种编码大多数常用的编程语言自带 encode,decode 库。

URL Encode

URL 编码是浏览器发送数据给服务器时使用的编码,它通常附加在 URL 的参数部分,例如:

https://www.baidu.com/s?wd=%E4%B8%AD%E6%96%87

之所以需要 URL 编码,是因为出于兼容性考虑,很多服务器只识别 ASCII 字符。但如果 URL 中包含中文、日文这些非 ASCII 字符怎么办?不要紧,URL 编码有一套规则:

如果字符是 AZ,az,0~9 以及-、_、.、*,则保持不变;
如果是其他字符,先转换为 UTF-8 编码,然后对每个字节以%XX 表示。
例如:字符中的 UTF-8 编码是 0xe4b8ad,因此,它的 URL 编码是%E4%B8%AD。URL 编码总是大写。

Base64

URL 编码是对字符进行编码,表示成%xx 的形式,而 Base64 编码是对二进制数据进行编码,表示成文本格式。

Base64 编码可以把任意长度的二进制数据变为纯文本,且只包含 AZ、az、0~9、+、/、=这些字符。它的原理是把 3 字节的二进制数据按 6bit 一组,用 4 个 int 整数表示,然后查表,把 int 整数用索引对应到字符,得到编码后的字符串。

面临的问题

根据背景判断,base64 首先需要排除掉,因为其中包含了 / 字符,这对于 PathVariable 来说是非常危险的。只要被编码了之后包含了 / ,比如 “你说这东西” 被编码之后是 “5L2g6K+06L+Z5Lic6KW/“ 这个时候在 api/domain/resource/{name} 设计的 URI 中,就会变成 api/domain/resource/5L2g6K+06L+Z5Lic6KW/,Spring Boot 会默认截断最后一个 / 符号,导致解码异常。

另一个场景下,URL Encode 也不适用,这里会有一些自定义的编码解码风格,比如 api/domain/resource/{query}, 这个 query 被设计成 “分类-风格-搜索关键词” 的格式,其中“搜索关键词”是用户自定义输入,比如有个资源名叫 “酷大师-最美模型” ,因此这个路由编码之前是 api/domain/resource/分类-风格-酷大师-最美模型,编码之后是 api/domain/resource/%E5%88%86%E7%B1%BB-%E9%A3%8E%E6%A0%BC-%E9%85%B7%E5%A4%A7%E5%B8%88-%E6%9C%80%E7%BE%8E%E6%A8%A1%E5%9E%8B。由于 URL Encode 会保留 - 字符因此当用户的输入带有了保留的特殊字符的时候,会破坏协议本身的语义。

Base62

base62 是一种编码之后只包含了 AZ、az、0~9 62 种字符的编码方式。这种方式可以避开任何符号字符,让 PathVariable 中的变量更加安全。那么选用哪一种 Base62 是最为合适的?

首先,Java 和 Js 自身的 lib 并不支持 base62 编码,现成的库有不少,但都有各自的实现。为了让前后端统一,我选用了 Base62x: An alternative approach to Base64 for non-alphanumeric characters 的实现,并在前后端各自配置了一套实现。这套算法的 GitHub 仓库是 https://github.com/wadelau/Base62x ,在线 encode/decode 地址是 https://ufqi.com/dev/base62x/。