Bootstrap

springboot对web项目post请求参数加密,响应数据加密

前端请求参数统一加密传输到服务端

最近项目是做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,稍微麻烦一点,但是为了安全也是可以接受的,顺便把重复请求一起处理了;服务端也能够放开某些不需要加密的接口,提供第三方使用等。

本文博主手打,请勿转载!

;