目录
前言
对项目中使用的加密通讯方案以及遇到的问题进行总结.
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.
- 在浏览器访问服务端会显示加锁图标.
- 移动端不能忽略证书: 需要实现客户端对服务器的校验,认证服务器证书的合法性,当https在握手的协议中返回给客户端的证书应该和保存在客户端本地的证书解析出来的域名一致,才能说明服务器是合法的.
- 使用域名与证书可以有效防止抓包,但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;
}