由于用JAVA开发的后台接口在和前端PHP人员调试时一直报错,怎么改前端都说都不行,没办法,只好自己动手了.
问题场景
- 某SpringBoot项目定义了一系列的接口,供前端调用,由于网络传输,所以返回时需要加密,这边使用的是AES对对象实体进行加密.
- 前端PHP人员取得了加密数据,并且解密成功了,肉眼观察没有任何问题.
- 然后就出现问题了,解密后的字符串需要转成JSON格式才好处理,我这边也是将对象转成JSON字符串回传的,但前端解析JSON时一直报错,一直报错,一直报错.
- 前端手动复制解密后的字符串进行转JSON是可以的.
前端那边一直解决不了,于是我们这边进行问题复现,自己写 java 代码调用接口,发现返回的加密字符串前后各多了一个双引号,于是就以为问题找到了. - 接下来就是解决问题, controller 层类的方法使用了 @ResponseBody 注解,会自动将返回对象转为 JSON 字符串,由于我们需要加密,于是就在返回对象前,将对象提前转为 JSON 字符串并加密.
- @ResponseBody 对于实体对象会自动转为 JSON 字符串,对于已经是字符串的,会在字符串两端添加上双引号.
- 所以我们要么将接口方法上的 @ResponseBody 注解去掉,直接返回 String 字符串,要么就将加密字符串放到一个对象里面去,最终我们采取了第二种方法.
- JAVA 再次测试时,确实可以了,然后通知前端PHP人员没有问题了,可是结果还是有问题,还是转 JSON 转不了.他们发现解密后的字符串里面多了一些特殊占位符,去掉之后就可以转 JSON 了.于是问题就这样解决了,代码上线了.
- 但是我这边有点上头了,感觉需要找出这些特殊占位符是哪来的,不可能凭空产生, JAVA 测试代码没有问题,于是我就打算用 PHP 再写一段测试代码,来调用接口,看看到底哪里出了问题.
PHP安装
- 网上饶了一圈,发现官网的安装教程就是个坑,然后其他地方的安装教程也很复杂.
- PHP 安装很简单,下载一个 wampserver64 就可以了.
- 然后开发工具使用的是 PhpStorm,官网打开会有点卡.
- 安装好 wampserver 和 PhpStorm 后,打开 PhpStorm ,配置一下 CLI,指向 wampserver 安装包下的 php.exe 文件,就安装好了.
PHP代码编写
这边的代码其实大部分都是网上百度的,因为很简单,只涉及发送 post 请求, AES解密, 并解析数据.
<?php
include("Aes.php");
//使用方法
$post_data = array(
'username' => 'a',
'password' => 'a'
);
// 查询接口
$result = send_post('url', $post_data);
// 返回结果
$result2 = json_decode($result) ->result;
// 解密
// 使用AES解密
$aes = new Aes();
$decode = $aes ->decrypt($result2);
// 打印解密后的字符串的ASC码
for ($i=0; $i < $strlen ; $i++) {
echo $decode[$i]. ' '. ord($decode[$i]);
echo "<br>";
}
// 打印后发现最后多了几个ASC码为0的空值
// 清除空值
//$decode = trim($decode);
// 将 String 转换成 JSON
//$data = json_decode($decode);
// 从 JSON 字符串里面取值
//$code = $data ->data;
// 从数组里面取第12条信息
//$info = $code[13];
// 取出姓名
//$name = $info ->name;
//echo $name;
function send_post($url, $post_data)
{
$postdata = http_build_query($post_data);
$options = array(
'http' => array(
'method' => 'POST',
'header' => 'Content-type:application/x-www-form-urlencoded',
'content' => $postdata,
'timeout' => 15 * 60 // 超时时间(单位:s)
)
);
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
return $result;
}
Aes工具类
<?php
// 使用AES解密
//$aes = new Aes();
//$decode = $aes ->encrypt('Hello World!');
//echo "decode: ". $decode;
/**
* aes 加密 解密类库
* Class Aes
*/
class Aes {
private $key = null;
/**
*
* @param $key 密钥
* @return String
*/
public function __construct() {
// 需要小伙伴在配置文件app.php中定义aeskey
$this->key = 'key';
}
/**
* 加密
* @param String input 加密的字符串
* @param String key 解密的key
* @return HexString
*/
public function encrypt($input) {
$size = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_ECB);
$input = $this->pkcs5_pad($input, $size);
$td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_ECB, '');
$iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
mcrypt_generic_init($td, $this->key, $iv);
$data = mcrypt_generic($td, $input);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
$data = base64_encode($data);
return $data;
}
/**
* 填充方式 pkcs5
* @param String text 原始字符串
* @param String blocksize 加密长度
* @return String
*/
private function pkcs5_pad($text, $blocksize) {
$pad = $blocksize - (strlen($text) % $blocksize);
return $text . str_repeat(chr($pad), $pad);
}
/**
* 解密
* @param String input 解密的字符串
* @param String key 解密的key
* @return String
*/
public function decrypt($sStr) {
$decrypted= mcrypt_decrypt(MCRYPT_RIJNDAEL_128,$this->key,base64_decode($sStr), MCRYPT_MODE_ECB);
$dec_s = strlen($decrypted);
$padding = ord($decrypted[$dec_s-1]);
$decrypted = substr($decrypted, 0, -$padding);
return $decrypted;
}
}
- 使用 PHP 访问我们的接口后,成功获取到了加密数据,在解码并打印出字符串中的每个字符后,发现字符串的最后多个几个ASC码值为0的字符,百度了一下是空值的意思.
- 在使用了 PHP 自带的 trim() 方法对解密字符串进行过滤后,发现转 JSON 成功了.
- 一开始以为这些空值的出现是 AES 加密工具自身的问题,后来直到看了自己的 AES 工具类中的 JAVA 代码,不禁泪流满面,[在最后补0],这哪是补0,这是给我补刀啊.
- 在移除了这段补 0 的代码后,一切正常了,这个问题搞了我差不多3天,心态爆炸.
JAVA AES 工具类加密代码
private static String _encrypt(String sSrc, String sKey) throws Exception {
if (StringUtils.isBlank(sKey)) {
Util.log.error("Key为空null");
throw new ServiceException("Key为空");
}
if (sKey.length() != 16) {//判断key是否为16位
Util.log.error("Key的长度不是16位");
throw new ServiceException("Key的长度不是16位");
}
byte[] raw = sKey.getBytes("ASCII");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] srawt = sSrc.getBytes("UTF-8");
int len = srawt.length;
//计算补0后的长度
while (len % 16 != 0) {
len++;
}
byte[] sraw = new byte[len];
//在最后补0
for (int i = 0; i < len; ++i) {
if (i < srawt.length) {
sraw[i] = srawt[i];
} else {
sraw[i] = 0;
}
}
byte[] encrypted = cipher.doFinal(sraw);
//此处使用BASE64做转码功能,同时能起到2次加密的作用
String originalString = Base64Utils.encodeToString(encrypted);
originalString = originalString.replaceAll("\n", "");
originalString = originalString.replaceAll("\r", "");
return originalString;
}
PS. 这个工具类是复制的前人的,我错了,下次我再也不做伸手党了,另外 JAVA代码没有出现这样的问题是因为 JAVA 代码解密时已经做了 trim()处理,如果不做处理也是有问题的.
总结
- 别人的代码好用是好用,就是出了问题应该第一个就要联想到它,否则就像我一样,浪费时间不说,还严重影响开发心态.
- 对于 AES 加密原理,和 Base64 加密原理,推荐两篇文章,说的很详细,可以看下面的参考链接.