使用Hashids来保护你的数据库主键
为什么要保护数据库主键?
数据库主键一般是有序自增主键,极易被爬虫抓取数据,作为应用开发者,这是不应该的,你辛辛苦苦收集的数据转眼之间被其他人给抓取了,是不是很大的损失?
Hashids的介绍
generate short unique ids from integers
理解为数字编码库即可,几乎支持市面上所有语言。
available in JavaScript, Ruby, Python, Java, Scala, PHP, Perl, Perl 6, Swift, Clojure, Objective-C, C, C++11, D, F#, Go, Erlang, Lua, Haskell, OCaml, Elixir, Rust, Smalltalk, ColdFusion, Groovy, Kotlin, Nim, VBA, Haxe, Crystal, Elm, ActionScript, CoffeeScript, Bash, R, TSQL, PostgreSQL and for
PHP使用
$hashids = new Hashids\Hashids('this is my salt');
$id = $hashids->encode(1, 2, 3);
$numbers = $hashids->decode($id);
注意
该库并不是一个加密库,所以不建议用来加密敏感数据,我们的数据库主键ID并不是业务上的敏感数据,所以这个没关系。
Yii2的使用
由于该编解码是独立与业务之外的,所以需要处理的地方在下面:
- 接收请求数据的自动解码
- 响应数据的自动编码(本文只针对JSON响应处理,有需要的可以添加ResponseFormatter自行处理)
这两个步骤不应该提现在控制器中,控制器拿到的数据是解码好的,响应的数据是原始数据,然后我们在响应中处理。
代码
助手类(HashidsHelper)
class HashidsHelper {
public static function encode($id)
{
$hashids = new \Hashids\Hashids('salt',16);
return $hashids->encode($id);
}
public static function decode($hash)
{
$hashids = new \Hashids\Hashids('salt',16);
$data= $hashids->decode($hash);
return empty($data)?null:$data;
}
public static function decodeArray(array $hashes)
{
return array_map([HashidsHelper::class, 'decode'], $hashes);
}
/**
* 递归编码
* @param array $data
*/
public static function encodeRecursive(array &$data)
{
foreach ($data as $key => &$value) {
if (is_array($value)) {
self::encodeRecursive($value);
continue;
}
if (strpos($key, 'id') !== false && is_numeric($value)) {
$data[$key] = static::encode($value);
}
}
}
/**
* 递归解码
* @param array $data
*/
public static function decodeRecursive(array &$data)
{
foreach ($data as $key => &$value) {
if (is_array($value)) {
self::decodeRecursive($value);
continue;
}
if (strpos($key, 'id') !== false) {
if (is_string($value)) {
$id = static::decode($value);
$data[$key] = $id ?? $value;
} elseif (is_array($value)) {
$data[$key] = static::decodeArray($value);
}
}
}
}
}
处理请求数据($_POST,$_PUT,$_GET)提交过来的数据
1.新建JsonParser继承Yii自带的JsonParser,代码如下
class JsonParser extends \yii\web\JsonParser
{
/**
* @inheritDoc
*/
public function parse($rawBody, $contentType)
{
$data = parent::parse($rawBody, $contentType);
if ($data !== null) {
HashidsHelper::decodeRecursive($data);
}
return $data;
}
}
2.新建Request集成Yii自带的Request,重写getQueryParams,代码如下:
public function getQueryParams()
{
$data = parent::getQueryParams();
if ($data !== null) {
HashidsHelper::decodeRecursive($data);
}
return $data;
}
3.配置web.php的components,更改为我们自定义的处理器
'request' => [
'class' => \app\components\Request::class,
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => '123456',
'enableCsrfValidation' => false,
'parsers' => [
'application/json' => \app\components\web\JsonParser::class
]
],
处理响应数据
1.新建JsonResponseFormatter继承Yii的JsonResponseFormatter,代码如下:
class JsonResponseFormatter extends \yii\web\JsonResponseFormatter
{
/**
* @inheritDoc
*/
public function format($response)
{
if ($response->data !== null) {
HashidsHelper::encodeRecursive($response->data);
}
parent::format($response);
}
}
2.配置web.php的components,替换response组件
'response' => [
'class' => \app\components\web\Response::class,
'format' => Response::FORMAT_JSON,
'formatters' => [
'json' => [
'class' => \app\components\web\JsonResponseFormatter::class,
'prettyPrint' => YII_DEBUG
]
]
],
测试
1.SiteController添加方法
public function actionA($corporation_id)
{
$data = Yii::$app->request->post();
var_dump($data, $corporation_id);
}
public function actionB()
{
return [
'app_id' => 1,
'app' => [
'app_id' => 2
]
];
}
2.请求测试,这个加密过的hash读者可能解不开,因为我们用的salt不一样,替换为你自己的即可
POST /site/a?corporation_id=XaYeAV2q80pkB4KL
{
"corporation_id": "XaYeAV2q80pkB4KL",
"applet":{
"id":"XaYeAV2q80pkB4KL",
"appid":"xxxxxx"
}
}
3.响应的内容如下:
array(2) {
["corporation_id"]=>
int(1)
["applet"]=>
array(2) {
["id"]=>
int(1)
["appid"]=>
string(6) "xxxxxx"
}
}
int(1)
4.响应测试
GET /site/b
5.响应内容如下
{
"app_id": "XaYeAV2q80pkB4KL",
"app": {
"app_id": "LOnMp3QR5lryDgRK"
}
}
写在最后
不知道这个算不算AOP编程?个人觉得算,在业务逻辑之外处理,业务层对外部输入和自身输出是透明的(理解为业务层自己不知道加解密
)。
本文核心在于两个递归方法,其他语言类似,像nodejs可以使用中间件来处理。