前端请求参数统一加密传输到服务端
最近项目是做C端APP项目,为了安全所以进行了请求参数和响应参数加密传输;设计思路仿照https,但是我们并没有秘钥交换这一个步骤,所以只能把秘钥放请求头携带传给后端;先讲一下具体实现如下:
1、APP端每一次请求生成16位随机AES秘钥和IV偏移量,使用后端生成的RSA公钥对这两个参数进行加密,放入请求头;例如:
publicKey:1111111111
{
"key":"xxxxxx",
"iv":"bbbbbb"
}
encryptKey:qqqqqqqqqssssssss #放入请求头
2、APP端使用当次请求的AES秘钥对POST请求的json穿进行加密,并设置到一个约定字段;
{
"业务参数":"参数值",
"timestamp":"时间戳",#使用这两个字段进行防止重复提交和请求
"nonce":"随机字符串"
}
#使用AES秘钥加密上面的业务请求整个json字符串得到aaaaaaaaaaaaaaaaaaa
3、发起正常post请求,服务端通过拦截和自定义注解处理解密请求
{
"data":"aaaaaaaaaaaaaaaaaaa"
}
后端对应上面的加密请求进行处理
对应的RSA秘钥自己找地方配置或存储,整体的代码全部贴上了,还有一些不同的需求处理的话,自行手动解密,例如你使用了springSecurity做权限,登录参数手动处理。
1、自定义注解标注需要参数解密或者响应加密
import cn.hutool.core.annotation.Alias;
import java.lang.annotation.*;
/**
* 该注解标识需要同时经过加密和加签来加固接口安全性
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiSecurity {
@Alias("isSign")
boolean value() default true;
/**
* 是否加签验证,默认开启
* @return
*/
@Alias("value")
boolean isSign() default true;
/**
* 接口请求参数是否需要解密,默认开启
* @return
*/
boolean decryptRequest() default true;
/**
* 接口响应参数是否需要加密,默认开启
* @return
*/
boolean encryptResponse() default true;
}
2、参数解密的切面处理
import com.aiper.app.bean.common.BaseController;
import com.aiper.app.bean.common.CommonConstants;
import com.aiper.app.bean.enums.ExceptionInfoEnum;
import com.aiper.app.bean.exception.ExceptionFactory;
import com.aiper.app.bean.qo.AesKeyQo;
import com.aiper.app.bean.qo.RequestQo;
import com.aiper.app.bean.qo.SignQo;
import com.aiper.app.component.aop.annotation.ApiSecurity;
import com.aiper.app.component.config.RSAConfig;
import com.aiper.app.component.redis.RedisConst;
import com.aiper.app.component.redis.RedisOpsUtils;
import com.aiper.app.component.security.filter.MyRequestWrapper;
import com.aiper.app.util.AES256Util;
import com.aiper.app.util.CacheKeyUtil;
import com.aiper.app.util.RSAUtils;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.security.interfaces.RSAPrivateKey;
import java.util.Objects;
@Aspect
@Component
@Slf4j
public class ApiSecurityAspect {
@Resource
private RSAConfig rsaConfig;
@Pointcut("@annotation(apiSecurity)")
public void securityPointcut(ApiSecurity apiSecurity){}
@Around("securityPointcut(apiSecurity)")
public Object aroundApiSecurity(ProceedingJoinPoint joinPoint, ApiSecurity apiSecurity) throws Throwable {
//=======AOP解密切面通知=======
// 获取request加密传递的参数
HttpServletRequest request = getRequest();
// 只能针对post接口的请求参数requestBody进行统一加解密和加签,这是规定
if (!Objects.equals("POST", request.getMethod())) {
//throw new BizException("只能POST接口才能加密加签操作");
throw ExceptionFactory.create(ExceptionInfoEnum.REQUEST_INVALID_ERROR);
}
// 获取controller接口方法定义的参数
Object[] args = joinPoint.getArgs();
Object[] newArgs = args;
/*// 不支持多个请求,因为解密请求参数之后会json字符串,再根据请求参数的类型映射过去,如果有多个参数就不知道映射关系了
if (args.length > 1) {
//throw new BizException("加密接口方法只支持一个参数,请修改");
throw ExceptionFactory.create(ExceptionInfoEnum.REQUEST_INVALID_ERROR);
}*/
// 判断 加解密总开关是否开启
if(!rsaConfig.getEnable()) {
return joinPoint.proceed(newArgs);
}
// 判断 是否开启参数解密
if(!apiSecurity.decryptRequest()) {
return joinPoint.proceed(newArgs);
}
MyRequestWrapper requestBodyWrapper;
if (request instanceof MyRequestWrapper) {
requestBodyWrapper = (MyRequestWrapper) request;
} else {
requestBodyWrapper = new MyRequestWrapper(request);
}
String body = requestBodyWrapper.getBody();
// 请求数据为空
if(StringUtils.isEmpty(body)) {
throw ExceptionFactory.create(ExceptionInfoEnum.REQUEST_INVALID_ERROR);
}
String encryptKey = request.getHeader(CommonConstants.ENCRYPT_KEY);
if(StringUtils.isEmpty(encryptKey)) {
throw ExceptionFactory.create(ExceptionInfoEnum.REQUEST_INVALID_ERROR);
}
//解密RSA
RSAPrivateKey rsaPrivateKey = RSAUtils.getRSAPrivateKeyByString(rsaConfig.getPrivateKey());
String aesKeyStr = RSAUtils.privateDecrypt(encryptKey, rsaPrivateKey);
log.info("AES Key密文---> {}", encryptKey);
log.info("AES Key密文,解密后的内容---> {}", aesKeyStr);
AesKeyQo aesKey = JSONObject.parseObject(aesKeyStr, AesKeyQo.class);
//log.info("请求数据密文---> {}", body);
RequestQo requestData = JSONObject.parseObject(body, RequestQo.class);
String decodeContent = AES256Util.decode(aesKey.getKey(), requestData.getData(), aesKey.getIv());
log.info("请求数据密文,解密后的内容---> {}", decodeContent);
// 判断 是否要验签
if(apiSecurity.isSign()) {
SignQo signQo = JSONObject.parseObject(decodeContent, SignQo.class);
boolean isTimeout = System.currentTimeMillis() - signQo.getTimestamp() < CommonConstants.REQUEST_DATA_TIMEOUT_MILLISECOND;
if(!isTimeout) {
throw ExceptionFactory.create(ExceptionInfoEnum.REQUEST_DATA_TIMEOUT);
}
String serialNumber = BaseController.getCurrentUser().getSerialNumber();
if(Objects.isNull(serialNumber)) {
serialNumber = request.getRemoteAddr();
}
String cacheKey = CacheKeyUtil.getRequestNonceKey(String.format(RedisConst.NONCE_KEY, serialNumber, signQo.getNonce())).getKey();
if(RedisOpsUtils.checkExist(cacheKey)) {
throw ExceptionFactory.create(ExceptionInfoEnum.DUPLICATE_REQUEST_DATA);
} else {
RedisOpsUtils.setWithExpire(cacheKey, null, RedisConst.NONCE_KEY_EXPIRE);
}
}
//获取接口入参的类
Class<?> c = args[0].getClass();
//将获取解密后的真实参数,封装到接口入参的类中
Object o = JSONObject.parseObject(decodeContent, c);
newArgs = new Object[]{o};
return joinPoint.proceed(newArgs);
}
private HttpServletRequest getRequest() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
return request;
}
}
3、响应数据统一加密
@Order(1)
@ControllerAdvice
@Slf4j
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> {
private final RSAConfig rsaConfig;
public EncryptResponseBodyAdvice(RSAConfig rsaConfig) {
this.rsaConfig = rsaConfig;
}
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
if(!rsaConfig.getEnable()) {
return false;
}
ApiSecurity serializedField = returnType.getMethodAnnotation(ApiSecurity.class);
if (Objects.isNull(serializedField)){
return false;
}
return serializedField.encryptResponse();
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
log.info("响应数据明文---> {}", JSONObject.toJSONString(body));
HttpHeaders headers = request.getHeaders();
List<String> encryptKeyList = headers.get("encryptKey");
String encryptKey = encryptKeyList.get(0);
//log.info("AES Key密文---> {}", encryptKey);
//解密RSA
RSAPrivateKey rsaPrivateKey = RSAUtils.getRSAPrivateKeyByString(rsaConfig.getPrivateKey());
String aesKeyStr = RSAUtils.privateDecrypt(encryptKey, rsaPrivateKey);
//log.info("AES Key 密文,解密后的内容---> {}", aesKeyStr);
JSONObject aesKeyJson = JSONObject.parseObject(aesKeyStr);
String encodeResponseStr = AES256Util.encode(aesKeyJson.getString("key"), JSONObject.toJSONString(body), aesKeyJson.getString("iv"));
log.info("响应数据,加密后的内容---> {}", encodeResponseStr);
return encodeResponseStr;
}
}
4、RSA/AES加解密工具
import org.apache.commons.io.IOUtils;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RSAUtils {
/**
* 字符集
*/
public static String CHARSET = "UTF-8";
/**
* 生成密钥对
* @param keyLength 密钥长度
* @return KeyPair
*/
public static KeyPair getKeyPair(int keyLength) {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); //默认:RSA/None/PKCS1Padding
keyPairGenerator.initialize(keyLength);
return keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("生成密钥对时遇到异常" + e.getMessage());
}
}
/**
* 获取公钥
*/
public static byte[] getPublicKey(KeyPair keyPair) {
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
return rsaPublicKey.getEncoded();
}
/**
* 获取私钥
*/
public static byte[] getPrivateKey(KeyPair keyPair) {
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
return rsaPrivateKey.getEncoded();
}
/**
* 公钥字符串转PublicKey实例
* @param publicKey 公钥字符串
* @return PublicKey
* @throws Exception e
*/
public static PublicKey getPublicKey(String publicKey) throws Exception {
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKey.getBytes());
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
}
/**
* 私钥字符串转PrivateKey实例
* @param privateKey 私钥字符串
* @return PrivateKey
* @throws Exception e
*/
public static PrivateKey getPrivateKey(String privateKey) throws Exception {
byte[] privateKeyBytes = Base64.getDecoder().decode(privateKey.getBytes());
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
/**
* 获取公钥字符串
* @param keyPair KeyPair
* @return 公钥字符串
*/
public static String getPublicKeyString(KeyPair keyPair){
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); // 得到公钥
return new String(org.apache.commons.codec.binary.Base64.encodeBase64(publicKey.getEncoded()));
}
/**
* 获取私钥字符串
* @param keyPair KeyPair
* @return 私钥字符串
*/
public static String getPrivateKeyString(KeyPair keyPair){
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); // 得到私钥
return new String(org.apache.commons.codec.binary.Base64.encodeBase64((privateKey.getEncoded())));
}
/**
* 公钥加密
* @param data 明文
* @param publicKey 公钥
* @return 密文
*/
public static String publicEncrypt(String data, RSAPublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] bytes = rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET), publicKey.getModulus().bitLength());
return new String(org.apache.commons.codec.binary.Base64.encodeBase64(bytes));
} catch (Exception e) {
throw new RuntimeException("加密字符串[" + data + "]时遇到异常"+ e.getMessage());
}
}
/**
* 私钥解密
* @param data 密文
* @param privateKey 私钥
* @return 明文
*/
public static String privateDecrypt(String data, RSAPrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.getDecoder().decode(data), privateKey.getModulus().bitLength()), CHARSET);
} catch (Exception e) {
throw new RuntimeException("privateKey解密字符串[" + data + "]时遇到异常"+ e.getMessage());
}
}
/**
* 私钥加密
* @param content 明文
* @param privateKey 私钥
* @return 密文
*/
public static String encryptByPrivateKey(String content, RSAPrivateKey privateKey){
try {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] bytes = rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE,content.getBytes(CHARSET), privateKey.getModulus().bitLength());
return new String(org.apache.commons.codec.binary.Base64.encodeBase64(bytes));
} catch (Exception e) {
throw new RuntimeException("privateKey加密字符串[" + content + "]时遇到异常" + e.getMessage());
}
}
/**
* 公钥解密
* @param content 密文
* @param publicKey 私钥
* @return 明文
*/
public static String decryByPublicKey(String content, RSAPublicKey publicKey){
try {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.getDecoder().decode(content), publicKey.getModulus().bitLength()), CHARSET);
} catch (Exception e) {
throw new RuntimeException("publicKey解密字符串[" + content + "]时遇到异常" +e.getMessage());
}
}
public static RSAPublicKey getRSAPublicKeyByString(String publicKey){
try {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPublicKey)keyFactory.generatePublic(keySpec);
} catch (Exception e) {
throw new RuntimeException("String转PublicKey出错" + e.getMessage());
}
}
public static RSAPrivateKey getRSAPrivateKeyByString(String privateKey){
try {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPrivateKey)keyFactory.generatePrivate(pkcs8EncodedKeySpec);
} catch (Exception e) {
throw new RuntimeException("String转PrivateKey出错" + e.getMessage());
}
}
//rsa切割解码 , ENCRYPT_MODE,加密数据 ,DECRYPT_MODE,解密数据
private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] datas, int keySize) {
int maxBlock = 0; //最大块
if (opmode == Cipher.DECRYPT_MODE) {
maxBlock = keySize / 8;
} else {
maxBlock = keySize / 8 - 11;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] buff;
int i = 0;
try {
while (datas.length > offSet) {
if (datas.length - offSet > maxBlock) {
//可以调用以下的doFinal()方法完成加密或解密数据:
buff = cipher.doFinal(datas, offSet, maxBlock);
} else {
buff = cipher.doFinal(datas, offSet, datas.length - offSet);
}
out.write(buff, 0, buff.length);
i++;
offSet = i * maxBlock;
}
} catch (Exception e) {
throw new RuntimeException("加解密阀值为[" + maxBlock + "]的数据时发生异常: " + e.getMessage());
}
byte[] resultDatas = out.toByteArray();
IOUtils.closeQuietly(out);
return resultDatas;
}
}
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.Base64;
public class AES256Util {
private static final String AES = "AES";
/**
* 初始向量IV, 初始向量IV的长度规定为128位16个字节, 初始向量的来源为随机生成.
*/
/**
* 加密解密算法/加密模式/填充方式
*/
//private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS7Padding";
private static final String CIPHER_ALGORITHM = "AES/CBC/ZeroBytePadding";
private static final Base64.Encoder base64Encoder = java.util.Base64.getEncoder();
private static final Base64.Decoder base64Decoder = java.util.Base64.getDecoder();
//通过在运行环境中设置以下属性启用AES-256支持
static {
Security.setProperty("crypto.policy", "unlimited");
}
/*
* 解决java不支持AES/CBC/PKCS7Padding模式解密
*/
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* AES加密
*/
public static String encode(String key, String content, String keyVI) {
try {
javax.crypto.SecretKey secretKey = new javax.crypto.spec.SecretKeySpec(key.getBytes(), AES);
javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, secretKey, new javax.crypto.spec.IvParameterSpec(keyVI.getBytes()));
// 获取加密内容的字节数组(这里要设置为utf-8)不然内容中如果有中文和英文混合中文就会解密为乱码
byte[] byteEncode = content.getBytes(java.nio.charset.StandardCharsets.UTF_8);
// 根据密码器的初始化方式加密
byte[] byteAES = cipher.doFinal(byteEncode);
// 将加密后的数据转换为字符串
return base64Encoder.encodeToString(byteAES);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* AES解密
*/
public static String decode(String key, String content,String keyVI) {
try {
javax.crypto.SecretKey secretKey = new javax.crypto.spec.SecretKeySpec(key.getBytes(), AES);
javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKey, new javax.crypto.spec.IvParameterSpec(keyVI.getBytes()));
// 将加密并编码后的内容解码成字节数组
byte[] byteContent = base64Decoder.decode(content);
// 解密
byte[] byteDecode = cipher.doFinal(byteContent);
return new String(byteDecode, java.nio.charset.StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* AES加密ECB模式PKCS7Padding填充方式
* @param str 字符串
* @param key 密钥
* @return 加密字符串
* @throws Exception 异常信息
*/
public static String aes256ECBPkcs7PaddingEncrypt(String str, String key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(keyBytes, AES));
byte[] doFinal = cipher.doFinal(str.getBytes(StandardCharsets.UTF_8));
return new String(Base64.getEncoder().encode(doFinal));
}
/**
* AES解密ECB模式PKCS7Padding填充方式
* @param str 字符串
* @param key 密钥
* @return 解密字符串
* @throws Exception 异常信息
*/
public static String aes256ECBPkcs7PaddingDecrypt(String str, String key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBytes, AES));
byte[] doFinal = cipher.doFinal(Base64.getDecoder().decode(str));
return new String(doFinal);
}
}
5、响应到前端的数据,前端通过当次请求的AES进行解密
总结
这种方式是对POST请求统一做拦截,这样的话所有的接口都只能写POST,稍微麻烦一点,但是为了安全也是可以接受的,顺便把重复请求一起处理了;服务端也能够放开某些不需要加密的接口,提供第三方使用等。
本文博主手打,请勿转载!