为防止API请求的参数或者返回的结果内容暴露,所以对PHP项目请求的参数和响应的结果进行加密。
下面以ThinkPHP8 为例,也支持 thinkphp6。
准备:
ThinkPHP8 (API端)
vue3(前端,vue2适用)
要点:
一、服务端(API端)配置
①响应(参数)内容加密函数解析,代码说明:
$data = []; //加密的数据
$method = 'AES-128-CBC'; //加密方式 下面说明
$pwd='params'.time(); //加密密钥
$iv='params_iv_123456'; //初始化向量 长度根据加密方式来确定
//加密 使用openssl_encrypt函数进行AES加密
$result = openssl_encrypt(json_encode($data), "AES-128-CBC", $pwd, 0, $iv);
return $result;
/**
* 加密(解密)方式说明
*AES:高级加密标准。这是加密算法的名称(对称加密)。其他对称加密算法有:DES,3-DES等。
*128:这可能是指密钥大小。 AES加密使用3种密钥大小(128位,192位和256位)。
*AES中的块大小也是128位。
*CBC:这是您想要的加密模式。有许多加密模式,这取决于您希望算法运行的速度,并行度和安全级别。
*一些模式*是CBC(密码块链接),ECB(电子密码本),CFB(密码反馈),CTR(计数器)等。
*
*例如:
*AES-128-ECB
*AES-256-CBC
*/
②请求(参数)内容解密,代码说明:
$data = ''; //解密字符串
$method = 'AES-128-CBC'; //解密方式
$pwd='params'.time(); //解密密钥
$iv='params_iv_123456'; //初始化向量 长度根据解密方式来确定
//解密 使用openssl_decrypt函数进行AES解密
$result = openssl_decrypt($data, "AES-128-CBC", $pwd, 0, $iv);
return $result;
完整代码配置(ThinkPHP5以上的项目)在用于目录(app)下的 common.php 进行全局配置:
<?php
// 应用公共文件
use think\exception\HttpResponseException;
use think\Response;
/**
* 应用公共文件
* ①统一数据返回格式
* ②参数加密
* ③参数解密
* ④图片/文件上传
*
*/
/**
* 统一返回数据格式
* 状态码 描述
* 200 请求成功
* 204 请求成功,未返回实体,比如option请求,
* 400 错误的请求
* 401 认证失败,这个一般在token验证那里
* 403 拒绝访问
* 404 请求的资源不存在
* 422 参数验证错误
* 500 服务器错误
*/
function result(string $msg = 'error', array $data = [], int $code = 200, string $type = 'json'): Response
{
$result = [
"code" => $code,
"msg" => $msg,
"data" => $data
];
// 调用Response的create方法,指定code可以改变请求的返回状态码
return Response::create($result, $type)->code($code);
}
/**
* 返回封装后的 API 数据到客户端
* @access protected
* @param mixed $msg 提示信息
* @param mixed $data 要返回的数据
* @param int $code 错误码,默认为0
* @param string $type 输出类型,支持json/xml/jsonp
* @param array $header 发送的 Header 信息
* @return void
* @throws HttpResponseException
* 状态码 描述
* 200 请求成功
* 204 请求成功,未返回实体,比如option请求,
* 400 错误的请求
* 401 认证失败,这个一般在token验证那里
* 403 拒绝访问
* 404 请求的资源不存在
* 422 参数验证错误
* 500 服务器错误
*/
function suc($msg, $data = null, $code = 200)
{
$time = time();
$result = [
'time' => $time,
'key' => $msg,
];
if (!empty($msg)) {
$result['key'] = reqEncode($data, $time);;
}
return result($msg, $result, $code);
}
/**
* 请求参数加密
* openssl_decrypt
* @param $encrypt_key 密钥
* @param $iv 初始化向量 长度根据加密方式来确定
*/
function reqEncode($data, $ekey)
{
$key = \config('req_key');
$ekey = $ekey . $key['admin_key'];
return openssl_encrypt(json_encode($data), "AES-128-CBC", $ekey, 0, $key['admin_iv']);
}
/**
* 请求参数解密
* openssl_decrypt
* @param $encrypt_key 密钥
* @param $iv 初始化向量
*/
function reqDecode()
{
$params = request()->param();//请求获取的参数
if (!empty($params)) {
$key = \config('req_key');
$ekey = $params['time'] . $key['admin_key'];
$data = openssl_decrypt($params['key'], "AES-128-CBC", $ekey, 0, $key['admin_iv']);
return json_decode($data, true);
}
return false;
}
/**
* 后台处理 图片上传
* 上传至本地服务器
* @param $file 文件对象
* @param $path 文件存储路径
*/
function uploadImage($path)
{
// 获取上传文件
$files = request()->file();
//校验是否有非法图片上传
$ckeck = check_illegal($files['file']);
if(!$ckeck){
return suc('非法图片','',1);
}
try {
validate(['image'=>'fileSize:10240|fileExt:jpg,png,jpeg'])
->check($files); //验证类对上传文件的验证,包括文件大小、文件类型和后缀
$savename = '';
foreach($files as $file) {
$savename= \think\facade\Filesystem::disk('public')->putFile( $path,$file,'md5'); //默认根据日期和微秒数生成文件命名规则。这里指定上传文件的命名规则 md5
$savename = request()->domain().'/storage/'.$savename;
}
return suc('上传成功',$savename);
} catch (\think\exception\ValidateException $e) {
return suc($e->getMessage(),'',1);
}
}
/**
* 检测上传图片是否包含有非法代码
*/
function check_illegal($image)
{
if (file_exists($image)) {
$resource = fopen($image, 'rb');
$fileSize = filesize($image);
fseek($resource, 0);
if ($fileSize > 512) { // 取头和尾
$hexCode = bin2hex(fread($resource, 512));
fseek($resource, $fileSize - 512);
$hexCode .= bin2hex(fread($resource, 512));
} else { // 取全部
$hexCode = bin2hex(fread($resource, $fileSize));
}
fclose($resource);
/* 匹配16进制中的 <% ( ) %> */
/* 匹配16进制中的 <? ( ) ?> */
/* 匹配16进制中的 <script | /script> 大小写亦可*/
if (preg_match("/(3c25)|(3c3f.*?706870)|(3C534352495054)|(2F5343524950543E)|(3C736372697074)|(2F7363726970743E)/is", $hexCode)) {
return false;
}
}
return true;
}
二、前端配置(VUE配置)
因为服务端使用的 AES对称加密,所以前端也需要配置 AES 加密和解密。
① 安装扩展 : npm install crypto-js
② 在src目录下新增目录 utils 新增 crypto.js 配置加密解密函数:
// npm install crypto-js
//请求参数加密类
/**
* AES 对称加密(不安全)
*/
import CryptoJS from "crypto-js";
export default {
/**
* 接口数据加密函数
* @param str string 需加密的json字符串
* @param key string 加密key
* @param iv string 加密向量(16位)
* @return string 加密密文字符串
*/
encrypt(str, key, iv) {
//密钥16位
var key = CryptoJS.enc.Utf8.parse(key);
//加密向量16位
var iv = CryptoJS.enc.Utf8.parse(iv);
var encrypted = CryptoJS.AES.encrypt(str, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
},
/**
* 接口数据解密函数
* @param str string 已加密密文
* @param key string 加密key
* @param iv string 加密向量(16位)
* @returns {*|string} 解密之后的json字符串
*/
decrypt(str, KEY, IV) {
var key = CryptoJS.enc.Utf8.parse(KEY);
var iv = CryptoJS.enc.Utf8.parse(IV);
var decrypt = CryptoJS.AES.decrypt(str, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return JSON.parse (decrypt.toString(CryptoJS.enc.Utf8)); //json 转为对象
}
}
并且在 axios 请求中 进行加密和解密,这样达到全局使用配置。如下代码:
/**
* get方法,对应get请求
* @param {String} url [请求的url地址]
* @param {Object} params [请求时携带的参数]
*/
const get = (url, params = {}, token = "") => {
if (token != "") {
axios.defaults.headers.common["X-CSRF-TOKEN"] = token;
}
let param = {};
//参数不为空,加密
if (Object.keys(params).length > 0) {
const nowtime = parseInt(new Date().getTime() / 1000) + "";
param.time = nowtime;
params = JSON.stringify(params); //对象转为字符串
param.key = crypto.encrypt(params, nowtime + reqKey, reqIv);
}
return new Promise((resolve, reject) => {
axios
.get(url, {
params: param,
})
.then((res) => {
let result = res.data;
result.data = crypto.decrypt(
result.data.key,
result.data.time + reqKey,
reqIv
); //解密
resolve(result); //执行
})
.catch((err) => {
// ElMessage.error(err.data.msg);
reject(err.data);
});
});
};
/**
* post方法,对应post请求
* @param {String} url [请求的url地址]
* @param {Object} params [请求时携带的参数]
*/
const post = (url, params = {}, token = "") => {
if (token != "") {
axios.defaults.headers.common["X-CSRF-TOKEN"] = token;
}
//参数不为空,加密
let param = {};
//参数不为空,加密
if (Object.keys(params).length > 0) {
const nowtime = parseInt(new Date().getTime() / 1000) + "";
param.time = nowtime;
params = JSON.stringify(params); //对象转为字符串
param.key = crypto.encrypt(params, nowtime + reqKey, reqIv);
}
return new Promise((resolve, reject) => {
axios
.post(url, param)
.then((res) => {
let result = res.data;
result.data = crypto.decrypt(
result.data.key,
result.data.time + reqKey,
reqIv
); //解密
resolve(result); //执行
})
.catch((err) => {
// ElMessage.error(err.data.msg);
reject(err.data);
});
});
};
以上服务端和客户端都配置完成了,下面可以进行使用了。
三、使用
数据接口加密:
(ThinkPHP5以上的项目)在用于目录(app)下的 common.php 进行封装API返回的数据
/**
*封装返回的数据,并加密,以当前时间为密钥组成数据
*/
function suc($msg, $data = null, $code = 200)
{
$time = time();
$result = [
'time' => $time,
'key' => $msg,
];
if (!empty($msg)) {
$result['key'] = reqEncode($data, $time);
}
return result($msg, $result, $code);
}
完整代码在上面common.php文件了。
输出结果示例:
客户端解密,已在http.js 请求 axios中配置了解密
结果:
至此,整个项目都可以对请求进行加密和解密的使用了。如果文中不明白的,也可以看下项目参考参考:
附上项目地址:
服务端(API)tp8blog: Thinkphp系统 博客API管理
客户端 VUE:https://gitee.com/IronTF/blogadmin_vue
并且开发了适用PHP项目的PHP扩展,更是易用简单。
composer require zishu/myextend