浅谈客户端与服务端的加密通讯(HTTPS/AES/RSA/RequestBodyAdviceAdapter/ResponseBodyAdvice)

前言

  对项目中使用的加密通讯方案以及遇到的问题进行总结.

HTTPS与SSL证书

  • 什么是HTTPS? 全称:Hyper Text Transfer Protocol over SecureSocket Layer,是以安全为目标的 HTTP通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性.HTTPS 在HTTP 的基础下加入SSL,HTTPS 的安全基础是SSL,因此加密的详细内容就需要 SSL. HTTPS 存在不同于 HTTP 的默认端口及一个加密/身份验证层(在 HTTP与 TCP之间).这个系统提供了身份验证与加密通讯方法.

  • 什么是SSL证书? 数字证书的一种,通过在客户端浏览器和Web服务器之间建立一条SSL安全通道Secure socket layer(SSL)安全协议是由Netscape Communication公司设计开发.该安全协议主要用来提供对用户和服务器的认证,对传送的数据进行加密和隐藏,确保数据在传送中不被改变,即数据的完整性,现已成为该领域中全球化的标准。

  • 为什么要防抓包? 抓包就是对客户端与服务端之间的网络通讯进行截获/重发/编辑.通过抓包获取接口地址以及参数信息,可以被用来编写恶意脚本,发动CC攻击,DDOS攻击等.

  简单来说就是需要购买认证一个域名,然后给这个域名申请SSL证书(各大云厂商都有免费的与收费的,收费的证书安全系数更高,常规抓包软件无法抓包).如果服务端使用ngnix代理转发,需要在ngnix中增加SSL相关配置.配置完成后:

  • WEB端或者服务端再向服务端请求时须使用域名,如:https://xxx.xxx.com/login.
  • 在浏览器访问服务端会显示加锁图标.
  • 使用域名与证书可以有效防止抓包,但GET接口请求还是不安全的,这里推荐主要业务接口都统一使用POST,包括查询.

AES对称加密

  • 什么是对称加密? 采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密,也称为单密钥加密.

  • 什么是AES? 密码学中的高级加密标准(Advanced Encryption Standard,AES).加密速度快,在目前的计算机体系结构下,没有任何有效的破解手段,绝大部分业务使用AES加密就可以满足需求.

  • 优点: 运算速度快,资源消耗少,理论上无法暴力破解.

  • 缺点: 密钥单一,客户端写死密钥在项目中,泄露风险大.

  • 方案: 客户端与服务端共同使用一个密钥,客户端发起POST请求时,将参数用AES加密成密文,服务端接受到请求后解密.推荐使用RequestBodyAdviceAdapter拦截器解密,见下文.

  • Java工具类:

@Component
public class AESUtil {
    private static final String AES = "AES";
    private static final String CHARSET = "UTF-8";
    private static final String Key = "1234567812345678";//自定义密钥,默认AES-128的密钥长度为16位
    private static final String IV_STRING = "A-16-Byte-String";//偏移量,增加加密复杂度,可以不用
    private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";//默认加密算法
    
    // 加密
    public static String encrypt(String content) {
        String result = "";
        try {
            byte[] contentBytes = content.getBytes(CHARSET);
            byte[] keyBytes = KEY.getBytes(CHARSET);
            byte[] encryptedBytes = aesEncryptBytes(contentBytes, keyBytes);
            Encoder encoder = Base64.getEncoder();
            result = encoder.encodeToString(encryptedBytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    // 解密
    public static String decrypt(String content) {
        String result = "";
        try {
            Decoder decoder = Base64.getDecoder();
            byte[] encryptedBytes = decoder.decode(content);
            byte[] keyBytes = KEY.getBytes(CHARSET);
            byte[] decryptedBytes = aesDecryptBytes(encryptedBytes, keyBytes);
            result = new String(decryptedBytes, CHARSET);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    // 指定key加密
    public static String encryptToken(String content, String key) {
        String result = "";
        try {
            byte[] contentBytes = content.getBytes(CHARSET);
            byte[] keyBytes = key.getBytes(CHARSET);
            byte[] encryptedBytes = aesEncryptBytes(contentBytes, keyBytes);
            Encoder encoder = Base64.getEncoder();
            result = encoder.encodeToString(encryptedBytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    // 指定key解密
    public static String decryptToken(String content, String key) {
        String result = "";
        try {
            Decoder decoder = Base64.getDecoder();
            byte[] encryptedBytes = decoder.decode(content);
            byte[] keyBytes = key.getBytes(CHARSET);
            byte[] decryptedBytes = aesDecryptBytes(encryptedBytes, keyBytes);
            result = new String(decryptedBytes, CHARSET);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    private static byte[] cipherOperation(byte[] contentBytes, byte[] keyBytes, int mode) throws Exception {
        SecretKeySpec secretKey = new SecretKeySpec(keyBytes, AES);99999        Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
        cipher.init(mode, secretKey);
        return cipher.doFinal(contentBytes);
    }

    public static byte[] aesEncryptBytes(byte[] contentBytes, byte[] keyBytes) throws Exception {
        return cipherOperation(contentBytes, keyBytes, Cipher.ENCRYPT_MODE);
    }

    public static byte[] aesDecryptBytes(byte[] contentBytes, byte[] keyBytes) throws Exception {
        return cipherOperation(contentBytes, keyBytes, Cipher.DECRYPT_MODE);
    }

}

特别注意:客户端与服务端要使用同样的AES加密算法,如"AES/ECB/PKCS5Padding"

RSA非对称加密

  • 什么是非对称加密? 密钥成对出现,公开密钥(publickey)和私有密钥(privatekey).如果用公钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。
  • 什么是RSA? RSA是被研究得最广泛的非对称加密算法,从提出到现在已近三十年,经历了各种攻击的考验,逐渐为人们接受,普遍认为是目前最优秀的公钥方案之一.
  • 优点: 密钥成对出现,安全性高,客户端与服务端交换公钥,泄露风险低.
  • 缺点: 加密解密字符串长度有限制,如果超过128字符则需要分段加密,不友好.
  • 方案: 客户端发起POST请求时,将参数用RSA公钥加密,服务端接受到请求后使用RSA私钥解密.
    进阶1: 登录时服务端针对这个客户端生成密钥对,将公钥返回给客户端,自己保存私钥,每个客户端的密钥对不同,有效防止密钥泄露.
    进阶2: 登录时服务端与客户端各自生成密钥对,交换公钥,各自保存私钥,客户端请求时使用服务端公钥加密,服务端返回参数时用客户端公钥加密.
    进阶3: 利用Redis控制密钥对的时效性.
  • 图解:
    在这里插入图片描述

  • Java工具类:
/**
 * RSA工具类
 */
@Slf4j
public class RSAUtil {
    
    private static final String RSA = "RSA";
    private static final String RSAPublicKey = "RSAPublicKey";
    private static final String RSAPrivateKey = "RSAPrivateKey";

    public static void main(String[] args) throws Exception {
        //post请求参数
        String param = "{"password": "123456","phoneNum": "15555555566"}";
        String rsaEncrypt = rsaEncrypt(param);
        log.info("rsaEncrypt: " + rsaEncrypt);
        String rsaDecrypt = rsaDecrypt(rsaEncrypt);
        log.info("rsaDecrypt: " + rsaDecrypt);
    }

	/**
     * 随机生成密钥对
     */
    public static void genKeyPair() {
        try {
            // KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象
            KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(RSA);
            // 初始化密钥对生成器
            keyPairGen.initialize(2048, new SecureRandom());
            // 生成一个密钥对,保存在keyPair中
            KeyPair keyPair = keyPairGen.generateKeyPair();
            RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();   // 得到私钥
            RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();  // 得到公钥
            String publicKeyString = new String(Base64.encodeBase64(publicKey.getEncoded()));
            String privateKeyString = new String(Base64.encodeBase64((privateKey.getEncoded())));
            
            log.info("公钥:{}", publicKeyString);
            log.info("私钥:{}", privateKeyString);
           
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * RSA公钥加密
     */
    public static String rsaEncrypt(String str) {
        String result = "";
        try {
            //base64编码的公钥
            byte[] decoded = Base64.decodeBase64(RSAPublicKey);
            RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance(RSA).generatePublic(new X509EncodedKeySpec(decoded));
            //RSA加密
            Cipher cipher = Cipher.getInstance(RSA);
            cipher.init(Cipher.ENCRYPT_MODE, pubKey);
            result = Base64.encodeBase64String(cipher.doFinal(str.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception e) {
            e.printStackTrace();
        }

        return result;
    }

    /**
     * RSA私钥解密
     */
    public static String rsaDecrypt(String str) {
        String result = "";
        try {
            //64位解码加密后的字符串
            byte[] inputByte = Base64.decodeBase64(str.getBytes(StandardCharsets.UTF_8));
            //base64编码的私钥
            byte[] decoded = Base64.decodeBase64(RSAPrivateKey);
            RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance(RSA).generatePrivate(new PKCS8EncodedKeySpec(decoded));
            //RSA解密
            Cipher cipher = Cipher.getInstance(RSA);
            cipher.init(Cipher.DECRYPT_MODE, priKey);
            result = new String(cipher.doFinal(inputByte));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

}

AES + RSA 组合加密

  • 为什么要使用组合加密? AES固定密钥泄露风险大,RSA受长度限制.
  • 方案: 使用AES对称密码体制对传输数据加密,同时使用RSA不对称密码体制来传送AES的密钥,就可以综合发挥AES和RSA的优点.
  • 图解:
    在这里插入图片描述

  • Java工具类:
/**
 * AES+RSA组合加密
 * 客户端使用随机产生的16位AES的密钥对参数进行AES加密,通过使用RSA公钥对AES密钥进行公钥加密.
 * 服务端对加密后的AES密钥进行RSA私钥解密,拿到密钥原文,对加密后的参数进行AES解密,拿到原始内容.
 */
@Slf4j
public class SecretAUtil {

    private static final String RSAPublicKey = "RSAPublicKey";
    private static final String RSAPrivateKey = "RSAPrivateKey";
    private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";

    public static void main(String[] args) throws Exception {

        //post请求参数,由key跟content两部分组成,key是用RSA公钥加密的AESKey,content是AES加密的原本参数
        String encryptBody = "{"key": "bAgOxdNtAGLFF2ly+8vMJt4M89A2fUDk\/1RM5jxWNdPjTsvuvIqRxJfkQKLOzafdolp7WWw725eaX1ee2CPID7yh2Vn3UcPOFfHeIFZd5Q4ehVd3tHqnv+aY0uj3Q5mDzWo92X1dY67\/wTrii7+D0LQjBHVmBxMEGwQdaXJskZis8lROV0ursfWr0fgZxeN3vEWbuM7EbIzXDDNH9Gp6zH3B27PPJ4+g+nv7sJ90KBM7ocMWzZmKfW+6H1Cis2jI9Gylm9gc71P04M1zKlNuXfw\/nWwAb1ez9pCjTp8AiOKLRjdPEk89ovPveOeaKCtd636wxSamHOuMA1YfUzlxIA=="," +
                ""content": "WgamUOMvavbJZW+kxU6ZT3TCtS\/m2+wwBKXjD9gLqfsG7XoGQf1XRRz7sL8Gdh9FJjAt6b94Nsw6Qip0FlBpLBEOF2f6joBP0bVIDVmne8moLZVpuV2faJhGUVwaZwDJ\/PMfwpFDs\/JBkPcBvtpauGXR+awsG3pkACs1GXxlbKa3e+EeEgii6xLDL54XpG7J"}";

        JSONObject jsonObject = JSON.parseObject(encryptBody);
        String key = String.valueOf(jsonObject.get("key"));
        String content = String.valueOf(jsonObject.get("content"));
        // 1.先使用RSA私钥解密出AESKey
        String AESKey = SecretUtil.rsaDecrypt(key, RSAPrivateKey);
        // 2.使用AESKey解密内容
        String original = SecretUtil.aesDecrypt(content, AESKey);
    }
    
    /**
     * RSA公钥加密
     */
    public static String rsaEncrypt(String str) {
        String result = "";
        try {
            //base64编码的公钥
            byte[] decoded = Base64.decodeBase64(RSAPublicKey);
            RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
            //RSA加密
            Cipher cipher = Cipher.getInstance("RSA");
            cipher.init(Cipher.ENCRYPT_MODE, pubKey);
            result = Base64.encodeBase64String(cipher.doFinal(str.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * RSA私钥解密
     */
    public static String rsaDecrypt(String str, String privateKey) {
        String result = "";
        try {
            //64位解码加密后的字符串
            byte[] inputByte = Base64.decodeBase64(str.getBytes(StandardCharsets.UTF_8));
            //base64编码的私钥
            byte[] decoded = Base64.decodeBase64(privateKey);
            RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));
            //RSA解密
            Cipher cipher = Cipher.getInstance("RSA");
            cipher.init(Cipher.DECRYPT_MODE, priKey);
            result = new String(cipher.doFinal(inputByte));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * AES加密ECB模式PKCS5Padding填充方式
     */
    public static String aesEncrypt(String str, String key) throws Exception {
        Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
        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.encodeBase64(doFinal));
    }

    /**
     * AES解密ECB模式PKCS5Padding填充方式
     */
    public static String aesDecrypt(String str, String key) throws Exception {
        Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
        byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBytes, "AES"));
        byte[] doFinal = cipher.doFinal(Base64.decodeBase64(str));
        return new String(doFinal);
    }

服务端请求参数解密拦截器RequestBodyAdviceAdapter

通过自定义拦截器实现部分接口解密,以及解密开关.

  • 配置类SecretConfig:
@Data
@Component
@ConfigurationProperties(prefix = "secret")
public class SecretConfig {

    /**
     * 是否开启
     */
    private boolean enabled;
    /**
     * 是否扫描注解
     */
    private boolean scanAnnotation;
    /**
     * 扫描自定义注解
     */
    private Class<? extends Annotation> annotationClass = SecretBody.class;
}
  • 配置文件:
secret:
  enabled: true
  scan-annotation: true
  • 自定义注解类@SecretBody:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface SecretBody {
}
  • 自定义解密拦截器:
//解密拦截器
@Slf4j
@ControllerAdvice
@ConditionalOnProperty(prefix = "secret", name = "enabled", havingValue = "true")
@EnableConfigurationProperties({SecretConfig.class})
public class RequestDecryptAdvice extends RequestBodyAdviceAdapter {

    @Autowired
    private SecretConfig secretConfig;
    
    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        //如果有注解
        boolean supportSafeMessage = supportSecretRequest(parameter);
        if (supportSafeMessage) {
            String httpBody;
            InputStream encryptStream = inputMessage.getBody();
            String encryptBody = StreamUtils.copyToString(encryptStream, Charset.defaultCharset());
            try {
                //AES+RSA组合解密
                httpBody = combinationDecryptBody(encryptBody, "userRSAPrivateKey");
            } catch (Exception e) {
                e.printStackTrace();
                log.error("解密失败:" + encryptBody);
                throw new BusinessException(StatusCode.TOKENERROR, "登录超时,请重新登录");
            }
            //返回处理后的消息体给messageConvert
            return new SecretHttpMessage(new ByteArrayInputStream(httpBody.getBytes()), inputMessage.getHeaders());
        }
        return inputMessage;
    }

    /**
     * 是否支持加密消息体
     */
    private boolean supportSecretRequest(MethodParameter methodParameter) {
        if (!secretConfig.isScanAnnotation()) {
            return true;
        }
        //判断class是否存在注解
        if (methodParameter.getContainingClass().getAnnotation(secretConfig.getAnnotationClass()) != null) {
            return true;
        }
        //判断方法是否存在注解
        return methodParameter.getMethodAnnotation(secretConfig.getAnnotationClass()) != null;
    }

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    /**
     * AES+RSA组合解密
     */
    private String combinationDecryptBody(String encryptBody, String privateKey) throws Exception {
        String original;
        JSONObject jsonObject = JSON.parseObject(encryptBody);
        String key = String.valueOf(jsonObject.get("key"));
        String content = String.valueOf(jsonObject.get("content"));
        // 1.先使用RSA私钥解密出AESKey
        String AESKey = SecretUtil.rsaDecrypt(key, privateKey);
        // 2.使用AESKey解密内容
        original = SecretUtil.aesDecrypt(content, AESKey);
        return original;
    }

服务端返回参数加密拦截器ResponseBodyAdvice

  • 自定义拦截器(配置类同上):
//加密拦截器
@Slf4j
@ControllerAdvice
@ConditionalOnProperty(prefix = "secret", name = "enabled", havingValue = "true")
@EnableConfigurationProperties({SecretConfig.class})
public class ResponseEncryptAdvice implements ResponseBodyAdvice<Object> {

    @Autowired
    private SecretConfig secretConfig;

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        //如果有注解
        boolean supportSafeMessage = supportSecretRequest(methodParameter);
        if (supportSafeMessage) {
            try {
                String returnStr;
                //可以在头部中加入标记告诉客户端此接口是密文返回
                response.getHeaders().add("encrypt", "true");
                String srcData = JSON.toJSONString(body);
                //客户端公钥加密
                returnStr = RSAUtil.encrypt(srcData, "userPublicKey");
                return returnStr;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return body;
    }
    
    /**
     * 是否支持加密消息体
     */
    private boolean supportSecretRequest(MethodParameter methodParameter) {
        if (!secretConfig.isScanAnnotation()) {
            return true;
        }
        //判断class是否存在注解
        if (methodParameter.getContainingClass().getAnnotation(secretConfig.getAnnotationClass()) != null) {
            return true;
        }
        //判断方法是否存在注解
        return methodParameter.getMethodAnnotation(secretConfig.getAnnotationClass()) != null;
    }

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>