Spring Cloud Gateway 數(shù)字簽名、URL動(dòng)態(tài)加密這樣設(shè)計(jì)真優(yōu)雅!
在網(wǎng)絡(luò)傳遞數(shù)據(jù)的時(shí)候,為了防止數(shù)據(jù)被篡改,我們會(huì)選擇對(duì)數(shù)據(jù)進(jìn)行加密,數(shù)據(jù)加密分為對(duì)稱加密和非對(duì)稱加密。其中RSA和AES,TLS等加密算法是比較常用的。
對(duì)稱加密
對(duì)稱加密是指加密和解密使用相同的密鑰的加密方法。其基本流程包括以下步驟
- 密鑰生成:雙方協(xié)商生成一個(gè)共享密鑰或由一方生成密鑰并安全地傳輸給另一方。
- 加密:使用共享密鑰對(duì)原始數(shù)據(jù)進(jìn)行加密,得到加密后的數(shù)據(jù)。
- 傳輸:將加密后的數(shù)據(jù)傳輸給另一方。
- 解密:接收方使用相同的共享密鑰對(duì)加密數(shù)據(jù)進(jìn)行解密,得到原始數(shù)據(jù)。
非對(duì)稱加密
非對(duì)稱加密是指加密和解密使用不同的密鑰的加密方法,通常稱為公鑰和私鑰。其基本流程包括以下步驟:
- 密鑰對(duì)生成:生成一對(duì)密鑰,一個(gè)是公鑰,另一個(gè)是私鑰。公鑰可以公開(kāi),而私鑰需要保密。
- 公鑰分發(fā):將公鑰發(fā)送給需要加密數(shù)據(jù)的一方。
- 加密:使用公鑰對(duì)原始數(shù)據(jù)進(jìn)行加密,得到加密后的數(shù)據(jù)。
- 傳輸:將加密后的數(shù)據(jù)傳輸給另一方。
- 解密:接收方使用私鑰對(duì)加密數(shù)據(jù)進(jìn)行解密,得到原始數(shù)據(jù)。
結(jié)合使用:
在實(shí)際應(yīng)用中,對(duì)稱加密和非對(duì)稱加密通常會(huì)結(jié)合使用以達(dá)到安全和效率的平衡。例如:
- 使用非對(duì)稱加密交換對(duì)稱密鑰。
- 使用對(duì)稱密鑰進(jìn)行數(shù)據(jù)加密和解密。
什么是數(shù)字簽名
再上面我們了解了RSA對(duì)稱加密,那么當(dāng)我們進(jìn)行數(shù)據(jù)交換的時(shí)候,如下:
假設(shè)有AB兩個(gè)人,假設(shè)前面他們已經(jīng)交換完畢了公鑰。
那么此時(shí)當(dāng)A使用B的公鑰加密原始數(shù)據(jù)然后發(fā)送數(shù)據(jù)給B的時(shí)候,它可以再數(shù)據(jù)的后面再攜帶上一個(gè)原始數(shù)據(jù)hash計(jì)算之后得到的hash值,然后用自己的私鑰進(jìn)行加密。
A將數(shù)據(jù)發(fā)送到B之后,由于數(shù)據(jù)使用的是B的公鑰加密,B可以用私鑰解密之后,得到A發(fā)送消息的原本內(nèi)容,然后,B可以使用A的公鑰對(duì)額外的數(shù)字簽名進(jìn)行校驗(yàn),因?yàn)樗僭O(shè)這個(gè)數(shù)據(jù)是A發(fā)送的,那么用A的公鑰就應(yīng)該可以解密成功,所以如果數(shù)據(jù)解密成功之后與A發(fā)送的原始消息經(jīng)過(guò)一樣的Hash運(yùn)算之后相等,那么說(shuō)明沒(méi)有被篡改,而如果不一致,那么就說(shuō)明被篡改了。因?yàn)榈谌绞遣恢繟的私鑰信息的,所以他是用自己的私鑰去加密,得到的hash會(huì)與A進(jìn)行hash之后的值不同,從而判斷數(shù)據(jù)被篡改了。關(guān)注公眾號(hào):碼猿技術(shù)專欄,回復(fù)關(guān)鍵詞:1111 獲取阿里內(nèi)部Java性能調(diào)優(yōu)手冊(cè)!
HTTPS與CA
https其實(shí)不是一個(gè)單獨(dú)的協(xié)議,而是數(shù)據(jù)傳輸?shù)臅r(shí)候使用TLS/SSL進(jìn)行了加密而已。而TLS就是一個(gè)非常典型的非對(duì)稱加密,其兼顧了AES和RSA的安全性和速度。
上面我們已經(jīng)聊完了一個(gè)加密數(shù)據(jù)的交換過(guò)程,那么如果有些人就是偽造了一些域名讓你去訪問(wèn)怎么辦呢?
HTTPS (HTTP Secure) 是一個(gè)安全的 HTTP 通道,它通過(guò) SSL/TLS 協(xié)議來(lái)保證數(shù)據(jù)的安全傳輸。在 HTTPS 請(qǐng)求的過(guò)程中,證書(shū)頒發(fā)機(jī)構(gòu) (CA, Certificate Authority) 扮演了重要的角色。以下是 CA 在保證 HTTPS 請(qǐng)求過(guò)程中數(shù)據(jù)安全交換的方式:
- 證書(shū)頒發(fā):CA 為服務(wù)器頒發(fā)一個(gè)數(shù)字證書(shū)。這個(gè)證書(shū)包含了服務(wù)器的公鑰和一些識(shí)別服務(wù)器身份的信息。數(shù)字證書(shū)是由 CA 簽名的,以驗(yàn)證證書(shū)的真實(shí)性和完整性。
- 建立安全連接:當(dāng)客戶端第一次連接到服務(wù)器時(shí),服務(wù)器會(huì)發(fā)送其數(shù)字證書(shū)給客戶端??蛻舳藭?huì)驗(yàn)證數(shù)字證書(shū)的合法性,比如檢查證書(shū)是否由一個(gè)受信任的 CA 簽名,檢查證書(shū)是否在有效期內(nèi)等。一旦證書(shū)驗(yàn)證通過(guò),客戶端就能確認(rèn)它是與正確的服務(wù)器進(jìn)行通信,而不是被中間人攻擊。
- 密鑰交換:客戶端和服務(wù)器會(huì)使用 SSL/TLS 協(xié)議中的密鑰交換機(jī)制來(lái)協(xié)商一個(gè)會(huì)話密鑰(通常是一個(gè)對(duì)稱密鑰)。一種常見(jiàn)的方法是客戶端生成一個(gè)隨機(jī)的對(duì)稱密鑰,然后用服務(wù)器的公鑰加密它,再發(fā)送給服務(wù)器。服務(wù)器用自己的私鑰解密得到對(duì)稱密鑰。
- 數(shù)據(jù)加密和傳輸:一旦會(huì)話密鑰被協(xié)商好,客戶端和服務(wù)器就會(huì)用這個(gè)密鑰來(lái)加密和解密傳輸?shù)臄?shù)據(jù)。這樣,即使數(shù)據(jù)在傳輸過(guò)程中被截獲,攻擊者也無(wú)法解讀數(shù)據(jù)的內(nèi)容,因?yàn)樗麄儧](méi)有會(huì)話密鑰。
- 完整性校驗(yàn):SSL/TLS 協(xié)議還提供了數(shù)據(jù)完整性校驗(yàn)。它會(huì)為傳輸?shù)臄?shù)據(jù)生成一個(gè) MAC (Message Authentication Code),以確保數(shù)據(jù)在傳輸過(guò)程中沒(méi)有被篡改。
圖片
其實(shí)前面的第一和第二隨機(jī)數(shù)都是正常傳輸,預(yù)主密鑰的得到就是使用RSA了,此時(shí)只有客戶端和服務(wù)端知道預(yù)主密鑰,之后,對(duì)第一和第二隨機(jī)數(shù)使用預(yù)主密鑰的加密,就可以得到會(huì)話密鑰,此時(shí)加密交互完成。
并且會(huì)話密鑰只應(yīng)用在當(dāng)前會(huì)話,每個(gè)會(huì)話都會(huì)新生成一個(gè),所以安全性大大增加。
只有前面的得到預(yù)主密鑰的過(guò)程用RSA,其他地方都是AES,因?yàn)椋菍?duì)稱實(shí)在太慢了。
Gateway網(wǎng)關(guān)的過(guò)濾器鏈
我們知道,我們可以再Gateway網(wǎng)關(guān)中自定義過(guò)濾器,并且實(shí)現(xiàn)Ordered接口來(lái)對(duì)過(guò)濾器的執(zhí)行順序進(jìn)行排序。如下圖我實(shí)現(xiàn)了三個(gè)自定義的全局過(guò)濾器。
圖片
并且,當(dāng)你實(shí)現(xiàn)全局過(guò)濾器接口的時(shí)候,你必須實(shí)現(xiàn)如下方法
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain)
其中exchange參數(shù)非常重要,他就是你的請(qǐng)求以及對(duì)你請(qǐng)求的響應(yīng)。而chain就是上面的過(guò)濾器鏈條。
圖片
而我們的過(guò)濾器鏈的作用,其實(shí)就是對(duì)request和response這兩個(gè)重要的類進(jìn)行操作。
比如我可以使用exchange.mutate方法來(lái)對(duì)request和response進(jìn)行修改。
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之前的請(qǐng)求體內(nèi)容。
圖片
如下就是我修改URI之后的request請(qǐng)求體的內(nèi)容。
圖片
在這里我們把這個(gè)reqeust給他修改有著重大意義,這意味著只要對(duì)加密后的數(shù)據(jù)進(jìn)行解密后,去修改這個(gè)request中的內(nèi)容,我們就能再一次成功的將我們的請(qǐng)求路由到我們指定的路徑。
圖片
而之后,我們最終路由到的請(qǐng)求路徑位置,保存在了DefaultServerWebExchange的attributes中。
![ttps://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a9bf57e6a521475fb8e403526290533e~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1766&h=1273&s=401141&e=png&b=f9f5f5)
最后只要進(jìn)入到ForwardRoutingFilter
圖片
在這里請(qǐng)求完成了最終的處理,然后進(jìn)行轉(zhuǎn)發(fā),發(fā)送到對(duì)應(yīng)的處理類去處理。
這時(shí)候我們看提供遠(yuǎn)程服務(wù)調(diào)用的類的調(diào)用棧即可。
圖片
如何對(duì)自己的路徑傳輸設(shè)定一個(gè)數(shù)字簽名?
上面我們已經(jīng)聊到了,先使用RSA的方式傳遞對(duì)稱密鑰,然后之后的請(qǐng)求使用AES來(lái)進(jìn)行加密解密。這樣子既保證了安全性也保證了請(qǐng)求的速度。
我就按照上面的說(shuō)法,簡(jiǎn)單的實(shí)現(xiàn)了一個(gè)數(shù)字簽名,大概方式如下:
公鑰獲?。?客戶端首先通過(guò)一個(gè)特定的接口從服務(wù)器獲取RSA公鑰。
對(duì)稱密鑰加密:
客戶端生成一個(gè)隨機(jī)的對(duì)稱密鑰,然后使用服務(wù)器的RSA公鑰對(duì)這個(gè)對(duì)稱密鑰進(jìn)行加密。
發(fā)送加密的對(duì)稱密鑰:
客戶端將加密后的對(duì)稱密鑰發(fā)送到服務(wù)器。
對(duì)稱密鑰解密:
服務(wù)器使用自己的RSA私鑰解密客戶端發(fā)送的加密對(duì)稱密鑰,從而得到原始的對(duì)稱密鑰。
加密通信:
從現(xiàn)在開(kāi)始,客戶端和服務(wù)器都會(huì)使用這個(gè)對(duì)稱密鑰來(lái)加密和解密他們之間的通信。這包括URL的動(dòng)態(tài)加密、請(qǐng)求和響應(yīng)的加密解密,以及數(shù)字簽名的驗(yàn)證等。
數(shù)字簽名:
為了確保數(shù)據(jù)的完整性和非否認(rèn)性,客戶端和/或服務(wù)器可以使用對(duì)稱密鑰來(lái)生成和驗(yàn)證數(shù)字簽名。
這樣,雙方都可以確信接收到的數(shù)據(jù)沒(méi)有被篡改,并且確實(shí)來(lái)自預(yù)期的發(fā)送方。
URL動(dòng)態(tài)加密:
使用對(duì)稱密鑰對(duì)URL進(jìn)行動(dòng)態(tài)加密,以保護(hù)URL中的敏感信息,并防止未經(jīng)授權(quán)的訪問(wèn)。
這個(gè)流程確保了客戶端和服務(wù)器之間的通信安全,防止數(shù)據(jù)被截獲或篡改,同時(shí)也提供了一個(gè)有效的機(jī)制來(lái)驗(yàn)證通信雙方的身份。
具體流程如下:
我們首先需要做的第一步是提供一個(gè)接口讓前端客戶端去訪問(wèn),
并且獲得到我們的公開(kāi)的RSA公鑰,
然后前端拿到這個(gè)RSA公鑰之后加密自己的對(duì)稱密鑰,
然后再一次發(fā)送一個(gè)請(qǐng)求,
這個(gè)請(qǐng)求攜帶的是通過(guò)RSA公鑰加密過(guò)后的對(duì)稱密鑰,
然后服務(wù)端收到這個(gè)對(duì)稱密鑰之后,
通過(guò)RSA私鑰解密可以得到原本的前端發(fā)送的對(duì)稱密鑰。
此時(shí),之后的URL動(dòng)態(tài)加密所需要使用到的密鑰,
以及之后請(qǐng)求的數(shù)字簽名的加密,
都使用AES的方式,
并且使用這個(gè)解密后的對(duì)稱密鑰進(jìn)行加密解密
前端獲取RSA公鑰
我們首先在gateway網(wǎng)關(guān)提供一個(gè)接口用于提供給前端獲取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: 公眾號(hào):碼猿技術(shù)專欄
* @date: 2023/10/2 15:13
* SecurityConfig的作用是返回公鑰
*/
@Configuration
publicclass 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) {
thrownew RuntimeException("Failed to generate RSA key pair", e);
}
}
/**
* 提供給前端獲取RSA公鑰
* @return
*/
@Bean
public RouterFunction<ServerResponse> 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;
}
}
圖片
發(fā)送加密后對(duì)稱密鑰
前端使用得到的公鑰對(duì)自己的對(duì)稱密鑰進(jìn)行加密,代碼如下:
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的公鑰之后,對(duì)對(duì)稱密鑰進(jìn)行加密,之后就是使用這個(gè)對(duì)稱密鑰進(jìn)行
//數(shù)據(jù)的加解密
// 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);
}
}
后端接收當(dāng)前會(huì)話對(duì)稱密鑰并保存
這里由于我沒(méi)有前端,不好操作,我就直接暫時(shí)寫(xiě)死了,但是具體的實(shí)現(xiàn)邏輯就是與前端制定一個(gè)唯一的會(huì)話id,然后之后只要是同一個(gè)會(huì)話就可以使用同一個(gè)對(duì)稱密鑰,這樣子才能進(jìn)一步保證安全,而不是一直使用同一個(gè)對(duì)稱密鑰。
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 公眾號(hào):碼猿技術(shù)專欄
* 對(duì)稱密鑰保存過(guò)濾器
* 當(dāng)前過(guò)濾器首先會(huì)先獲取請(qǐng)求頭中的對(duì)稱密鑰
* 如果有,那么獲取對(duì)稱密鑰并且保存到Redis中
*/
//@Component
publicclass SymmetricKeyFilter implements GlobalFilter, Ordered {
@Autowired
private SecurityConfig securityConfig;
@Autowired
private StringRedisTemplate stringRedisTemplate;
//TODO 3:這里會(huì)把加密好的對(duì)稱密鑰 解密 然后放入到redis中
@Override
public Mono<Void> 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));
//得到對(duì)稱密鑰
String symmetricKey = new String(decryptedKeyBytes, StandardCharsets.UTF_8);
////在非阻塞上下文中阻塞調(diào)用可能會(huì)導(dǎo)致線程饑餓
////TODO 需要優(yōu)化一下這里 來(lái)確保每個(gè)請(qǐng)求可以唯一對(duì)應(yīng)一個(gè)加密密鑰
//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);
// 獲取響應(yīng)對(duì)象
//ServerHttpResponse response = exchange.getResponse();
////處理對(duì)稱密鑰出現(xiàn)了問(wèn)題
//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;
}
}
前端發(fā)送AES加密請(qǐng)求
比如這里的請(qǐng)求參數(shù)為productId=1,然后我們額外發(fā)送一個(gè)signature=wHYOLLkTn00DVrcmuCFzFQ==,signature的值就是對(duì)這個(gè)參數(shù)productId=1進(jìn)行AES加密之后得到的數(shù)據(jù)。
然后我們?cè)僖淮螌?duì)String plaintext = "productId=1&signature=wHYOLLkTn00DVrcmuCFzFQ==";來(lái)進(jìn)行加密,然后發(fā)送的請(qǐng)求以這個(gè)為參數(shù)。
也就是發(fā)送:
http://localhost:8080/v1/product/encrypt/8lPoJ5k/aHpfgKlxB5A9eUXqZ4MvgpFqN/SwDBVwDbERjBkQw62kfAmfsDW2Bngm
只要后端檢測(cè)到這個(gè)路徑有任何一點(diǎn)不對(duì)勁,就會(huì)直接報(bào)錯(cuò)返回。
package blossom.star.project.product;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* @author: 公眾號(hào):碼猿技術(shù)專欄
* @date: 2023/10/2 17:32
* AES類
*/
publicclass AES {
//1:首先讓前端對(duì)請(qǐng)求路徑傳輸進(jìn)行AES的加密 密鑰已經(jīng)傳遞
//比如productId=1 ---》wHYOLLkTn00DVrcmuCFzFQ==
//如果有多個(gè) 就直接 & 的方式進(jìn)行拼接然后AES加密即可
//2:signature=wHYOLLkTn00DVrcmuCFzFQ==
//3:然后在對(duì)整個(gè)URL進(jìn)行加密傳輸,傳輸方式為 /encrypt +
// /5s7/98nWOXAJKujQ7nj66ZhohFdur/pPBzd3Y9kZqeIrZmPvTegG8
// +OYwY6IMr9dXtK9vmZvJoEEsWZT+LLBCQ==
//其中 + 后面的就是我們aes加密后的url ,/encrypt用于表示進(jìn)行前端的路由
public static void main(String[] args) throws Exception {
//TODO 1:首先設(shè)定一下加密的內(nèi)容 這里直接用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);
}
}
驗(yàn)證請(qǐng)求
而如果請(qǐng)求的參數(shù)被篡改了,比如上面的productId=2,那么有如下圖情況:
圖片
此時(shí)驗(yàn)證請(qǐng)求是否被修改的方法就會(huì)報(bào)錯(cuò)。
圖片
下面再驗(yàn)證請(qǐng)求是否被篡改的過(guò)程中,代碼寫(xiě)的可能有一點(diǎn)丑陋。
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 公眾號(hào):碼猿技術(shù)專欄
* 當(dāng)前類首先會(huì)解析加密后的URL
* 當(dāng)前類用于解析參數(shù) 如果參數(shù)解密后和signature不一樣則返回
* 并且會(huì)重新設(shè)定路由路徑
*/
@Component
publicclass CryptoFilter implements GlobalFilter, Ordered {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private CryptoHelper cryptoHelper;
//TODO 4:在這里對(duì)加密的URL進(jìn)行解密
//并且會(huì)得到路徑的參數(shù)
//然后對(duì)參數(shù)進(jìn)行加密之后和signature比較判斷是否被修改
@Override
public Mono<Void> 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動(dòng)態(tài)加密 數(shù)字簽名 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);
//這個(gè)方法直接修改的是exchange里面的request
exchange = exchange.mutate().request(build -> {
try {
build.uri(new URI(decryptedUri));
} catch (URISyntaxException e) {
thrownew RuntimeException(e);
}
}).build();
//TODO 需要前端這里首先按照前后端約定的加密方式進(jìn)行一次加密
//然后得到一個(gè)signature,放在請(qǐng)求的末尾
//然后對(duì)整個(gè)URL進(jìn)行加密請(qǐng)求
// 解析解密后的URL以獲取解密的查詢參數(shù)
UriComponents uriComponents = UriComponentsBuilder.fromUriString(decryptedUri).build();
MultiValueMap<String, String> decryptedQueryParams = uriComponents.getQueryParams();
// 驗(yàn)證請(qǐng)求參數(shù)的簽名
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 公眾號(hào):碼猿技術(shù)專欄
* 密碼學(xué)工具包
*/
@Configuration
publicclass 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));
returnnew String(decryptedBytes, StandardCharsets.UTF_8);
}
//解析路徑參數(shù)并且加密,后判斷是否和signature一樣
public boolean verifySignature(MultiValueMap<String, String> queryParams, String signature, String symmetricKey) throws Exception {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
//將簽名本身從要驗(yàn)證的數(shù)據(jù)中排除
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);
}
}
如果請(qǐng)求的過(guò)程中,請(qǐng)求的數(shù)據(jù)并沒(méi)有被修改,那么可以正確解析,如下:
圖片
如何實(shí)現(xiàn)URL的動(dòng)態(tài)加密?
動(dòng)態(tài)加密其實(shí)在上面就已經(jīng)說(shuō)了。
可以發(fā)現(xiàn)我們發(fā)送的實(shí)際請(qǐng)求是下面這個(gè),/encrypt/后面的就是我們約定好的加密參數(shù)。
http://localhost:8080/v1/product/encrypt/WLB8EDs2LNTsUJpS/aANt0XqZ4MvgpFqN/SwDBVwDbERjBkQw62kfAmfsDW2Bngm
實(shí)際再處理過(guò)程中會(huì)去掉/encrypt,他只是用于標(biāo)識(shí)具體的加密參數(shù)位置而已。