涉及到知识点
1.AES 非加密
2.RSA对称
3.LUA 包含中文字符串切割成table
4.JS LUA PHP 对字符串排序
接口安全设计
一、使用token进行用户身份认证
二、使用sign 防止传入参数被篡改
1、理解对称加密与非对称加密
加称加密:加密与解密用的是同样的密钥,特点:快速,简单、效率高、密钥越大,加密越强。常用:AES
非对称加密: 使用了一对密钥,公钥和私钥。私钥只能由一方安全保管,不能外泄,而公钥则可以发给任何请求它的人。非对称加密使用这对密钥中的一个进行加密,而解密则需要另一个密钥。常用:RSA
2、加解密思路
客户端的加密思路须要3步:
i. 将AES密钥使用RAS公钥进行加密
ii. 把参数数据进行AES 加密
iii. 将第i与第ii生成的内容传给服务端。
服务端的解密思路需3步:
i. 获取到client传过来的AES密钥密文和内容密文;
ii. 使用RSA私钥解密从client拿到的AES密钥密文。
iii. 再使用第ii步解密出来的明文密钥。通过AES解密内容的密文。
三、用时间戳防止暴力请求
具体操作:客户端在形成sign值时,除了使用所有参数和token外,再加一个发起请求时的时间戳。即
sign值 = 所有非空参数升序排序+token+timestamp
后端根据当前时间和sign值的时间戳进行比较,差值超过一段时间则不予放行。
JS 方案
import {JSEncrypt} from 'jsencrypt';
import CryptoJS from 'crypto-js';
import md5 from 'js-md5'
// 公钥
const KEY = `-----BEGIN PUBLIC KEY-----
*******************
-----END PUBLIC KEY-----`;
export const encode = (params: any) => {
const isEmptyObj = Object.prototype.toString.call(params) === '[object Object]' && Object.keys(params).length === 0;
const isEmptyAry = Array.isArray(params) && params.length === 0;
if (!params || isEmptyObj || isEmptyAry) {
params = {'dft':'GlGGJ8HNtxlE27JLPqSHRf'};
}
// 1、对参数排序
const str = JSON.stringify(params).replace(/"/g, '');
// 2、对排序后的参数去掉数据类型 如果参数为空 对 null 进行stringify
const hashString = Array.from(str).sort().join('');
console.log('hash排序后', hashString);
const hashMd5 = md5(hashString);
console.log('hash md5后', hashMd5);
// 3、随机生成Aes加密数据的key,用秒级时间戳,同时服务器对时间戳做校验
const key = new Date().getTime() + 'abc'; // 必须16位 才是aes-128-cbc
// 4、用公钥对AesKey加密
const jsEncrypt = new JSEncrypt();
jsEncrypt.setPublicKey(KEY as string);
const keyEncode = jsEncrypt.encrypt(key);
// console.log(hashArray, '用公钥对AesKey加密');
// 5、用AesKey对数据加密
const dataEncode = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(hashMd5), CryptoJS.enc.Utf8.parse(key), {
// mode: CryptoJS.mode.ECB,
iv: CryptoJS.enc.Utf8.parse(key),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString();
console.log(dataEncode, '用AesKey对数据加密');
// 调试用、对加密数据的解密
// const cipherData1 = CryptoJS.DES.decrypt(cipherData, CryptoJS.enc.Utf8.parse(key), {
// mode: CryptoJS.mode.ECB,
// }).toString(CryptoJS.enc.Utf8);
// 6、 数据给到后端时uriEncode一下,base64会导致数据对不上
// 7、 在请求后端API时统一放到header中即可
return {sign: encodeURIComponent(keyEncode), hash: encodeURIComponent(dataEncode),beforeMd5:hashString};
};
php 方案
/**
* Api 签名校验
* @param \EasySwoole\Http\Request $request
* @param \EasySwoole\Http\Response $response
* @return bool
* @throws \ReflectionException
*/
public function decode(Request $request, Response $response): bool
{
/** 组合参数 */
$params = $request->getRequestParam();
$raw = $request->getBody()->__toString();
$rawParams = json_decode($raw, true);
if ($rawParams) {
$params = $rawParams + $params; //重点前面数组覆盖后面数组
}
/** 组合参数 */
$signHeader = urldecode($request->getHeader(AppConst::HEADER_SIGN_SIGN)[0] ?? null);
$hashHeader = urldecode($request->getHeader(AppConst::HEADER_SIGN_HASH)[0] ?? null);
if (superEmpty($signHeader) || superEmpty($hashHeader)) {
return false;
}
/** 解析header签名 */
$rasConf = config("sign.ras", true);
$private = $rasConf['private'] ?? null;
/** 获取不到参数后直接报错 */
if (superEmpty($private)) {
return false;
}
$aesKey = Ras::decode($signHeader, $private);
if (superEmpty($aesKey)) {
return false;
}
/** 解析参数中的签名 */
$result = AesUtil::decode($hashHeader, 'AES-128-CBC', $aesKey, 1, $aesKey);
$string = preg_replace('/\"/', '', json_encode(superEmpty($params) ? null : $params, JSON_UNESCAPED_UNICODE));
/**这里是php的重点 先对字符串进行中文分割 一般涉及到中文字符串的用mb相关函数**/
$stringParts = mb_str_split($string);
sort($stringParts); //根据ASSIC进行排序
$string = implode($stringParts); // 拼接成字符串
$md5 = md5($string);
if ($md5 != $result) {
return false;
}
// 解析失败 报错
if (superEmpty($result)) {
return false;
}
return true;
}
<?php
namespace App\Util;
class AesUtil
{
/**
* 加密
*
* @param string $data 加密明文
* @param string $method 加密方法 (DES-ECB,DES-CBC,DES-CTR,DES-OFB,DES-CFB)
* @param string $aesKey 加密密钥
* @param int $options 数据格式 (0、OPENSSL_RAW_DATA=1 、OPENSSL_ZERO_PADDING=2 、OPENSSL_NO_PADDING=3)
* @param string $vi 密初始化向量(可选)
* @return string
*/
public static function encode(string $data, string $method, string $aesKey, int $options = OPENSSL_RAW_DATA, string $vi = ""): string
{
return base64_encode(openssl_encrypt($data, $method, $aesKey, $options, $vi));
}
/**
* 解密
*
* @param string $data 加密明文
* @param string $method 加密方法 (DES-ECB,DES-CBC,DES-CTR,DES-OFB,DES-CFB)
* @param string $aesKey 加密密钥
* @param int $options 数据格式 (0、OPENSSL_RAW_DATA=1 、OPENSSL_ZERO_PADDING=2 、OPENSSL_NO_PADDING=3)
* @param string $vi 密初始化向量(可选)
* @return string
*/
public static function decode(string $data, string $method, string $aesKey, int $options = OPENSSL_RAW_DATA, string $vi = ""): string
{
return openssl_decrypt(base64_decode($data), $method, $aesKey, $options, $vi);
}
}
class RasUtil
{
/**
* 加密
*/
public static function encode(string $data, string $key, $padding = OPENSSL_PKCS1_PADDING): string
{
$result = '';
openssl_public_encrypt($data, $result, $key, $padding);
return $result;
}
/**
* 解密
*/
public static function decode(string $data, string $key, $padding = OPENSSL_PKCS1_PADDING): string
{
$result = '';
openssl_private_decrypt(base64_decode($data), $result, $key, $padding);
return $result;
}
}
lua 方案
/**这里是LUA的重点 先对字符串进行中文分割 一般涉及到中文字符串的用mb相关函数**/
--将字符串转为table
function GetWordTable(str)
local temp = {}
for uchar in string.gmatch(str, "[%z\1-\127\194-\244][\128-\191]*") do
temp[#temp+1] = uchar
end
return temp
end
local testT = GetWordTable(newStr) --%z:匹配0 *:表示0个至任意多个