赞
踩
近期在项目中因为安全红线要求需要进行接口加解密,在此记录一下。
通过@ControllerAdvice扫描所有接口进行接口加密以及接口解密,本文选择的是AES加密,通过密匙及偏移量加密接口数据。
//加密方法,通过@ControllerAdvice扫描所有接口,对含有@EncryptResponse注解的类或者方法进行加密
- /**
- * 请求响应处理类
- * 对加了@Encrypt的方法的数据进行加密操作
- *
- * @author gzy
- */
- @ControllerAdvice
- @Slf4j
- public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> {
-
- @Override
- public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
- //在这里调用needEncrypet方法判断是否需要加密
- return new NeedCrypto().needEncrypt(returnType);
- }
-
- //这个方法截取了接口中返回的对象,在对对象加密后返回
- @Override
- public Object beforeBodyWrite(Object obj, MethodParameter returnType, MediaType selectedContentType,
- Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest serverHttpRequest,
- ServerHttpResponse serverHttpResponse) {
- // 通过 ServerHttpRequest的实现类ServletServerHttpRequest 获得HttpServletRequest
- ServletServerHttpRequest sshr = (ServletServerHttpRequest) serverHttpRequest;
- // 此处获取到request 是为了取到在拦截器里面设置的一个对象 是我项目需要,可以忽略
- HttpServletRequest request = sshr.getServletRequest();
-
- String returnData;
- String realData = "";
- JSONObject data = new JSONObject();
- try {
- data = (JSONObject) JSONObject.toJSON(obj);
- if(!data.get("data").toString().isEmpty()){
- realData = data.get("data").toString();
- }
- // 添加encry header,告诉前端数据已加密
- serverHttpResponse.getHeaders().add("encrypt", "true");
- // 加密
- returnData = AESOperator.replace(AESUtil.encrypt(realData));
-
- log.debug("接口={},原始数据={},加密后数据={}", request.getRequestURI(), realData, returnData);
- data.put("data", returnData);
- } catch (Exception e) {
- log.error("异常!", e);
- }
- return data;
- }
- }
//解密方法,通过@ControllerAdvice扫描所有接口,对含有@DecryptRequest注解的类或者方法进行解密
- /**
- * 请求数据接收处理类
- * 对加了@Decrypt的方法的数据进行解密操作
- *
- * @author gzy
- */
- @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类,判断是否需要进行加解密,类中是否需要加解密的逻辑可以根据需要进行修改,
目前代码中的逻辑为:注解在类上加密解密整个类,在方法是加密解密单个方法
- /**
- * 判断是否需要加解密
- *
- * @author gzy
- * @date 2021/09/03
- */
- @Configuration
- @Log4j2
- public class NeedCrypto {
-
- //开关变量,在配置文件中配置是否开启加解密功能,不需要可以去掉
- @Value("${encrypt.api.flag}")
- private boolean encryptApiFlag;
-
- private static boolean flag = false;
-
- @PostConstruct
- public void NeedCrypto() {
- flag = encryptApiFlag;
- }
-
- /**
- * 是否需要对结果加密
- * 1.类上标注或者方法上标注,并且都为true
- * 2.有一个标注为false就不需要加密
- */
- 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(!encrypt){
- return false;
- }*/
- }
- if (methodPresentAnno) {
- //方法上标注的是否需要加密
- encrypt = returnType.getMethod().getAnnotation(EncryptResponse.class).value();
- }
- }
- return encrypt;
- }
-
- /**
- * 是否需要参数解密
- * 1.类上标注或者方法上标注,并且都为true
- * 2.有一个标注为false就不需要解密
- */
- 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 (!encrypt) {
- return false;
- }*/
- }
- if (methodPresentAnno) {
- //方法上标注的是否需要解密
- encrypt = parameter.getMethod().getAnnotation(DecryptRequest.class).value();
- }
- }
- return encrypt;
- }
- }
ps:encryptApiFlag变量为开关变量,在配置文件中配置加解密是否生效,不需要可以去除这部分
@Value("${encrypt.api.flag}") private boolean encryptApiFlag;
yml配置文件:
//AESUtil 加解密工具类,这里可以换成自己需要的加密方法
- /**
- * @author gzy
- */
- public class AESUtil {
-
- /**
- * 加密用的Key 可以用26个字母和数字组成 此处使用AES-128-CBC加密模式,key需要为16位。
- */
- 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 (解密)
- /**
- * 加密注解
- *
- * <p>加了此注解的接口(true)将进行数据加密操作
- * 可以放在类上,可以放在方法上 </p>
- *
- * @author gzy
- */
- @Target({ElementType.METHOD, ElementType.TYPE})
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface EncryptResponse {
- /**
- * 是否对结果加密
- */
- boolean value() default true;
- }
- /**
- * 解密注解
- *
- * <p>加了此注解的接口(true)将进行数据解密操作(post的body) 可
- * 以放在类上,可以放在方法上 </p>
- *
- * @author gzy
- */
- @Target({ElementType.METHOD, ElementType.TYPE})
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface DecryptRequest {
- /**
- * 是否对body进行解密
- */
- boolean value() default true;
- }
//加解密操作类,本文使用的是AES,可以换成自己需要的加解密方式
// 提供了两种加解密方式(加入或者不加入偏移量,通过加入偏移量来增加加密算法复杂度)。这里需要注意一点,加密后的字符串需要去除换行符及制表符,不然前端解密时会出现无法解密的问题(下文类中replace方法)。
- /**
- * AES加解密操作类
- */
- public class AESOperator {
-
- /**
- * 加密用的Key 可以用26个字母和数字组成 此处使用AES-128-CBC加密模式,key需要为16位。
- */
- 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);
- }
-
- /**
- * 加密
- *
- * @param sSrc
- * @param sKey
- * @return
- * @throws Exception
- */
- 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));
- }
-
- /**
- * 解密
- *
- * @param sSrc
- * @param sKey
- * @return
- */
- 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();
- }
-
- /**
- * 去除 换行符、制表符
- *
- * @param str
- * @return
- */
- 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 {
- .......
- }
-
- 或
-
- /**
- * 新增菜单
- *
- * @param params menu
- * @return addMenu
- */
- @DecryptRequest
- @EncryptResponse
- @PostMapping("/add")
- public ApiResult addMenu(@RequestBody JSONObject params) {
- Menu menu = params.toJavaObject(Menu.class);
- return menuService.addMenu(menu);
- }
欢迎大家技术交流,有问题可以与我联系,微信13022509053
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。