Spring Cloud Gateway 數字簽名、URL動態加密這樣設計真優雅!
在網絡傳遞數據的時候,為了防止數據被篡改,我們會選擇對數據進行加密,數據加密分為對稱加密和非對稱加密。其中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之后的值不同,從而判斷數據被篡改了。關注公眾號:碼猿技術專欄,回復關鍵詞:1111 獲取阿里內部Java性能調優手冊!
HTTPS與CA
https其實不是一個單獨的協議,而是數據傳輸的時候使用TLS/SSL進行了加密而已。而TLS就是一個非常典型的非對稱加密,其兼顧了AES和RSA的安全性和速度。
上面我們已經聊完了一個加密數據的交換過程,那么如果有些人就是偽造了一些域名讓你去訪問怎么辦呢?
HTTPS (HTTP Secure) 是一個安全的 HTTP 通道,它通過 SSL/TLS 協議來保證數據的安全傳輸。在 HTTPS 請求的過程中,證書頒發機構 (CA, Certificate Authority) 扮演了重要的角色。以下是 CA 在保證 HTTPS 請求過程中數據安全交換的方式:
- 證書頒發:CA 為服務器頒發一個數字證書。這個證書包含了服務器的公鑰和一些識別服務器身份的信息。數字證書是由 CA 簽名的,以驗證證書的真實性和完整性。
- 建立安全連接:當客戶端第一次連接到服務器時,服務器會發送其數字證書給客戶端。客戶端會驗證數字證書的合法性,比如檢查證書是否由一個受信任的 CA 簽名,檢查證書是否在有效期內等。一旦證書驗證通過,客戶端就能確認它是與正確的服務器進行通信,而不是被中間人攻擊。
- 密鑰交換:客戶端和服務器會使用 SSL/TLS 協議中的密鑰交換機制來協商一個會話密鑰(通常是一個對稱密鑰)。一種常見的方法是客戶端生成一個隨機的對稱密鑰,然后用服務器的公鑰加密它,再發送給服務器。服務器用自己的私鑰解密得到對稱密鑰。
- 數據加密和傳輸:一旦會話密鑰被協商好,客戶端和服務器就會用這個密鑰來加密和解密傳輸的數據。這樣,即使數據在傳輸過程中被截獲,攻擊者也無法解讀數據的內容,因為他們沒有會話密鑰。
- 完整性校驗:SSL/TLS 協議還提供了數據完整性校驗。它會為傳輸的數據生成一個 MAC (Message Authentication Code),以確保數據在傳輸過程中沒有被篡改。
圖片
其實前面的第一和第二隨機數都是正常傳輸,預主密鑰的得到就是使用RSA了,此時只有客戶端和服務端知道預主密鑰,之后,對第一和第二隨機數使用預主密鑰的加密,就可以得到會話密鑰,此時加密交互完成。
并且會話密鑰只應用在當前會話,每個會話都會新生成一個,所以安全性大大增加。
只有前面的得到預主密鑰的過程用RSA,其他地方都是AES,因為,非對稱實在太慢了。
Gateway網關的過濾器鏈
我們知道,我們可以再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中。
![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)
最后只要進入到ForwardRoutingFilter
圖片
在這里請求完成了最終的處理,然后進行轉發,發送到對應的處理類去處理。
這時候我們看提供遠程服務調用的類的調用棧即可。
圖片
如何對自己的路徑傳輸設定一個數字簽名?
上面我們已經聊到了,先使用RSA的方式傳遞對稱密鑰,然后之后的請求使用AES來進行加密解密。這樣子既保證了安全性也保證了請求的速度。
我就按照上面的說法,簡單的實現了一個數字簽名,大概方式如下:
公鑰獲取:
客戶端首先通過一個特定的接口從服務器獲取RSA公鑰。
對稱密鑰加密:
客戶端生成一個隨機的對稱密鑰,然后使用服務器的RSA公鑰對這個對稱密鑰進行加密。
發送加密的對稱密鑰:
客戶端將加密后的對稱密鑰發送到服務器。
對稱密鑰解密:
服務器使用自己的RSA私鑰解密客戶端發送的加密對稱密鑰,從而得到原始的對稱密鑰。
加密通信:
從現在開始,客戶端和服務器都會使用這個對稱密鑰來加密和解密他們之間的通信。這包括URL的動態加密、請求和響應的加密解密,以及數字簽名的驗證等。
數字簽名:
為了確保數據的完整性和非否認性,客戶端和/或服務器可以使用對稱密鑰來生成和驗證數字簽名。
這樣,雙方都可以確信接收到的數據沒有被篡改,并且確實來自預期的發送方。
URL動態加密:
使用對稱密鑰對URL進行動態加密,以保護URL中的敏感信息,并防止未經授權的訪問。
這個流程確保了客戶端和服務器之間的通信安全,防止數據被截獲或篡改,同時也提供了一個有效的機制來驗證通信雙方的身份。
具體流程如下:
我們首先需要做的第一步是提供一個接口讓前端客戶端去訪問,
并且獲得到我們的公開的RSA公鑰,
然后前端拿到這個RSA公鑰之后加密自己的對稱密鑰,
然后再一次發送一個請求,
這個請求攜帶的是通過RSA公鑰加密過后的對稱密鑰,
然后服務端收到這個對稱密鑰之后,
通過RSA私鑰解密可以得到原本的前端發送的對稱密鑰。
此時,之后的URL動態加密所需要使用到的密鑰,
以及之后請求的數字簽名的加密,
都使用AES的方式,
并且使用這個解密后的對稱密鑰進行加密解密
前端獲取RSA公鑰
我們首先在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
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;
}
}
圖片
發送加密后對稱密鑰
前端使用得到的公鑰對自己的對稱密鑰進行加密,代碼如下:
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
publicclass SymmetricKeyFilter implements GlobalFilter, Ordered {
@Autowired
private SecurityConfig securityConfig;
@Autowired
private StringRedisTemplate stringRedisTemplate;
//TODO 3:這里會把加密好的對稱密鑰 解密 然后放入到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));
//得到對稱密鑰
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;
}
}
前端發送AES加密請求
比如這里的請求參數為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類
*/
publicclass 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
publicclass CryptoFilter implements GlobalFilter, Ordered {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private CryptoHelper cryptoHelper;
//TODO 4:在這里對加密的URL進行解密
//并且會得到路徑的參數
//然后對參數進行加密之后和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動態加密 數字簽名 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) {
thrownew RuntimeException(e);
}
}).build();
//TODO 需要前端這里首先按照前后端約定的加密方式進行一次加密
//然后得到一個signature,放在請求的末尾
//然后對整個URL進行加密請求
// 解析解密后的URL以獲取解密的查詢參數
UriComponents uriComponents = UriComponentsBuilder.fromUriString(decryptedUri).build();
MultiValueMap<String, String> 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
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);
}
//解析路徑參數并且加密,后判斷是否和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()) {
//將簽名本身從要驗證的數據中排除
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);
}
}
如果請求的過程中,請求的數據并沒有被修改,那么可以正確解析,如下:
圖片
如何實現URL的動態加密?
動態加密其實在上面就已經說了。
可以發現我們發送的實際請求是下面這個,/encrypt/后面的就是我們約定好的加密參數。
http://localhost:8080/v1/product/encrypt/WLB8EDs2LNTsUJpS/aANt0XqZ4MvgpFqN/SwDBVwDbERjBkQw62kfAmfsDW2Bngm
實際再處理過程中會去掉/encrypt,他只是用于標識具體的加密參數位置而已。