Laravel框架Encrypter接口之Java实现
前言
去年开始负责一个项目,使用用 PHP 语言,Laravel
框架开发,其中登录认证使用了 Laravel 内置的 Encrypter
接口中的 encrypt
和 decrypt
方法,用 Java 重构就一定绕不开这两个关键方法。
1. PHP 运行环境配置
为了可以运行 PHP 代码验证,我们先安装 PHP 运行环境。
- 进如 PHP 官网下载 PHP 7.4.27 版本,有线程不安全和线程安全两种版本,只是简单跑一下 PHP 代码用哪种版本都行,这里出于习惯性下载线程安全版,点击 zip[24.96MB] 下载得到一个压缩包,选择合适的地方解压,将 PHP 目录配置到环境变量。
- 找到
php.ini-development
文件,拷贝一份并重命名为php.ini
然后打开编辑。- 搜索
date.timezone
删掉前面的分号;
,并将值设置为Asia/Shanghai
; - 搜索
extension_dir
、extension=mysqli
、extension=openssl
、extension=pdo_mysql
删掉前面的分号;
;
- 搜索
- 打开
cmd
窗口输入php -v
,可以看到 PHP 版本即安装成功。
2. 创建 Java 工程
创建 maven 工程并引入如下依赖:
<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.20</version>
</dependency>
<!-- php 对象序列化与反序列化 -->
<dependency>
<groupId>org.sction</groupId>
<artifactId>phprpc</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
</dependencies>
我们先看一下 Encrypter.php
文件:
<?php
namespace Contracts\Encryption;
interface Encrypter
{
/**
* Encrypt the given value.
*
* @param string $value
* @param bool $serialize
* @return string
*/
public function encrypt($value, $serialize = true);
/**
* Decrypt the given value.
*
* @param string $payload
* @param bool $unserialize
* @return string
*/
public function decrypt($payload, $unserialize = true);
}
再看看实现类:
<?php
namespace Encryption;
use Contracts\Encryption\DecryptException;
use Contracts\Encryption\Encrypter as EncrypterContract;
use Contracts\Encryption\EncryptException;
use RuntimeException;
class Encrypter implements EncrypterContract
{
/**
* The encryption key.
*
* @var string
*/
protected $key;
/**
* The algorithm used for encryption.
*
* @var string
*/
protected $cipher;
/**
* Create a new encrypter instance.
*
* @param string $key
* @param string $cipher
* @return void
*
* @throws \RuntimeException
*/
public function __construct($key, $cipher = 'AES-128-CBC')
{
$key = (string)$key;
if (static::supported($key, $cipher)) {
$this->key = $key;
$this->cipher = $cipher;
} else {
throw new RuntimeException('The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths.');
}
}
/**
* Determine if the given key and cipher combination is valid.
*
* @param string $key
* @param string $cipher
* @return bool
*/
public static function supported($key, $cipher)
{
$length = mb_strlen($key, '8bit');
return ($cipher === 'AES-128-CBC' && $length === 16) ||
($cipher === 'AES-256-CBC' && $length === 32);
}
/**
* Create a new encryption key for the given cipher.
*
* @param string $cipher
* @return string
*/
public static function generateKey($cipher)
{
return random_bytes($cipher == 'AES-128-CBC' ? 16 : 32);
}
/**
* Encrypt the given value.
*
* @param mixed $value
* @param bool $serialize
* @return string
*
* @throws \Illuminate\Contracts\Encryption\EncryptException
*/
public function encrypt($value, $serialize = true)
{
$iv = random_bytes(openssl_cipher_iv_length($this->cipher));
// First we will encrypt the value using OpenSSL. After this is encrypted we
// will proceed to calculating a MAC for the encrypted value so that this
// value can be verified later as not having been changed by the users.
$value = \openssl_encrypt(
$serialize ? serialize($value) : $value,
$this->cipher, $this->key, 0, $iv
);
if ($value === false) {
throw new EncryptException('Could not encrypt the data.');
}
// Once we get the encrypted value we'll go ahead and base64_encode the input
// vector and create the MAC for the encrypted value so we can then verify
// its authenticity. Then, we'll JSON the data into the "payload" array.
$mac = $this->hash($iv = base64_encode($iv), $value);
$json = json_encode(compact('iv', 'value', 'mac'));
if (json_last_error() !== JSON_ERROR_NONE) {
throw new EncryptException('Could not encrypt the data.');
}
return base64_encode($json);
}
/**
* Encrypt a string without serialization.
*
* @param string $value
* @return string
*/
public function encryptString($value)
{
return $this->encrypt($value, false);
}
/**
* Decrypt the given value.
*
* @param mixed $payload
* @param bool $unserialize
* @return string
*
* @throws \Illuminate\Contracts\Encryption\DecryptException
*/
public function decrypt($payload, $unserialize = true)
{
$payload = $this->getJsonPayload($payload);
$iv = base64_decode($payload['iv']);
// Here we will decrypt the value. If we are able to successfully decrypt it
// we will then unserialize it and return it out to the caller. If we are
// unable to decrypt this value we will throw out an exception message.
$decrypted = \openssl_decrypt(
$payload['value'], $this->cipher, $this->key, 0, $iv
);
if ($decrypted === false) {
throw new DecryptException('Could not decrypt the data.');
}
return $unserialize ? unserialize($decrypted) : $decrypted;
}
/**
* Decrypt the given string without unserialization.
*
* @param string $payload
* @return string
*/
public function decryptString($payload)
{
return $this->decrypt($payload, false);
}
/**
* Create a MAC for the given value.
*
* @param string $iv
* @param mixed $value
* @return string
*/
protected function hash($iv, $value)
{
return hash_hmac('sha256', $iv . $value, $this->key);
}
/**
* Get the JSON array from the given payload.
*
* @param string $payload
* @return array
*
* @throws \Illuminate\Contracts\Encryption\DecryptException
*/
protected function getJsonPayload($payload)
{
$payload = json_decode(base64_decode($payload), true);
// If the payload is not valid JSON or does not have the proper keys set we will
// assume it is invalid and bail out of the routine since we will not be able
// to decrypt the given value. We'll also check the MAC for this encryption.
if (!$this->validPayload($payload)) {
throw new DecryptException('The payload is invalid.');
}
if (!$this->validMac($payload)) {
throw new DecryptException('The MAC is invalid.');
}
return $payload;
}
/**
* Verify that the encryption payload is valid.
*
* @param mixed $payload
* @return bool
*/
protected function validPayload($payload)
{
return is_array($payload) && isset($payload['iv'], $payload['value'], $payload['mac']) &&
strlen(base64_decode($payload['iv'], true)) === openssl_cipher_iv_length($this->cipher);
}
/**
* Determine if the MAC for the given payload is valid.
*
* @param array $payload
* @return bool
*/
protected function validMac(array $payload)
{
$calculated = $this->calculateMac($payload, $bytes = random_bytes(16));
return hash_equals(
hash_hmac('sha256', $payload['mac'], $bytes, true), $calculated
);
}
/**
* Calculate the hash of the given payload.
*
* @param array $payload
* @param string $bytes
* @return string
*/
protected function calculateMac($payload, $bytes)
{
return hash_hmac(
'sha256', $this->hash($payload['iv'], $payload['value']), $bytes, true
);
}
/**
* Get the encryption key.
*
* @return string
*/
public function getKey()
{
return $this->key;
}
}
java 代码如下:
package com.mayee;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.crypto.symmetric.AES;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.phprpc.util.PHPSerializer;
import java.lang.reflect.InvocationTargetException;
import java.util.Objects;
/**
* @program: xgc-shumei
* @description: 重构 php 的 laravel 框架中 Encrypter 类的 encrypt 和 decrypt 方法。
* 该类中默认的加/解密方式为 AES-128-CBC,可通过外部文件 app.php 配置其加密方式和秘钥,这里有外部配置 AES-256-CBC。
* AES-256-CBC 对应秘钥字节长度为 32,偏移量直接长度为 16。
* @author: Bobby.Ma
* @create: 2022-02-10 12:16
**/
@Slf4j
public class Encrypter {
/**
* 秘钥
*/
private final String key = "YqrxNCQvKu5hnHu1qrEb1EzmSagWSzEM12aBASV+kG4=";
/*
php 在外部需要配置加密模式 AES-128-CBC 或 AES-256-CBC,并且只支持这两种模式。
但 java 无需指定模式,会根据秘钥长度来自动判断。
*/
/**
* 加密给定的文本
*
* @param value 原文对象
* @return 密文
*/
public String encrypt(Object value) {
byte[] iv = RandomUtil.randomBytes(16);
byte[] serialize;
PHPSerializer ps = new PHPSerializer();
try {
serialize = ps.serialize(value);
} catch (IllegalAccessException | InvocationTargetException e) {
log.error("Could not serialize the data: {}", e.getMessage());
return null;
}
AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, Base64.decode(key), iv);
String encrypt = aes.encryptBase64(serialize);
String mac = this.hash(Base64.encode(iv), encrypt);
JSONObject json = new JSONObject(true);
json.put("iv", Base64.encode(iv));
json.put("value", encrypt);
json.put("mac", mac);
return Base64.encode(json.toJSONString());
}
/**
* 解密给定的文本
*
* @param payload 密文
* @return 原文
*/
public <T> T decrypt(String payload, Class<T> type) {
JSONObject payloadObj = this.getJsonPayload(payload);
byte[] iv = Base64.decode(payloadObj.getString("iv"));
AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, Base64.decode(key), iv);
byte[] decrypt = aes.decrypt(payloadObj.getString("value"));
PHPSerializer ps = new PHPSerializer();
try {
return (T) ps.unserialize(decrypt, type);
} catch (IllegalAccessException | InvocationTargetException e) {
log.error("Could not unserialize the data: {}", e.getMessage());
}
return null;
}
private JSONObject getJsonPayload(String payload) {
JSONObject payloadObj = JSON.parseObject(Base64.decodeStr(payload));
if (!this.validPayload(payloadObj)) {
throw new RuntimeException("The payload is invalid.");
}
if (!this.validMac(payloadObj)) {
throw new RuntimeException("The MAC is invalid.");
}
return payloadObj;
}
/**
* 验证 payload 是否有效
*
* @param payloadObj
* @return
*/
private boolean validPayload(JSONObject payloadObj) {
if (Objects.nonNull(payloadObj)) {
return payloadObj.containsKey("iv") && payloadObj.containsKey("value") && payloadObj.containsKey("mac") &&
Base64.decode(payloadObj.getString("iv")).length == 16;
}
return false;
}
private boolean validMac(JSONObject payloadObj) {
byte[] bytes = RandomUtil.randomBytes(16);
String calculated = this.calculateMac(payloadObj, bytes);
HMac mac = new HMac(HmacAlgorithm.HmacSHA256, bytes);
return Objects.equals(mac.digestHex(payloadObj.getString("mac")), calculated);
}
private String calculateMac(JSONObject payloadObj, byte[] bytes) {
HMac mac = new HMac(HmacAlgorithm.HmacSHA256, bytes);
return mac.digestHex(this.hash(payloadObj.getString("iv"), payloadObj.getString("value")));
}
private String hash(String iv, String value) {
HMac mac = new HMac(HmacAlgorithm.HmacSHA256, Base64.decode(key));
return mac.digestHex(iv + value);
}
}
3. 验证
- Java 版:
public static void main(String[] args) {
User origin = new User();
origin.setName("bobby");
origin.setId(1);
origin.setAge(26);
Encrypter encrypter = new Encrypter();
String encrypt = encrypter.encrypt(origin);
System.out.println("密文:" + encrypt);
User user = encrypter.decrypt(encrypt, User.class);
System.out.println("原文:" + JSON.toJSONString(user));
}
输出:
密文:eyJpdiI6InE0ZG1pSDNNYmQ0VVBvSm5vTzhueGc9PSIsInZhbHVlIjoiZE9VK05sVVdwd2x4Z3kvcmgvOXJlMjBXdC9PdmFLaHNUdVZZMjF5QzFoVUtjS09Ud3lBaWhOZmJhSm11ME9PU2hBd3BMOXBFZk9ZMkUySnNhelVMTFdzVjcrSUM0Z1laQVY5KzFoRkhaczA9IiwibWFjIjoiMjE3NzNlN2IwY2VjNjMxMzBlNmI5MWMyYjhkNjAwZTQxMmExYzM1MDg0OTc5YzcwNGRmZDBmZmUxMTdmMzgxNyJ9
原文:{"age":26,"id":1,"name":"bobby"}
- PHP 版:
<?php
spl_autoload_register(function ($class_name) {
require_once $class_name . '.php';
});
use Encryption\Encrypter;
$key = 'YqrxNCQvKu5hnHu1qrEb1EzmSagWSzEM12aBASV+kG4=';
$arr = [
'name' => 'bobby',
'id' => 1,
'age' => 26
];
$origin = json_encode($arr);
$cipher = new Encrypter(base64_decode($key), 'AES-256-CBC');
$enc_str = $cipher->encrypt($origin);
echo '密文:' . $enc_str . PHP_EOL;
$dec_str = $cipher->decrypt($enc_str . PHP_EOL);
echo '原文:' . $dec_str . PHP_EOL;
输出:
密文:eyJpdiI6Ik5jeURsVGVaQnZQSytoMGlWTG16NWc9PSIsInZhbHVlIjoiU3VSRnpGTVNaOGROZzNQM2hQbzNiUys2WjNVU3JOMytQb0xJek1DWjZmXC9nVldDQ05sUmJYd3VOVGVSZEhpNmQiLCJtYWMiOiI0MjE4MTZmNGJkMzdlNmI0MTc1MjZlYmRmNTI3NDY3NzVlYTliNmY2NDNlN2Q5N2FmMWQ0ZGZhZjEyNDVlMjlmIn0=
原文:{"name":"bobby","id":1,"age":26}
注:Java 可以解密 PHP 的密文,但 PHP 解密 Java 的密文会提示文件不存在,这是因为 PHP 在解密后反序列化为对象时找不到文件,如果只是对字符串操作则可以相互加解密。
Tip:本文完整示例代码已上传至 GitHub 。代码中php
包下的文件拷贝置 PhpStorm 中可运行验证。
版权所有
版权归属:Mayee