近期在项目中因为安全红线要求需要进行接口加解密,在此记录一下。
通过@ControllerAdvice扫描所有接口进行接口加密以及接口解密,本文选择的是AES加密,通过密匙及偏移量加密接口数据。
一.结构://加密方法,通过@ControllerAdvice扫描所有接口,对含有@EncryptResponse注解的类或者方法进行加密
@ControllerAdvice @Slf4j public class EncryptResponseBodyAdvice implements ResponseBodyAdvice
//解密方法,通过@ControllerAdvice扫描所有接口,对含有@DecryptRequest注解的类或者方法进行解密
@ControllerAdvice
@Slf4j
public class DecryptRequestBodyAdvice implements RequestBodyAdvice {
//needDecrypt判断是否需要解密
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class extends HttpMessageConverter>> converterType) {
return new NeedCrypto().needDecrypt(methodParameter);
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class extends HttpMessageConverter>> converterType) {
return body;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class extends HttpMessageConverter>> converterType) throws IOException {
return inputMessage;
}
//拦截接口中的入参,对入参进行解密后返回
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class extends HttpMessageConverter>> converterType) {
JSonObject dealData = new JSonObject();
try {
// 解密操作
JSonObject encryptObject = (JSONObject) JSONObject.toJSON(body);
String srcData = String.valueOf(encryptObject.get("encryptData"));
String decryptData = AESUtil.decrypt(srcData);
dealData = JSON.parseObject(decryptData);
} catch (Exception e) {
log.error("请求body参数格式解析异常!", e);
}
return dealData;
}
}
//NeedCrypto类,判断是否需要进行加解密,类中是否需要加解密的逻辑可以根据需要进行修改,
目前代码中的逻辑为:注解在类上加密解密整个类,在方法是加密解密单个方法
@Configuration
@Log4j2
public class NeedCrypto {
//开关变量,在配置文件中配置是否开启加解密功能,不需要可以去掉
@Value("${encrypt.api.flag}")
private boolean encryptApiFlag;
private static boolean flag = false;
@PostConstruct
public void NeedCrypto() {
flag = encryptApiFlag;
}
public boolean needEncrypt(MethodParameter returnType) {
boolean encrypt = false;
if (flag) {
boolean classPresentAnno = returnType.getContainingClass().isAnnotationPresent(EncryptResponse.class);
boolean methodPresentAnno = returnType.getMethod().isAnnotationPresent(EncryptResponse.class);
if (classPresentAnno) {
//类上标注的是否需要加密
encrypt = returnType.getContainingClass().getAnnotation(EncryptResponse.class).value();
if (encrypt) {
return encrypt;
}
//类不加密,所有都不加密
}
if (methodPresentAnno) {
//方法上标注的是否需要加密
encrypt = returnType.getMethod().getAnnotation(EncryptResponse.class).value();
}
}
return encrypt;
}
public boolean needDecrypt(MethodParameter parameter) {
boolean encrypt = false;
if (flag) {
boolean classPresentAnno = parameter.getContainingClass().isAnnotationPresent(DecryptRequest.class);
boolean methodPresentAnno = parameter.getMethod().isAnnotationPresent(DecryptRequest.class);
if (classPresentAnno) {
//类上标注的是否需要解密
encrypt = parameter.getContainingClass().getAnnotation(DecryptRequest.class).value();
if (encrypt) {
return encrypt;
}
//类不加密,所有都不加密
}
if (methodPresentAnno) {
//方法上标注的是否需要解密
encrypt = parameter.getMethod().getAnnotation(DecryptRequest.class).value();
}
}
return encrypt;
}
}
ps:encryptApiFlag变量为开关变量,在配置文件中配置加解密是否生效,不需要可以去除这部分
@Value("${encrypt.api.flag}")
private boolean encryptApiFlag;
yml配置文件:
//AESUtil 加解密工具类,这里可以换成自己需要的加密方法
public class AESUtil {
public static final String skey = "smkldospd121daaa";
private static final String ivParameter = "1016449182184177";
public static String encrypt(String srcData) throws Exception {
String enString = AESOperator.getInstance().Encrypt(srcData, skey, ivParameter);
return enString;
}
public static String decrypt(String srcData) throws Exception {
String DeString = AESOperator.getInstance().Decrypt(srcData, skey, ivParameter);
return DeString;
}
}
二.自定义注解:
添加在类或方法中判断接口是否需要加解密
@EncryptResponse (加密)
@DecryptRequest (解密)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@documented
public @interface EncryptResponse {
boolean value() default true;
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@documented
public @interface DecryptRequest {
boolean value() default true;
}
三.AES加密
//加解密操作类,本文使用的是AES,可以换成自己需要的加解密方式
// 提供了两种加解密方式(加入或者不加入偏移量,通过加入偏移量来增加加密算法复杂度)。这里需要注意一点,加密后的字符串需要去除换行符及制表符,不然前端解密时会出现无法解密的问题(下文类中replace方法)。
public class AESOperator {
public static final String skey = "smkldospd121daaa";
private static String ivParameter = "1016449182184177";
private static AESOperator instance = null;
private AESOperator() {
}
public static AESOperator getInstance() {
if (instance == null) {
instance = new AESOperator();
}
return instance;
}
public String Encrypt(String encData, String secretKey, String vector) throws Exception {
if (secretKey == null) {
return null;
}
if (secretKey.length() != 16) {
return null;
}
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
byte[] raw = secretKey.getBytes();
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
// 使用CBC模式,需要一个向量iv,可增加加密算法的强度
IvParameterSpec iv = new IvParameterSpec(vector.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
byte[] encrypted = cipher.doFinal(encData.getBytes("utf-8"));
// 此处使用base64做转码。
return new base64Encoder().encode(encrypted);
}
public String encrypt(String sSrc, String sKey) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
byte[] raw = sKey.getBytes();
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
// 使用CBC模式,需要一个向量iv,可增加加密算法的强度
IvParameterSpec iv = new IvParameterSpec(ivParameter.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
byte[] encrypted = cipher.doFinal(sSrc.getBytes("utf-8"));
// 此处使用base64做转码。
return replace(new base64Encoder().encode(encrypted));
}
public String decrypt(String sSrc, String sKey) {
try {
byte[] raw = sKey.getBytes("ASCII");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(ivParameter.getBytes());
cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
// 先用base64解密
byte[] encrypted1 = new base64Decoder().decodeBuffer(sSrc);
byte[] original = cipher.doFinal(encrypted1);
String originalString = new String(original, "utf-8");
return originalString;
} catch (Exception ex) {
return null;
}
}
public String Decrypt(String sSrc, String key, String ivs) {
try {
byte[] raw = key.getBytes("ASCII");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(ivs.getBytes());
cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
// 先用base64解密
byte[] encrypted1 = new base64Decoder().decodeBuffer(sSrc);
byte[] original = cipher.doFinal(encrypted1);
String originalString = new String(original, "utf-8");
return originalString;
} catch (Exception ex) {
return null;
}
}
public static String encodeBytes(byte[] bytes) {
StringBuffer strBuf = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
strBuf.append((char) (((bytes[i] >> 4) & 0xF) + ((int) 'a')));
strBuf.append((char) (((bytes[i]) & 0xF) + ((int) 'a')));
}
return strBuf.toString();
}
public static String replace(String str) {
if (!StringUtil.isEmpty(str)) {
return str.replaceAll("r|n", "");
}
return str;
}
//测试
public static void main(String[] args) throws Exception {
// 需要加密的字串
String cSrc = "{"loginName":"master","secret":"123456"}";
System.out.println(cSrc);
// 加密
String enString = AESOperator.getInstance().Encrypt(cSrc, skey, ivParameter);
System.out.println("加密后的字串是:" + replace(enString));
String test = replace(enString);
// 解密
String DeString = AESOperator.getInstance().Decrypt(test, skey, ivParameter);
System.out.println("解密后的字串是:" + DeString);
}
}
四.实战
注解加在类或者方法上就可以使用啦(类的优先级高于方法)
ps:解密使用时入参不可以直接写实体类,需要先用json接收,使用实体类无法存放加密后的数据
@DecryptRequest
@EncryptResponse
@RestController
@RequestMapping("/menu")
public class MenuController {
.......
}
或
@DecryptRequest
@EncryptResponse
@PostMapping("/add")
public ApiResult addMenu(@RequestBody JSonObject params) {
Menu menu = params.toJavaObject(Menu.class);
return menuService.addMenu(menu);
}
欢迎大家技术交流,有问题可以与我联系,微信13022509053



