再网络传递数据的时候,为了防止数据被篡改,我们会选择对数据进行加密,数据加密分为对称加密和非对称加密。其中RSA和AES,TLS等加密算法是比较常用的。
对称加密是指加密和解密使用相同的密钥的加密方法。其基本流程包括以下步骤:
非对称加密是指加密和解密使用不同的密钥的加密方法,通常称为公钥和私钥。其基本流程包括以下步骤:
结合使用:
在实际应用中,对称加密和非对称加密通常会结合使用以达到安全和效率的平衡。例如:
再上面我们了解了RSA对称加密,那么当我们进行数据交换的时候,如下:
假设有AB两个人,假设前面他们已经交换完毕了公钥。
那么此时当A使用B的公钥加密原始数据然后发送数据给B的时候,它可以再数据的后面再携带上一个原始数据hash计算之后得到的hash值,然后用自己的私钥进行加密。
A将数据发送到B之后,由于数据使用的是B的公钥加密,B可以用私钥解密之后,得到A发送消息的原本内容,然后,B可以使用A的公钥对额外的数字签名进行校验,因为它假设这个数据是A发送的,那么用A的公钥就应该可以解密成功,所以如果数据解密成功之后与A发送的原始消息经过一样的Hash运算之后相等,那么说明没有被篡改,而如果不一致,那么就说明被篡改了。因为第三方是不知道A的私钥信息的,所以他是用自己的私钥去加密,得到的hash会与A进行hash之后的值不同,从而判断数据被篡改了。
https其实不是一个单独的协议,而是数据传输的时候使用TLS/SSL进行了加密而已。而TLS就是一个非常典型的非对称加密,其兼顾了AES和RSA的安全性和速度。
上面我们已经聊完了一个加密数据的交换过程,那么如果有些人就是伪造了一些域名让你去访问怎么办呢?
HTTPS (HTTP Secure) 是一个安全的 HTTP 通道,它通过 SSL/TLS 协议来保证数据的安全传输。在 HTTPS 请求的过程中,证书颁发机构 (CA, Certificate Authority) 扮演了重要的角色。以下是 CA 在保证 HTTPS 请求过程中数据安全交换的方式:
其实前面的第一和第二随机数都是正常传输,预主密钥的得到就是使用RSA了,此时只有客户端和服务端知道预主密钥,之后,对第一和第二随机数使用预主密钥的加密,就可以得到会话密钥,此时加密交互完成。
并且会话密钥只应用在当前会话,每个会话都会新生成一个,所以安全性大大增加。
只有前面的得到预主密钥的过程用RSA,其他地方都是AES,因为,非对称实在太慢了。
我们知道,我们可以再Gateway网关中自定义过滤器,并且实现Ordered接口来对过滤器的执行顺序进行排序。如下图我实现了三个自定义的全局过滤器。
并且,当你实现全局过滤器接口的时候,你必须实现如下方法
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain)
其中exchange参数非常重要,他就是你的请求以及对你请求的响应。而chain就是上面的过滤器链条。
而我们的过滤器链的作用,其实就是对request和response这两个重要的类进行操作。
比如我可以使用exchange.mutate方法来对request和response进行修改。
exchange = exchange.mutate().request(build -> {
try {
build.uri(
new URI("http://localhost:8080/v1/product?productId=1"))
.build();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
).build();
如下是我修改request之前的请求体内容。
如下就是我修改URI之后的request请求体的内容。
在这里我们把这个reqeust给他修改有着重大意义,这意味着只要对加密后的数据进行解密后,去修改这个request中的内容,我们就能再一次成功的将我们的请求路由到我们指定的路径。
而之后,我们最终路由到的请求路径位置,保存在了DefaultServerWebExchange的attributes中。
最后只要进入到ForwardRoutingFilter
在这里请求完成了最终的处理,然后进行转发,发送到对应的处理类去处理。
这时候我们看提供远程服务调用的类的调用栈即可。
上面我们已经聊到了,先使用RSA的方式传递对称密钥,然后之后的请求使用AES来进行加密解密。这样子既保证了安全性也保证了请求的速度。
我就按照上面的说法,简单的实现了一个数字签名,大概方式如下:
公钥获取:
客户端首先通过一个特定的接口从服务器获取RSA公钥。
对称密钥加密:
客户端生成一个随机的对称密钥,然后使用服务器的RSA公钥对这个对称密钥进行加密。
发送加密的对称密钥:
客户端将加密后的对称密钥发送到服务器。
对称密钥解密:
服务器使用自己的RSA私钥解密客户端发送的加密对称密钥,从而得到原始的对称密钥。
加密通信:
从现在开始,客户端和服务器都会使用这个对称密钥来加密和解密他们之间的通信。这包括URL的动态加密、请求和响应的加密解密,以及数字签名的验证等。
数字签名:
为了确保数据的完整性和非否认性,客户端和/或服务器可以使用对称密钥来生成和验证数字签名。
这样,双方都可以确信接收到的数据没有被篡改,并且确实来自预期的发送方。
URL动态加密:
使用对称密钥对URL进行动态加密,以保护URL中的敏感信息,并防止未经授权的访问。
这个流程确保了客户端和服务器之间的通信安全,防止数据被截获或篡改,同时也提供了一个有效的机制来验证通信双方的身份。
具体流程如下:
我们首先需要做的第一步是提供一个接口让前端客户端去访问,
并且获得到我们的公开的RSA公钥,
然后前端拿到这个RSA公钥之后加密自己的对称密钥,
然后再一次发送一个请求,
这个请求携带的是通过RSA公钥加密过后的对称密钥,
然后服务端收到这个对称密钥之后,
通过RSA私钥解密可以得到原本的前端发送的对称密钥。
此时,之后的URL动态加密所需要使用到的密钥,
以及之后请求的数字签名的加密,
都使用AES的方式,
并且使用这个解密后的对称密钥进行加密解密
我们首先在gateway网关提供一个接口用于提供给前端获取RSA公钥
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import javax.annotation.PostConstruct;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/**
* @author: 张锦标
* @date: 2023/10/2 15:13
* SecurityConfig的作用是返回公钥
*/
@Configuration
public class SecurityConfig {
private KeyPair keyPair;
@PostConstruct
public void init() {
// Generate RSA key pair
KeyPairGenerator keyGen;
try {
keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
keyPair = keyGen.genKeyPair();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Failed to generate RSA key pair", e);
}
}
/**
* 提供给前端获取RSA公钥
* @return
*/
@Bean
public RouterFunction publicKeyEndpoint() {
return RouterFunctions.route()
.GET("/public-key", req -> {
String publicKey = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
return ServerResponse.ok().bodyValue(publicKey);
})
.build();
}
public KeyPair getKeyPair() {
return keyPair;
}
}
前端使用得到的公钥对自己的对称密钥进行加密,代码如下:
package blossom.star.project.product;
import org.junit.jupiter.api.Test;
import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
//@SpringBootTest
class RSA {
@Test
void contextLoads() {
}
public static void main(String[] args) throws Exception {
//TODO 2:这里得到的是获取rsa的公钥之后,对对称密钥进行加密,之后就是使用这个对称密钥进行
//数据的加解密
// Replace with your RSA public key
String publicKeyPEM = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvXBSqSyOPb01/uOnhnFN8Hvaz1IQbXnxFzGp9rWBxRAI2p6o67Elr1+SW68JnXx4swq7+z0U+YZSuszsoqwIrn8XF75bpJ+NKLkH7Bpe5A+If78zTihsCoPs+x74FIaJTSiVCzWP9mCaDSVO2bPTwOvqMwQ7xlmTmN9QShCIJ6uBXaggB5aWdpkh/IsIsZXIlzFB5HxA8AYj3u0AyWZO+pNS1fwq2Q7GPwWG7Zl7bCrUjIbG40k/Ef1BjdJBhQakMUq3Zqx+LJP37Tk4FzW47bwD9AiSL4DAXT+sc+Hw1fNspd2qFZBN94h5Pxkxoc9ZBMWB2bFBdRb6zkEg0/2OwwIDAQAB" ;
// Replace with your symmetric key
String symmetricKey = "zhangjinbiao6666";
// Converting PEM to PublicKey
byte[] decoded = Base64.getDecoder().decode(publicKeyPEM);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decoded);
PublicKey publicKey = keyFactory.generatePublic(keySpec);
// Encrypting symmetric key
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedSymmetricKey = cipher.doFinal(symmetricKey.getBytes());
String encryptedSymmetricKeyBase64 = Base64.getEncoder().encodeToString(encryptedSymmetricKey);
// Printing encrypted symmetric key
System.out.println(encryptedSymmetricKeyBase64);
}
}
这里由于我没有前端,不好操作,我就直接暂时写死了,但是具体的实现逻辑就是与前端制定一个唯一的会话id,然后之后只要是同一个会话就可以使用同一个对称密钥,这样子才能进一步保证安全,而不是一直使用同一个对称密钥。
package blossom.star.project.gateway.filter;
import blossom.star.framework.common.constant.HttpStatus;
import blossom.star.project.gateway.config.SecurityConfig;
import blossom.star.project.gateway.util.GatewayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* @author 张锦标
* 对称密钥保存过滤器
* 当前过滤器首先会先获取请求头中的对称密钥
* 如果有,那么获取对称密钥并且保存到Redis中
*/
//@Component
public class SymmetricKeyFilter implements GlobalFilter, Ordered {
@Autowired
private SecurityConfig securityConfig;
@Autowired
private StringRedisTemplate stringRedisTemplate;
//TODO 3:这里会把加密好的对称密钥 解密 然后放入到redis中
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String encryptedSymmetricKey = exchange.getRequest().getHeaders().getFirst("X-Encrypted-Symmetric-Key");
if (encryptedSymmetricKey != null) {
try {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, securityConfig.getKeyPair().getPrivate());
byte[] decryptedKeyBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedSymmetricKey));
//得到对称密钥
String symmetricKey = new String(decryptedKeyBytes, StandardCharsets.UTF_8);
在非阻塞上下文中阻塞调用可能会导致线程饥饿
TODO 需要优化一下这里 来确保每个请求可以唯一对应一个加密密钥
//String sessionId = exchange.getSession().block().getId();
//stringRedisTemplate.opsForValue().set(sessionId, symmetricKey);
String redisSymmetricKey = "symmetric:key:"+1;
stringRedisTemplate.opsForValue().set(redisSymmetricKey, symmetricKey);
} catch (Exception e) {
e.printStackTrace();
String responseBody = "there are something wrong occurs when decrypt your key!!!";
GatewayUtil.responseMessage(exchange,responseBody);
// 获取响应对象
//ServerHttpResponse response = exchange.getResponse();
处理对称密钥出现了问题
//response.setRawStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
//response.getHeaders().setContentType(MediaType.TEXT_PLAIN);
//
返回你想要的字符串
//return response.writeWith(
// Mono.just(response.bufferFactory().wrap(responseBody.getBytes())));
}
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -300;
}
}
比如这里的请求参数为productId=1,然后我们额外发送一个signature=wHYOLLkTn00DVrcmuCFzFQ==,
signature的值就是对这个参数productId=1进行AES加密之后得到的数据。
然后我们再一次对String plaintext = “productId=1&signature=wHYOLLkTn00DVrcmuCFzFQ==”;来进行加密,然后发送的请求以这个为参数。
也就是发送http://localhost:8080/v1/product/encrypt/8lPoJ5k/aHpfgKlxB5A9eUXqZ4MvgpFqN/SwDBVwDbERjBkQw62kfAmfsDW2Bngm
只要后端检测到这个路径有任何一点不对劲,就会直接报错返回。
package blossom.star.project.product;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* @author: 张锦标
* @date: 2023/10/2 17:32
* AES类
*/
public class AES {
//1:首先让前端对请求路径传输进行AES的加密 密钥已经传递
//比如productId=1 ---》wHYOLLkTn00DVrcmuCFzFQ==
//如果有多个 就直接 & 的方式进行拼接然后AES加密即可
//2:signature=wHYOLLkTn00DVrcmuCFzFQ==
//3:然后在对整个URL进行加密传输,传输方式为 /encrypt +
// /5s7/98nWOXAJKujQ7nj66ZhohFdur/pPBzd3Y9kZqeIrZmPvTegG8
// +OYwY6IMr9dXtK9vmZvJoEEsWZT+LLBCQ==
//其中 + 后面的就是我们aes加密后的url ,/encrypt用于表示进行前端的路由
public static void main(String[] args) throws Exception {
//TODO 1:首先设定一下加密的内容 这里直接用java代码加密
String plaintext = "productId=1";
//String plaintext = "productId=1&signature=wHYOLLkTn00DVrcmuCFzFQ==";
String symmetricKey = "zhangjinbiao6666"; // Ensure this key has 16 bytes
String encryptedText = encryptUrl(plaintext, symmetricKey);
System.out.println(encryptedText);
}
public static String encryptUrl(String url, String symmetricKey) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(symmetricKey.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encryptedBytes = cipher.doFinal(url.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedBytes);
}
}
而如果请求的参数被篡改了,比如上面的productId=2,那么有如下图情况
此时验证请求是否被修改的方法就会报错
下面再验证请求是否被篡改的过程中,代码写的可能有一点丑陋。
package blossom.star.project.gateway.filter;
import blossom.star.framework.common.constant.HttpStatus;
import blossom.star.project.gateway.util.CryptoHelper;
import blossom.star.project.gateway.util.GatewayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.net.URISyntaxException;
/**
* @author 张锦标
* 当前类首先会解析加密后的URL
* 当前类用于解析参数 如果参数解密后和signature不一样则返回
* 并且会重新设定路由路径
*/
@Component
public class CryptoFilter implements GlobalFilter, Ordered {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private CryptoHelper cryptoHelper;
//TODO 4:在这里对加密的URL进行解密
//并且会得到路径的参数
//然后对参数进行加密之后和signature比较判断是否被修改
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//String sessionId = exchange.getSession().block().getId();
String redisSymmetricKey = "symmetric:key:" + 1;
//String symmetricKey = stringRedisTemplate.opsForValue().get(sessionId);
String symmetricKey = stringRedisTemplate.opsForValue().get(redisSymmetricKey);
if (symmetricKey == null) {
return GatewayUtil.responseMessage(exchange, "this session has not symmetricKey!!!");
}
try {
//URL动态加密 数字签名 signature
//如果URL已加密,则解密该URL
//path:/v1/product/encrypt/WyYSV30Cor8QX/eWGsQ7yPD3EvNRRS0HF845UOb+KAdwHPKZByMa3250J/z2S4at
//uri:http://localhost:8080/v1/product/encrypt/WyYSV30Cor8QX/eWGsQ7yPD3EvNRRS0HF845UOb+KAdwHPKZByMa3250J/z2S4at
String encryptedUrl = exchange.getRequest().getURI().toString();
String path = exchange.getRequest().getURI().getPath();
String encryptPathParam = path.substring(path.indexOf("/encrypt/") + 9);
String decryptedPathParam = cryptoHelper.decryptUrl(encryptPathParam, symmetricKey);
String decryptedUri =
encryptedUrl.substring(0, encryptedUrl.indexOf("/encrypt/"))
.concat("?").concat(decryptedPathParam);
//这个方法直接修改的是exchange里面的request
exchange = exchange.mutate().request(build -> {
try {
build.uri(new URI(decryptedUri));
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}).build();
//TODO 需要前端这里首先按照前后端约定的加密方式进行一次加密
//然后得到一个signature,放在请求的末尾
//然后对整个URL进行加密请求
// 解析解密后的URL以获取解密的查询参数
UriComponents uriComponents = UriComponentsBuilder.fromUriString(decryptedUri).build();
MultiValueMap decryptedQueryParams = uriComponents.getQueryParams();
// 验证请求参数的签名
String signature = decryptedQueryParams.getFirst("signature");
if (!cryptoHelper.verifySignature(decryptedQueryParams, signature, symmetricKey)) {
return GatewayUtil.responseMessage(exchange,
"the param has something wrong!!!");
}
} catch (Exception e) {
return GatewayUtil.responseMessage(exchange,
"the internal server occurs an error!!!");
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -200;
}
}
package blossom.star.project.gateway.util;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.MultiValueMap;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.Map;
/**
* @author 张锦标
* 密码学工具包
*/
@Configuration
public class CryptoHelper {
public String decryptUrl(String encryptedUrl, String symmetricKey) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(symmetricKey.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedUrl));
return new String(decryptedBytes, StandardCharsets.UTF_8);
}
//解析路径参数并且加密,后判断是否和signature一样
public boolean verifySignature(MultiValueMap queryParams, String signature, String symmetricKey) throws Exception {
StringBuilder sb = new StringBuilder();
for (Map.Entry> entry : queryParams.entrySet()) {
//将签名本身从要验证的数据中排除
if (!"signature".equals(entry.getKey())) {
sb.append(entry.getKey()).append("=").append(String.join(",", entry.getValue())).append("&");
}
}
sb.setLength(sb.length()-1);
String computedSignature = encryptRequestParam(sb.toString(), symmetricKey);
return computedSignature.equals(signature);
}
public static String encryptRequestParam(String requestParam, String symmetricKey) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(symmetricKey.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encryptedBytes = cipher.doFinal(requestParam.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedBytes);
}
}
如果请求的过程中,请求的数据并没有被修改,那么可以正确解析,如下
动态加密其实在上面就已经说了。
可以发现我们发送的实际请求是下面这个,/encrypt/后面的就是我们约定好的加密参数。
http://localhost:8080/v1/product/encrypt/WLB8EDs2LNTsUJpS/aANt0XqZ4MvgpFqN/SwDBVwDbERjBkQw62kfAmfsDW2Bngm
实际再处理过程中会去掉/encrypt,他只是用于标识具体的加密参数位置而已。