优雅的处理API 接口敏感数据加密解密
前言
最近遇到了很多网站爬取和反爬取的问题,所以在此分享一种简单的 api 接口加密策略。
我们与客户端的接口交互过程中,为了更高的安全性,防止数据被其他人抓取,可能需要对部分接口进行加密(请求参数加密,服务端解密)、返回信息加密(服务端加密,客户端解密),但也不是所有的接口都这样,有些接口可能不需要,所以我们可以使用注解的方式来轻松达到此要求。
完成代码已发布至 github:https://github.com/wiltonicp/vihacker-cloud
一、定义注解
自定义注解
@Decrypt
和@Encrypt
使用方法:
在接口上面添加 @Encrypt 注解就对这个接口返回的数据进行加密
在接口上面添加 @Decrypt 注解就对这个接口参数进行解密
加密注解
package cc.vihackerframework.core.encrypt.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by Ranger on 2021/9/14
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Encrypt {
}
解密注解
package cc.vihackerframework.core.encrypt.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by Ranger on 2021/9/14
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.PARAMETER})
public @interface Decrypt {
}
二、定义配置文件
用于在配置文件配置加解密密钥,定义一个 ViHackerEncryptProperties 类来读取用户配置的 key:
package cc.vihackerframework.core.encrypt.properties;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Created by Ranger on 2021/9/14
*/
@Getter
@Setter
@ConfigurationProperties(ViHackerEncryptProperties.PREFIX)
public class ViHackerEncryptProperties {
public static final String PREFIX = "vihacker.encrypt";
/**
* 解密密钥
*/
public String key;
}
三、AES 算法加密工具
加密这块有多种方案可以选择,对称加密、非对称加密,其中对称加密又可以使用 AES、DES、3DES 等不同算法,这里我们使用 Java 自带的 Cipher 来实现对称加密,使用 AES 算法
package cc.vihackerframework.core.encrypt.util;
import cc.vihackerframework.core.exception.Asserts;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Random;
/**
* AES加、解密算法工具类
* Created by Ranger on 2021/09/14
*/
@Slf4j
public class AesUtil {
/**
* 加密算法AES
*/
private static final String KEY_ALGORITHM = "AES";
/**
* key的长度,Wrong key size: must be equal to 128, 192 or 256
* 传入时需要16、24、36
*/
private static final Integer KEY_LENGTH = 16 * 8;
/**
* 算法名称/加密模式/数据填充方式
* 默认:AES/ECB/PKCS5Padding
*/
private static final String ALGORITHMS = "AES/ECB/PKCS5Padding";
/**
* 后端AES的key,由静态代码块赋值
*/
public static String key;
/**
* 不能在代码中创建
* JceSecurity.getVerificationResult 会将其put进 private static final Map<Provider,Object>中,导致内存缓便被耗尽
*/
private static final BouncyCastleProvider PROVIDER = new BouncyCastleProvider();
static {
key = getKey();
}
/**
* 获取key
*/
public static String getKey() {
StringBuilder uid = new StringBuilder();
//产生16位的强随机数
Random rd = new SecureRandom();
for (int i = 0; i < KEY_LENGTH / 8; i++) {
//产生0-2的3位随机数
int type = rd.nextInt(3);
switch (type) {
case 0:
//0-9的随机数
uid.append(rd.nextInt(10));
break;
case 1:
//ASCII在65-90之间为大写,获取大写随机
uid.append((char) (rd.nextInt(25) + 65));
break;
case 2:
//ASCII在97-122之间为小写,获取小写随机
uid.append((char) (rd.nextInt(25) + 97));
break;
default:
break;
}
}
return uid.toString();
}
/**
* 加密
*
* @param content 加密的字符串
* @param encryptKey key值
*/
public static String encrypt(String content, String encryptKey) throws Exception {
if(StringUtils.isBlank(encryptKey)){
Asserts.fail("加密的 key 不能为空");
}
//设置Cipher对象
Cipher cipher = Cipher.getInstance(ALGORITHMS, PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), KEY_ALGORITHM));
//调用doFinal
// 转base64
return Base64.encodeBase64String(cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)));
}
/**
* 解密
*
* @param encryptStr 解密的字符串
* @param decryptKey 解密的key值
*/
public static String decrypt(String encryptStr, String decryptKey) throws Exception {
if(StringUtils.isBlank(decryptKey)){
Asserts.fail("解密的 key 不能为空");
}
//base64格式的key字符串转byte
byte[] decodeBase64 = Base64.decodeBase64(encryptStr);
//设置Cipher对象
Cipher cipher = Cipher.getInstance(ALGORITHMS,PROVIDER);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), KEY_ALGORITHM));
//调用doFinal解密
return new String(cipher.doFinal(decodeBase64));
}
}
四、定义统一返回数据对象
用于api 接口统一返回数据模型
package cc.vihackerframework.core.api;
import lombok.Data;
import java.io.Serializable;
/**
* 通用api 返回对象
*
* @author Ranger
* @since 2021/1/15
* @email wilton.icp@gmail.com
*/
@Data
public class ViHackerApiResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 状态码
*/
private long code;
/**
* 提示信息
*/
private String message;
/**
* 时间戳
*/
private long time;
/**
* 数据封装
*/
private T data;
/**
* 数据是否加密
*/
private Boolean encrypt = Boolean.FALSE;
protected ViHackerApiResult() {
}
protected ViHackerApiResult(long code, String message) {
this.code = code;
this.message = message;
this.time = System.currentTimeMillis();
}
protected ViHackerApiResult(long code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
this.time = System.currentTimeMillis();
}
/**
* 成功
*
* @param <T>
* @return
*/
public static <T> ViHackerApiResult<T> success() {
return new ViHackerApiResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage());
}
/**
* 成功
*
* @param <T>
* @return
*/
public static <T> ViHackerApiResult<T> success(String message) {
return new ViHackerApiResult<T>(ResultCode.SUCCESS.getCode(), message);
}
/**
* 成功返回结果
*
* @param data 获取的数据
*/
public static <T> ViHackerApiResult<T> data(T data) {
return new ViHackerApiResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/**
* 成功返回结果
*
* @param data 获取的数据
* @param message 提示信息
*/
public static <T> ViHackerApiResult<T> success(T data, String message) {
return new ViHackerApiResult<T>(ResultCode.SUCCESS.getCode(), message, data);
}
/**
* 失败返回结果
*
* @param errorCode 错误码
*/
public static <T> ViHackerApiResult<T> failed(IErrorCode errorCode) {
return new ViHackerApiResult<T>(errorCode.getCode(), errorCode.getMessage(), null);
}
/**
* 失败返回结果
*
* @param errorCode 错误码
* @param message 错误信息
*/
public static <T> ViHackerApiResult<T> failed(IErrorCode errorCode, String message) {
return new ViHackerApiResult<T>(errorCode.getCode(), message);
}
/**
* 失败返回结果
*
* @param message 提示信息
*/
public static <T> ViHackerApiResult<T> failed(String message) {
return new ViHackerApiResult<T>(ResultCode.FAILED.getCode(), message);
}
/**
* 失败返回结果
*/
public static <T> ViHackerApiResult<T> failed() {
return failed(ResultCode.FAILED);
}
/**
* 参数验证失败返回结果
*/
public static <T> ViHackerApiResult<T> validateFailed() {
return failed(ResultCode.VALIDATE_FAILED);
}
/**
* 参数验证失败返回结果
*
* @param message 提示信息
*/
public static <T> ViHackerApiResult<T> validateFailed(String message) {
return new ViHackerApiResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message);
}
/**
* 未登录返回结果
*/
public static <T> ViHackerApiResult<T> unauthorized(T data) {
return new ViHackerApiResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
}
/**
* 未授权返回结果
*/
public static <T> ViHackerApiResult<T> forbidden(T data) {
return new ViHackerApiResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
}
/**
* 自定义返回结果
*/
public static <T> ViHackerApiResult<T> customize(long code, String message) {
return new ViHackerApiResult<T>(code, message);
}
}
五、请求和响应进行预处理
我们有多种方式可以对数据进行拦截,从而去实现加解密,而在SpringMVC 中给我们提供了 ResponseBodyAdvice 和 RequestBodyAdvice,利用这两个工具可以对请求和响应进行预处理,非常方便,所以我们在这利用它们来实现对数据的加密和解密。
接口加密
package cc.vihackerframework.core.encrypt.response;
import cc.vihackerframework.core.api.ViHackerApiResult;
import cc.vihackerframework.core.encrypt.annotation.Encrypt;
import cc.vihackerframework.core.encrypt.properties.ViHackerEncryptProperties;
import cc.vihackerframework.core.encrypt.util.AesUtil;
import cc.vihackerframework.core.exception.Asserts;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.annotation.Resource;
/**
* 接口加密
* Created by Ranger on 2021/9/15
*/
@Slf4j
@ControllerAdvice
public class ViHackerEncryptResponse implements ResponseBodyAdvice<ViHackerApiResult> {
private ObjectMapper om = new ObjectMapper();
@Resource
private ViHackerEncryptProperties encryptProperties;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
return returnType.hasMethodAnnotation(Encrypt.class);
}
@Override
public ViHackerApiResult beforeBodyWrite(ViHackerApiResult body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
String key = encryptProperties.getKey();
if(StringUtils.isBlank(key)){
Asserts.fail("加密的 key 不能为空,请添加如下配置:\nvihacker:\n" +
" encrypt:\n" +
" key:");
}
try {
if (body.getData() != null) {
body.setData(AesUtil.encrypt(om.writeValueAsBytes(body.getData()).toString(), key));
body.setEncrypt(Boolean.TRUE);
}
} catch (Exception e) {
log.error("接口返回加密失败:{}",e.getMessage());
}
return body;
}
}
我们在这实现了 ResponseBodyAdvice
接口,实现了两个方法。
1、supports:用于判断在什么情况下我们去实现加密的流程,我们这里就是判断它是否使用了 @Encrypt
注解。
2、beforeBodyWrite:这个方法一般会在响应之前去执行,所以我们在这对返回的数据进行二次处理,对数据进行加密。
解密接口
package cc.vihackerframework.core.encrypt.response;
import cc.vihackerframework.core.encrypt.annotation.Decrypt;
import cc.vihackerframework.core.encrypt.properties.ViHackerEncryptProperties;
import cc.vihackerframework.core.encrypt.util.AesUtil;
import cc.vihackerframework.core.exception.Asserts;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
/**
* 解密接口
* Created by Ranger on 2021/9/15
*/
@Slf4j
@ControllerAdvice
public class ViHackerDecryptRequest extends RequestBodyAdviceAdapter {
@Resource
private ViHackerEncryptProperties encryptProperties;
@Override
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return methodParameter.hasMethodAnnotation(Decrypt.class) || methodParameter.hasParameterAnnotation(Decrypt.class);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
if(StringUtils.isBlank(encryptProperties.getKey())){
Asserts.fail("解密的 key 不能为空,请添加如下配置:\nvihacker:\n" +
" encrypt:\n" +
" key:");
}
byte[] body = new byte[inputMessage.getBody().available()];
String bodyStr = new String(body);
try {
String decryptStr = AesUtil.decrypt(bodyStr, encryptProperties.getKey());
byte[] decrypt = decryptStr.getBytes();
final ByteArrayInputStream bais = new ByteArrayInputStream(decrypt);
return new HttpInputMessage() {
@Override
public InputStream getBody() throws IOException {
return bais;
}
@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
};
} catch (Exception e) {
log.error("参数解密失败:{}",e.getMessage());
e.printStackTrace();
}
return super.beforeBodyRead(inputMessage, parameter, targetType, converterType);
}
}
我们在这继承了 RequestBodyAdvice
接口的子类 RequestBodyAdviceAdapter
,实现了两个方法。
1、supports:用于判断在什么情况下我们去实现解密的流程,我们这里就是判断它是否使用了 @Decrypt
注解。
2、beforeBodyRead:这个方法一般会在参数转换成具体的对象之前执行,所以我们在这对参数进行,对数据进行加密。
至此,就已经完成了所有的流程。
六、使用测试
application.yml配置
vihacker:
encrypt:
key: Av0qiVlXXtlpu19W
controller
package cc.vihackerframework.demo.controller;
import cc.vihackerframework.core.api.ViHackerApiResult;
import cc.vihackerframework.core.encrypt.annotation.Encrypt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* Created by Ranger on 2021/9/15
*/
@RestController
public class DemoController {
@Encrypt
@GetMapping("/")
public ViHackerApiResult test(){
Map<String,Object> map = new HashMap<>();
map.put("username",1881818181);
map.put("password","admin");
return ViHackerApiResult.data(map);
}
}
调用情况: