SpringBoot Starter自定义注解 - 接口加解密

Starter介绍

目标

本章我们将编写一个starter,目标如下:

1、对外提供@OpenAPI 注解,使用此注解它会对接收的请求数据进行解密,对要返回的数据进行加密。

2、完成服务端使用示例

3、完成前端调用示例

加密规则

1、对业务数据进行AES加密,示意代码:encryptData=AES("业务数据", aesKey)

2、对AES的key进行公钥加密,示意代码:encryptKey=RSA(aesKey, 公钥)

3、签名sign=md5(encryptData+encryptKey)

加密后请求示例

Content-Type: application/x-www-form-urlencoded;charset=UTF-8

请求参数

encryptData: p7gqncRzLliA/u/zY4vXeUn
encryptKey: Moxi6Q570jgW2zE+LEZCONd1Zk=
sign: 47e42d6e6daa68c35f858fd69e1adddf

服务端返回示例

-- 未使用@OpenAPI,data为明文的数据
{code: 200, msg: '成功', data: Object}

-- 使用@OpenAPI,data如下
{code: 200, msg: '成功', data: {
  encryptData: p7gqncRzOIqUn
  encryptKey: Moxi6Q570JvL4RIqzZ5d1Zk=
  sign: 47e42d6e6daa68c35f858fd69e1adddf
}}


Starter内容

pom.xml

<?xml version="1.0" encoding="UTF-8"?>

	4.0.0
	
		org.springframework.boot
		spring-boot-starter-parent
		2.0.3.RELEASE
		 
	
	com.v5ba
	openapi-spring-boot-starter
	0.0.1-SNAPSHOT
	jar
	openapi-spring-boot-starter

	
		1.8
		UTF-8
		1.8
		1.8
	
	
		
			org.springframework.boot
			spring-boot-starter
		
		
			org.springframework.boot
			spring-boot-starter-web
		
		
			org.springframework.boot
			spring-boot-starter-aop
		
		
			cn.hutool
			hutool-all
			5.8.3
		
	

定义注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OpenAPI {
}

配置公私钥

@ConfigurationProperties("com.v5ba.common.openapi")
public class OpenAPIProperties {
    private String privateKey;
    private String publicKey;
    //... get and set ...
}

具体代码逻辑

public class OpenAPIAdvice implements MethodBeforeAdvice, AfterReturningAdvice {

    private OpenAPIProperties openAPIProperties;
    public OpenAPIAdvice(OpenAPIProperties openAPIProperties) {
        this.openAPIProperties = openAPIProperties;
    }

    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        if (returnValue instanceof ResponseVO) {
            ResponseVO responseVO = (ResponseVO)returnValue;
            Object obj = responseVO.getData();
            if (obj != null) {
                // 加密数据
                byte[] keyByte = GengerCode.getCode(16).getBytes();
                AES aes = new AES(Mode.ECB, Padding.ISO10126Padding, keyByte);
                EncryptVO encryptVO = new EncryptVO();
                encryptVO.setEncryptData(aes.encryptBase64(JSONUtil.toJsonStr(obj)));
                // 加密key
                RSA rsa = SecureUtil.rsa(null, openAPIProperties.getPublicKey());
                encryptVO.setEncryptKey(rsa.encryptBase64(keyByte, KeyType.PublicKey));
                // 签名
                encryptVO.setSign(SecureUtil.md5(encryptVO.getEncryptData() + encryptVO.getEncryptKey()));
                responseVO.setData(encryptVO);
            }
        }
    }

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        // 解密
        Map requestParam = decryptRequest(attr.getRequest());
        resetParam(method, args, requestParam);
    }

    /**
     * 初始化请求参数
     *
     * 将解密后的参数,设置到入参的对应属性中
     * @param method
     * @param args
     * @param requestParam 解密后的参数
     */
    private void resetParam(Method method, Object[] args, Map requestParam)  {
        Parameter[] parameters = method.getParameters();
        Class<?>[] paramClass = method.getParameterTypes();

        for (int index = 0; index < paramClass.length; index++) {
            Class<?> clazz = paramClass[index];
            Parameter paramObj = parameters[index];
            Annotation[] annotations = paramObj.getAnnotations();
            if (annotations.length != 0) {
                continue;
            }
            if (clazz == HttpServletRequest.class || clazz == HttpServletResponse.class) {
                continue;
            }
            if (ObjectUtil.isBaseType(clazz)) {
                String attrName = paramObj.getName();
                Object attrVal = requestParam.get(attrName);
                args[index] = ObjectUtil.getBaseTypeVal(attrVal, clazz);
            } else {
                Field[] fields = clazz.getDeclaredFields();
                for (Field field : fields) {
                    field.setAccessible(true);
                    try {
                        field.set(args[index], ObjectUtil.getBaseTypeVal(requestParam.get(field.getName()), field.getType()));
                    }catch (Exception e){
                        e.printStackTrace();
                        throw new RuntimeException("参数:"+field.getName()+"设值失败");
                    }
                }
            }
        }
    }

    /**
     * 将请求参数解密
     * @param request
     * @return
     */
    private Map decryptRequest(HttpServletRequest request){
        String sign = request.getParameter("sign");
        String encryptData = request.getParameter("encryptData");
        String encryptKey = request.getParameter("encryptKey");
        // 检查签名
        String signParamContent = encryptData + encryptKey;
        String signStr = SecureUtil.md5(signParamContent);
        if (signStr == null || !signStr.equals(sign)){
            throw new RuntimeException("验签失败");
        }
        // 解密key 获取AES密钥
        byte[] keyByte = null;
        try {
            RSA rsa = SecureUtil.rsa(openAPIProperties.getPrivateKey(), null);
            keyByte = rsa.decrypt(encryptKey, KeyType.PrivateKey);
        }catch (Exception e){
            throw new RuntimeException("参数错误,解密key失败");
        }
        // 解密encrypt
        try {
            AES aes = new AES(Mode.ECB, Padding.ISO10126Padding, keyByte);
            String _data = aes.decryptStr(encryptData);
            return JSONUtil.toBean(_data, Map.class);
        }catch (Exception e){
            throw new RuntimeException( "参数错误,解密encrypt失败");
        }
    }
}

public class ObjectUtil {
    public static boolean isBaseType(Class clazz) {
        if (clazz == String.class || clazz == Byte.class || clazz == Short.class
                || clazz == Integer.class ||clazz == Long.class || clazz == Double.class) {
            return true;
        }
        return false;
    }
    public static  T getBaseTypeVal(Object attrVal, Class clazz) {
        if (attrVal == null) {
            return null;
        }
        T t = null;
        if (clazz == String.class) {
            t = (T) attrVal.toString();
        } else if (clazz == Byte.class) {
            t = (T) Byte.valueOf(attrVal.toString());
        } else if (clazz == Short.class) {
            t = (T) Short.valueOf(attrVal.toString());
        } else if (clazz == Integer.class) {
            t = (T) Integer.valueOf(attrVal.toString());
        } else if (clazz == Long.class) {
            t = (T) Long.valueOf(attrVal.toString());
        } else if (clazz == Double.class) {
            t = (T) Double.valueOf(attrVal.toString());
        }
        return t;
    }
}

定义切面和自动装配

@Configuration
public class OpenAPIConfiguration {
    public static final String traceExecution = "@annotation(com.v5ba.common.openapi.OpenAPI)";

    @Bean
    public OpenAPIProperties getOpenAPIProperties(){
        return new OpenAPIProperties();
    }
    @Bean
    public DefaultPointcutAdvisor openAPIPointcutAdvisor(OpenAPIProperties openAPIProperties) {
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(traceExecution);
        advisor.setPointcut(pointcut);
        advisor.setAdvice(new OpenAPIAdvice(openAPIProperties));
        return advisor;
    }
}

spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.v5ba.common.openapi.OpenAPIConfiguration


使用示例

服务端修改

在业务项目中引入jar 包


  com.v5ba
  openapi-spring-boot-starter
  0.0.1-SNAPSHOT

配置公私钥

com:
  v5ba:
    common:
      openapi:
        privateKey: 设置你的私钥
        publicKey: 设置你的公钥

改动的地方很少,只需要在原接口增加@OpenAPI注解即可

@RestController
public class TestController {
  
    @OpenAPI
    @PostMapping("get")
    public ResponseVO get(String name, User user) throws BaseException {
        return ResponseVO.of();
    }
}

前端修改

修改前代码

//let param = {name: '张三'}}
post: (param) {
  Http.axios.post(url, Qs.stringify(param), {
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
    }
  })
  .then(resp => {
     if(!resp || resp.status != "200"){
        alert("网络异常")
        return false;
     }
     const respData = resp.data;
     if (respData.code == '200') {
         let d = respData.data // 这个就是返回的明文数据
     }
  })
  .catch(error => {

  })
},

修改后要先对参数加密,然后对返回数据解密

//let param = {name: '张三'}}
post: (param) {
  // 将请求参数加密
  let encryptParam = beforePostEncrypt(param)
  Http.axios.post(url, Qs.stringify(encryptParam), {
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
    }
  })
  .then(resp => {
     if(!resp || resp.status != "200"){
        alert("网络异常")
        return false;
     }
     const respData = resp.data;
     if (respData.code == '200') {
         // param是解密后的明文数据
         let param = afterPostEncrypt(respData.data)
     }

  })
  .catch(error => {

  })
},

beforePostEncrypt: (param) => {
    let reqParams = {
        encryptData: "", // AES加密param
        encryptKey: ""
    }
    // console.log("param 加密前:"+JSON.stringify(param))
    // 业务数据加密
    let keyStr = Util.randomStr(16);
    reqParams.encryptData = Encrypt.encryptAES(JSON.stringify(param), keyStr);
    // 密钥加密
    reqParams.encryptKey = Encrypt.encryptRSA(keyStr)
    // 请求签名
    reqParams.sign = Encrypt.md5Str(reqParams.encryptData+reqParams.encryptKey)    
    return reqParams;
},

afterPostEncrypt: (result) => {
    let response = undefined
    if (!result) {
      return response;
    }
    const sign = Encrypt.md5Str(result.encryptData+result.encryptKey)
    if (sign != result.sign){
        alert("验签失败")
        return response;
    }
    const key = Encrypt.decryptRSA(result.encryptKey);
    const encryptStr = Encrypt.decryptAESFromBase64(result.encryptData, key);
    if(!encryptStr){
        alert("未找到响应数据")
        return response;
    }
    response = JSON.parse(encryptStr)
    return response
}


安装两个加密库

npm install jsencrypt
npm install crypto-js

封装RSA、AES和Base64加解密

import JsEncrypt from 'jsencrypt'
import Base64 from 'crypto-js/enc-base64';
import AES from 'crypto-js/aes';
import MD5 from 'crypto-js/md5';
import ENC_UTF8 from 'crypto-js/enc-utf8'
// 加密模式
import MODE from 'crypto-js/mode-ecb'
// 填充方式
import PAD_PKCS from 'crypto-js/pad-iso10126'
const _privKey = '私钥';

const _pubKey = '公钥';

let Encrypt = {
    /**
     * 加密
     * return string
     */
    encryptRSA: function (str) {
        // eslint-disable-next-line
        let jse = new JSEncrypt()
        jse.setPublicKey(_pubKey)
        return jse.encrypt(str)
    },
    /**
     * 解密
     * return {}
     */
    decryptRSA: function (str) {
        // eslint-disable-next-line
        let jse = new JSEncrypt()
        jse.setPrivateKey(_privKey)
        return jse.decrypt(str)
    },
    md5Str: function (str) {
        return MD5(str).toString()
    },
    encryptAES: function (str, key) {
        return AES.encrypt(ENC_UTF8.parse(str), ENC_UTF8.parse(key), {
            // iv: Encrypt.aesIv,
            mode: MODE,
            padding: PAD_PKCS
        }).toString()
    },
    /**
     * 解密
     * return {}
     */
    decryptAESFromBase64: function (base64Str, key) {
        return AES.decrypt(base64Str, ENC_UTF8.parse(key), {
            // iv: Encrypt.aesIv,
            mode: MODE,
            padding: PAD_PKCS
        }).toString(ENC_UTF8)
    },
    /**
     * 加密base64
     * @param {} str 
     */
    encryptBase64: function(str){
        return Base64.stringify(str)
    },
    /**
     * 解密base64
     * @param {*} str 
     */
    decryptBase64: function(str){
        return Base64.parse(str).toString()
    }
};
export default Encrypt;
发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章