RSA非對稱加密
RSA是一種常用的非對稱加密算法,加密和加密使用不同的密鑰,常用于要求安全性較高的加密場景,比如接口的驗簽和接口數據的加密與解密。與非對稱加密算法對比,其安全性較高,但是加密性能卻比較低,不適合高并發場景,一般只加密少量的數據。
AES對稱加密
AES是一種最常見的對稱加密算法(微信小程序加密傳輸就是用這個加密算法的),加密和解密使用的是相同的密鑰。其加密性能好,加密解密速度非常快,內存需求低,適用于經常發送數據的場合。
RSA+AES實現接口驗簽和請求參數的加密與解密
背景:作為程序猿,我們經常需要在我們自己開發的系統上,開發一些接口供第三方調用,那么這個時候,對我們接口的安全性要求就比較高了,尤其是那種需要傳輸比較私密的信息的時候,其對安全性的要求就更高了。
實現思路
調用方:
- 使用AES對稱加密算法對業務請求參數進行加密后傳輸
- 使用RSA非對稱加密算法對AES的密鑰進行公鑰加密后傳輸
- 使用RSA的私鑰對請求參數進行簽名
接收方:
- 獲取到請求參數后,對參數進行驗簽和業務參數的解密
問題:為什么要對AES的密鑰進行RSA公鑰加密后傳輸?
AES是對稱加密算法,加密和解密的密鑰都是同一個,為了防止被別人惡意獲取到該密鑰,然后對我們的業務請求參數進行解密,我們需要將AES密鑰進行非對稱加密后再進行傳輸。
代碼實現
<!--RSA依賴-->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.56</version>
</dependency>
<!--Jackson依賴-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
<!--Jackson依賴-->
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId>
<version>1.8.3</version>
</dependency>
請求和接收的實體對象
@Data
public class JsonRequest {
//接口id 可空
private String serviceId;
//請求唯一id 非空
private String requestId;
//商戶id 非空
private String AppId;
//參數簽名 非空
private String sign;
//對稱加密key 非空
private String aseKey;
//時間戳,精確到毫秒 非空
private long timestamp;
//請求的業務參數(AES加密后傳入) 可空
private String body;
}
- serviceId:服務id(接口id)。接口設計分為兩種,一種是所有的調用方針對類似的業務,都調用的是同一接口地址,然后內部系統根據serviceId去判斷具體是要調用哪個業務方法;另一種是針對不同的調用方,開發不同的接口,接口地址也是不一樣,那么這個時候就可以不要serviceId這個字段。本章是使用第二種方式,所以serviceId可以不要(可空)。
- requestId:請求唯一id。方便查詢定位某個請求和防止同個請求多次調用。
- appId:商戶id,即我們會給調用方分配一個這樣的id,并且將這個id與調用方的信息進行關聯,比如“通過appId查詢出調用方的加密密鑰等”
- aseKey:是AES對稱加密的密鑰。用于解密業務請求參數。這里要先用RSA公鑰對aseKey進行加密后傳輸。
- timestamp:請求時間戳。可以用該字段,對請求的時間合法性進行校驗。(如:過濾掉請求時間不在當前時間的正負10分鐘范圍內的請求)
- body:請求的業務參數。對請求的業務參數AES加密后再賦值。
AES工具類
/**
* @author: Longer
* @date: 2020/8/23
* @description: AES工具類
*/
public class AESUtil {
/**
* 加密
* @param content 加密文本
* @param key 加密密鑰,appSecret的前16位
* @param iv 初始化向量,appSecret的后16位
* @return
* @throws Exception
*/
public static String encrypt(String content, String key, String iv) throws Exception {
byte[] raw = key.getBytes();
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); //"算法/模式/補碼方式"
IvParameterSpec ivParam = new IvParameterSpec(iv.getBytes()); //使用CBC模式,需要一個向量iv,可增加加密算法的強度
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, ivParam);
byte[] encrypted = cipher.doFinal(content.getBytes());
return new BASE64Encoder().encode(encrypted);
}
public static String decrypt(String content, String key, String iv) throws Exception {
byte[] raw = key.getBytes();
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding "); //"算法/模式/補碼方式"
IvParameterSpec ivParam = new IvParameterSpec(iv.getBytes()); //使用CBC模式,需要一個向量iv,可增加加密算法的強度
cipher.init(Cipher.DECRYPT_MODE, skeySpec, ivParam);
byte[] encrypted = new BASE64Decoder().decodeBuffer(content); //先用base64解密
byte[] original = cipher.doFinal(encrypted);
return new String(original);
}
public static void main(String[] args) throws Exception {
String encrypt = AESUtil.encrypt("你好啊!!!", "1234567890123456", "1234567890123456");
String decrypt = AESUtil.decrypt(encrypt, "1234567890123456", "1234567890123456");
System.out.println(decrypt);
}
RSA工具類
/**
* @author: Longer
* @date: 2020/8/23
* @description: RSA工具類
*/
public class RSAUtil {
/**
* 定義加密方式
*/
private final static String KEY_RSA = "RSA";
/**
* 定義簽名算法
*/
private final static String KEY_RSA_SIGNATURE = "MD5withRSA";
/**
* 定義公鑰算法
*/
private final static String KEY_RSA_PUBLICKEY = "RSAPublicKey";
/**
* 定義私鑰算法
*/
private final static String KEY_RSA_PRIVATEKEY = "RSAPrivateKey";
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* 初始化密鑰
*/
public static Map<String, Object> init() {
Map<String, Object> map = null;
try {
KeyPairGenerator generator = KeyPairGenerator.getInstance(KEY_RSA);
generator.initialize(2048);
KeyPair keyPair = generator.generateKeyPair();
// 公鑰
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
// 私鑰
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// 將密鑰封裝為map
map = new HashMap<>();
map.put(KEY_RSA_PUBLICKEY, publicKey);
map.put(KEY_RSA_PRIVATEKEY, privateKey);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return map;
}
/**
* 公鑰加密
*
* @param data 待加密數據
* @param key 公鑰
*/
public static byte[] encryptByPublicKey(String data, String key) {
byte[] result = null;
try {
byte[] bytes = decryptBase64(key);
// 取得公鑰
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance(KEY_RSA);
PublicKey publicKey = factory.generatePublic(keySpec);
// 對數據加密
Cipher cipher = Cipher.getInstance("RSA/None/PKCS1Padding", "BC");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encode = cipher.doFinal(data.getBytes());
// 再進行Base64加密
result = Base64.encode(encode);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 私鑰解密
*
* @param data 加密數據
* @param key 私鑰
*/
public static String decryptByPrivateKey(byte[] data, String key) {
String result = null;
try {
// 對私鑰解密
byte[] bytes = decryptBase64(key);
// 取得私鑰
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance(KEY_RSA);
PrivateKey privateKey = factory.generatePrivate(keySpec);
// 對數據解密
Cipher cipher = Cipher.getInstance("RSA/None/PKCS1Padding", "BC");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
// 先Base64解密
byte[] decoded = Base64.decode(data);
result = new String(cipher.doFinal(decoded));
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 獲取公鑰
*/
public static String getPublicKey(Map<String, Object> map) {
String str = "";
try {
Key key = (Key) map.get(KEY_RSA_PUBLICKEY);
str = encryptBase64(key.getEncoded());
} catch (Exception e) {
e.printStackTrace();
}
return str;
}
/**
* 獲取私鑰
*/
public static String getPrivateKey(Map<String, Object> map) {
String str = "";
try {
Key key = (Key) map.get(KEY_RSA_PRIVATEKEY);
str = encryptBase64(key.getEncoded());
} catch (Exception e) {
e.printStackTrace();
}
return str;
}
/**
* 用私鑰對信息生成數字簽名
*
* @param data 加密數據
* @param privateKey 私鑰
*/
public static String sign(byte[] data, String privateKey) {
String str = "";
try {
// 解密由base64編碼的私鑰
byte[] bytes = decryptBase64(privateKey);
// 構造PKCS8EncodedKeySpec對象
PKCS8EncodedKeySpec pkcs = new PKCS8EncodedKeySpec(bytes);
// 指定的加密算法
KeyFactory factory = KeyFactory.getInstance(KEY_RSA);
// 取私鑰對象
PrivateKey key = factory.generatePrivate(pkcs);
// 用私鑰對信息生成數字簽名
Signature signature = Signature.getInstance(KEY_RSA_SIGNATURE);
signature.initSign(key);
signature.update(data);
str = encryptBase64(signature.sign());
} catch (Exception e) {
e.printStackTrace();
}
return str;
}
/**
* 校驗數字簽名
*
* @param data 加密數據
* @param publicKey 公鑰
* @param sign 數字簽名
* @return 校驗成功返回true,失敗返回false
*/
public static boolean verify(byte[] data, String publicKey, String sign) {
boolean flag = false;
try {
// 解密由base64編碼的公鑰
byte[] bytes = decryptBase64(publicKey);
// 構造X509EncodedKeySpec對象
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes);
// 指定的加密算法
KeyFactory factory = KeyFactory.getInstance(KEY_RSA);
// 取公鑰對象
PublicKey key = factory.generatePublic(keySpec);
// 用公鑰驗證數字簽名
Signature signature = Signature.getInstance(KEY_RSA_SIGNATURE);
signature.initVerify(key);
signature.update(data);
flag = signature.verify(decryptBase64(sign));
} catch (Exception e) {
e.printStackTrace();
}
return flag;
}
/**
* BASE64 解密
*
* @param key 需要解密的字符串
* @return 字節數組
*/
public static byte[] decryptBase64(String key) throws Exception {
return Base64.decode(key);
}
/**
* BASE64 加密
*
* @param key 需要加密的字節數組
* @return 字符串
*/
public static String encryptBase64(byte[] key) throws Exception {
return new String(Base64.encode(key));
}
/**
* 按照紅黑樹(Red-Black tree)的 NavigableMap 實現
* 按照字母大小排序
*/
public static Map<String, Object> sort(Map<String, Object> map) {
if (map == null) {
return null;
}
Map<String, Object> result = new TreeMap<>((Comparator<String>) (o1, o2) -> {
return o1.compareTo(o2);
});
result.putAll(map);
return result;
}
/**
* 組合參數
*
* @param map
* @return 如:key1Value1Key2Value2....
*/
public static String groupStringParam(Map<String, Object> map) {
if (map == null) {
return null;
}
StringBuffer sb = new StringBuffer();
for (Map.Entry<String, Object> item : map.entrySet()) {
if (item.getValue() != null) {
sb.append(item.getKey());
if (item.getValue() instanceof List) {
sb.append(JSON.toJSONString(item.getValue()));
} else {
sb.append(item.getValue());
}
}
}
return sb.toString();
}
/**
* bean轉map
* @param obj
* @return
*/
public static Map<String, Object> bean2Map(Object obj) {
if (obj == null) {
return null;
}
Map<String, Object> map = new HashMap<>();
try {
BeanInfo beanInfo = Introspector.getBeanInfo(obj.getClass());
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor property : propertyDescriptors) {
String key = property.getName();
// 過濾class屬性
if (!key.equals("class")) {
// 得到property對應的getter方法
Method getter = property.getReadMethod();
Object value = getter.invoke(obj);
if (StringUtils.isEmpty(value)) {
continue;
}
map.put(key, value);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
/**
* 按照紅黑樹(Red-Black tree)的 NavigableMap 實現
* 按照字母大小排序
*/
public static Map<String, Object> sort(Map<String, Object> map) {
if (map == null) {
return null;
}
Map<String, Object> result = new TreeMap<>((Comparator<String>) (o1, o2) -> {
return o1.compareTo(o2);
});
result.putAll(map);
return result;
}
/**
* 組合參數
*
* @param map
* @return 如:key1Value1Key2Value2....
*/
public static String groupStringParam(Map<String, Object> map) {
if (map == null) {
return null;
}
StringBuffer sb = new StringBuffer();
for (Map.Entry<String, Object> item : map.entrySet()) {
if (item.getValue() != null) {
sb.append(item.getKey());
if (item.getValue() instanceof List) {
sb.append(JSON.toJSONString(item.getValue()));
} else {
sb.append(item.getValue());
}
}
}
return sb.toString();
}
/**
* bean轉map
* @param obj
* @return
*/
public static Map<String, Object> bean2Map(Object obj) {
if (obj == null) {
return null;
}
Map<String, Object> map = new HashMap<>();
try {
BeanInfo beanInfo = Introspector.getBeanInfo(obj.getClass());
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor property : propertyDescriptors) {
String key = property.getName();
// 過濾class屬性
if (!key.equals("class")) {
// 得到property對應的getter方法
Method getter = property.getReadMethod();
Object value = getter.invoke(obj);
if (StringUtils.isEmpty(value)) {
continue;
}
map.put(key, value);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
public static void main(String[] args) throws Exception {
System.out.println("公鑰加密======私鑰解密");
String str = "Longer程序員";
byte[] enStr = RSAUtil.encryptByPublicKey(str, publicKey);
String miyaoStr = HexUtils.bytesToHexString(enStr);
byte[] bytes = HexUtils.hexStringToBytes(miyaoStr);
String decStr = RSAUtil.decryptByPrivateKey(bytes, privateKey);
System.out.println("加密前:" + str + "nr解密后:" + decStr);
System.out.println("nr");
System.out.println("私鑰簽名======公鑰驗證");
String sign = RSAUtil.sign(str.getBytes(), privateKey);
System.out.println("簽名:nr" + sign);
boolean flag = RSAUtil.verify(str.getBytes(), publicKey, sign);
System.out.println("驗簽結果:nr" + flag);
}
jackson工具類
import com.fasterxml.jackson.databind.JAVAType;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.codehaus.jackson.type.TypeReference;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* @author: Longer
* @date: 2020/8/23
* @description: Jackson轉換工具類
*/
@Slf4j
public class JacksonUtil {
private static ObjectMapper objectMapper = new ObjectMapper();
/**
* 對象轉換成json
*
* @param obj
* @param <T>
* @return
*/
public static <T> String beanToJson(T obj) {
if (obj == null) {
return null;
}
try {
return obj instanceof String ? (String) obj : objectMapper.writeValueAsString(obj);
} catch (Exception e) {
log.error("beanToJson error", e);
e.printStackTrace();
return null;
}
}
/**
* 將JSON字符串根據指定的Class反序列化成Java對象。
*
* @param json JSON字符串
* @param pojoClass Java對象Class
* @return 反序列化生成的Java對象
* @throws Exception 如果反序列化過程中發生錯誤,將拋出異常
*/
public static Object decode(String json, Class<?> pojoClass)
throws Exception {
try {
return objectMapper.readValue(json, pojoClass);
} catch (Exception e) {
throw e;
}
}
/**
* 將JSON字符串根據指定的Class反序列化成Java對象。
*
* @param json JSON字符串
* @param reference 類型引用
* @return 反序列化生成的Java對象
* @throws Exception 如果反序列化過程中發生錯誤,將拋出異常
*/
public static Object decode(String json, TypeReference<?> reference) throws Exception {
try {
return objectMapper.readValue(json, reference.getClass());
} catch (Exception e) {
throw e;
}
}
/**
* 將Java對象序列化成JSON字符串。
*
* @param obj 待序列化生成JSON字符串的Java對象
* @return JSON字符串
* @throws Exception 如果序列化過程中發生錯誤,將拋出異常
*/
public static String encode(Object obj) throws Exception {
try {
return objectMapper.writeValueAsString(obj);
} catch (Exception e) {
throw e;
}
}
/**
* 對象轉換成格式化的json
*
* @param obj
* @param <T>
* @return
*/
public static <T> String beanToJsonPretty(T obj) {
if (obj == null) {
return null;
}
try {
return obj instanceof String ? (String) obj : objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
} catch (Exception e) {
log.error("beanToJsonPretty error", e);
e.printStackTrace();
return null;
}
}
/**
* 將json轉換成對象Class
*
* @param str
* @param clazz
* @param <T>
* @return
*/
public static <T> T jsonToBean(String str, Class<T> clazz) {
if (StringUtils.isEmpty(str) || clazz == null) {
return null;
}
try {
return clazz.equals(String.class) ? (T) str : objectMapper.readValue(str, clazz);
} catch (Exception e) {
log.error("jsonToBean error", e);
e.printStackTrace();
return null;
}
}
/**
* 將json轉換為對象集合
*
* @param str
* @param clazz
* @param <T>
* @return
*/
public static <T> List<T> jsonToBeanList(String str, Class<T> clazz) {
if (StringUtils.isEmpty(str) || clazz == null) {
return null;
}
JavaType javaType = getCollectionType(ArrayList.class, clazz);
try {
return objectMapper.readValue(str, javaType);
} catch (IOException e) {
log.error("jsonToBeanList error", e);
e.printStackTrace();
return null;
}
}
public static JavaType getCollectionType(Class<?> collectionClass, Class<?>... elementClasses) {
return objectMapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);
}
}
加密解密驗簽名流程演示
/**
* @author: Longer
* @date: 2020/8/23
* @description: 測試
*/
public class RequestTest {
public static void main(String[] args) {
/****先給調用方分配一組RSA密鑰和一個appId****/
//初始化RSA密鑰
Map<String, Object> init = RSAUtil.init();
//私鑰
String privateKey = RSAUtil.getPrivateKey(init);
//公鑰
String publicKey = RSAUtil.getPublicKey(init);
//appId,32位的uuid
String appId = getUUID32();
/****先給調用方分配一組RSA密鑰和一個appId****/
/*****調用方(請求方)*****/
//業務參數
Map<String,Object> businessParams = new HashMap<>();
businessParams.put("name","Longer");
businessParams.put("job","程序猿");
businessParams.put("hobby","打籃球");
JsonRequest jsonRequest = new JsonRequest();
jsonRequest.setRequestId(getUUID32());
jsonRequest.setAppId(appId);
jsonRequest.setTimestamp(System.currentTimeMillis());
//使用appId的前16位作為AES密鑰,并對密鑰進行rsa公鑰加密
String aseKey = appId.substring(0, 16);
byte[] enStr = RSAUtil.encryptByPublicKey(aseKey, publicKey);
String aseKeyStr = HexUtils.bytesToHexString(enStr);
jsonRequest.setAseKey(aseKeyStr);
//請求的業務參數進行加密
String body = "";
try {
body = AESUtil.encrypt(JacksonUtil.beanToJson(businessParams), aseKey, appId.substring(16));
} catch (Exception e) {
throw new RuntimeException("報文加密異常", e);
}
jsonRequest.setBody(body);
//簽名
Map<String, Object> paramMap = RSAUtil.bean2Map(jsonRequest);
paramMap.remove("sign");
// 參數排序
Map<String, Object> sortedMap = RSAUtil.sort(paramMap);
// 拼接參數:key1Value1key2Value2
String urlParams = RSAUtil.groupStringParam(sortedMap);
//私鑰簽名
String sign = RSAUtil.sign(HexUtils.hexStringToBytes(urlParams), privateKey);
jsonRequest.setSign(sign);
/*****調用方(請求方)*****/
/*****接收方(自己的系統)*****/
//參數判空(略)
//appId校驗(略)
//本條請求的合法性校驗《唯一不重復請求;時間合理》(略)
//驗簽
Map<String, Object> paramMap2 = RSAUtil.bean2Map(jsonRequest);
paramMap2.remove("sign");
//參數排序
Map<String, Object> sortedMap2 = RSAUtil.sort(paramMap2);
//拼接參數:key1Value1key2Value2
String urlParams2 = RSAUtil.groupStringParam(sortedMap2);
//簽名驗證
boolean verify = RSAUtil.verify(HexUtils.hexStringToBytes(urlParams2), publicKey, jsonRequest.getSign());
if (!verify) {
throw new RuntimeException("簽名驗證失敗");
}
//私鑰解密,獲取aseKey
String aseKey2 = RSAUtil.decryptByPrivateKey(HexUtils.hexStringToBytes(jsonRequest.getAseKey()), privateKey);
if (!StringUtils.isEmpty(jsonRequest.getBody())) {
// 解密請求報文
String requestBody = "";
try {
requestBody = AESUtil.decrypt(jsonRequest.getBody(), aseKey, jsonRequest.getAppId().substring(16));
} catch (Exception e) {
throw new RuntimeException("請求參數解密異常");
}
System.out.println("業務參數解密結果:"+requestBody);
}
/*****接收方(自己的系統)*****/
}
public static String getUUID32() {
String uuid = UUID.randomUUID().toString();
uuid = uuid.replace("-", "");
return uuid;
}
}
執行結果:
業務參數解密結果:{"name":"Longer","job":"程序猿","hobby":"打籃球"}
到此,調用方要做的和接收方做的其實都已經清楚了。
調用方:
- 1.業務參數進行AES對稱加密
- 2.AES密鑰進行RSA非對稱加密
- 3.使用RSA生成簽名
接收方:
- 驗證簽名
- AES密鑰解密
- 業務參數解密
請求參數的統一處理
上面講到,我們接受的請求對象是JsonRequst對象,里面除了body成員變量是跟業務相關,其他成員變量(sericeId,appId等)都是與業務不相關的。那么,如果我們在controller層用JsonRequest對象去接收請求參數的話,其實是不那么規范的。
那么我們能不能對請求參數進行統一處理,使得傳到controller層的參數只是跟業務相關的參數,并且在controller層也無需關注加密解密和驗簽的東西。
實現方法:
- 使用過濾器,攔截請求,并對請求參數進行統一處理(加密解密,驗簽等)
- 自定義request對象(新增類繼承HttpServletRequestWrapper類),對請求參數進行過濾處理,使得controller只接受業務參數。
問題:為什么需要自定義request對象?
因為獲取post請求傳遞的json對象,需要用request對象流取獲取,而一旦我們調用了request.getInputStream()方法后,流將會自動關閉,那么到了我們的controller層就不能再獲取到請求參數了。
自定義request對象
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;
/**
* @author Longer
* @description 獲取請求參數并處理
* @date 2020/8/23
*/
public class RequestWrapper extends HttpServletRequestWrapper {
private byte[] body;
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
String sessionStream = getBodyString(request);
body = sessionStream.getBytes(Charset.forName("UTF-8"));
}
public String getBodyString() {
return new String(body, Charset.forName("UTF-8"));
}
public void setBodyString(byte[] bodyByte){
body = bodyByte;
}
/**
* 獲取請求Body
*
* @param request
* @return
*/
public String getBodyString(final ServletRequest request) {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = cloneInputStream(request.getInputStream());
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString();
}
/**
* Description: 復制輸入流</br>
*
* @param inputStream
* @return</br>
*/
public InputStream cloneInputStream(ServletInputStream inputStream) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
try {
while ((len = inputStream.read(buffer)) > -1) {
byteArrayOutputStream.write(buffer, 0, len);
}
byteArrayOutputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
InputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
return byteArrayInputStream;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}
自定義過濾器
/**
* @author Longer
* @description 獲取請求參數并處理。簽名校驗,報文解密,參數過濾。
* @date 2020/8/23
*/
@Slf4j
public class OutRequestFilter extends OncePerRequestFilter {
@SneakyThrows
@Override
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException {
redisUtil redisUtil = SpringUtils.getBean(RedisUtil.class);
HttpServletRequest request = (HttpServletRequest) servletRequest;
String requestURL = request.getRequestURI();
log.info("請求路徑:" + requestURL);
String method = request.getMethod();
if (!"POST".equals(method)) {
throw new RuntimeException("暫不支持" + method + "請求方式");
}
//獲取請求參數
RequestWrapper requestWrapper = new RequestWrapper(request);
String bodyString = requestWrapper.getBodyString();
if (StringUtils.isEmpty(bodyString)) {
throw new RuntimeException("請求體不能為空");
}
log.info("請求參數:" + bodyString);
JsonRequest jsonRequest = JacksonUtil.jsonToBean(bodyString, JsonRequest.class);
//step0 參數合法性校驗(非空判斷等)
parameterValidate(jsonRequest);
//step1 判斷請求合法性。1.不允許重復請求(通過請求唯一id判斷)2.不允許請求時間與當前時間差距過大(正負10分鐘)
long currentTime = System.currentTimeMillis();
long subTime = currentTime - jsonRequest.getTimestamp();
long tenMinuteMs = 10 * 60 * 1000;
if (subTime < -tenMinuteMs || subTime > tenMinuteMs) {
throw new RuntimeException("請求異常,請求時間異常");
}
String requestUUIDKey = MessageFormat.format(RedisConstant.REQUEST_UUID, jsonRequest.getRequestId());
Object requestFlag = redisUtil.get(requestUUIDKey);
if (!StringUtils.isEmpty(requestFlag)) {
throw new RuntimeException("請求異常,重復的請求");
}
redisUtil.set(requestUUIDKey, JacksonUtil.beanToJson(jsonRequest), 15 * 60);
//step2 參數解密,簽名校驗,參數過濾和傳遞
Map<String, Object> paramMap = RSAUtil.bean2Map(jsonRequest);
paramMap.remove("sign");
//根據appkey獲取rsa密鑰
String appIdKey = MessageFormat.format(RedisConstant.REQUEST_APPID, jsonRequest.getAppId());
Object ob = redisUtil.get(appIdKey);
if (StringUtils.isEmpty(ob)) {
throw new RuntimeException("找不到指定的appid");
}
String jsonString = (String) ob;
JSONObject jsonObject = JSONObject.parseobject(jsonString);
//rsa公鑰
String publicKey = jsonObject.getString("publicKey");
//rsa私鑰
String privateKey = jsonObject.getString("privateKey");
//參數排序
Map<String, Object> sortedMap = RSAUtil.sort(paramMap);
//拼接參數:key1Value1key2Value2
String urlParams = RSAUtil.groupStringParam(sortedMap);
//簽名驗證
boolean verify = RSAUtil.verify(HexUtils.hexStringToBytes(urlParams), publicKey, jsonRequest.getSign());
if (!verify) {
throw new RuntimeException("簽名驗證失敗");
}
//私鑰解密,獲取aseKey
String aseKey = RSAUtil.decryptByPrivateKey(HexUtils.hexStringToBytes(jsonRequest.getAseKey()), privateKey);
if (!StringUtils.isEmpty(jsonRequest.getBody())) {
// 解密請求報文
String body = "";
try {
body = AESUtil.decrypt(jsonRequest.getBody(), aseKey, jsonRequest.getAppId().substring(16));
} catch (Exception e) {
log.error("請求參數解密異常:",e);
throw new RuntimeException("請求參數解密異常");
}
//報文傳遞至controller層
requestWrapper.setBodyString(body.getBytes(Charset.forName("UTF-8")));
}
//將request傳遞下去
filterChain.doFilter(requestWrapper, servletResponse);
}
private void parameterValidate(JsonRequest jsonRequest) {
if (StringUtils.isEmpty(jsonRequest.getAppId())) {
throw new RuntimeException("參數異常,appId不能為空");
}
if (StringUtils.isEmpty(jsonRequest.getAseKey())) {
throw new RuntimeException("參數異常,aseKey不能為空");
}
if (StringUtils.isEmpty(jsonRequest.getRequestId())) {
throw new RuntimeException("參數異常,requestId不能為空");
}
if (StringUtils.isEmpty(jsonRequest.getSign())) {
throw new RuntimeException("參數異常,sign不能為空");
}
if (jsonRequest.getTimestamp() == 0l) {
throw new RuntimeException("參數異常,timestamp不能為空");
}
}
}
完整流程演示
調用方
HttpClientUtils類
/**
* @author: Longer
* @description:
*/
public class HttpClientUtils {
private static Logger logger = Logger.getLogger(HttpClientUtils.class.getName());
public static String doPostJson(String url, String json) {
// 創建Httpclient對象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 創建Http Post請求
HttpPost httpPost = new HttpPost(url);
// 創建請求內容
StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
httpPost.setEntity(entity);
// 執行http請求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
resultString = e.getMessage();
logger.info("http訪問失敗:" + e);
} finally {
try {
response.close();
} catch (IOException e) {
logger.info("response關閉失敗:" + e);
}
}
return resultString;
}
/**
* post請求,簽名和報文加密
*
* @param url 請求地址
* @param json 請求json參數
* @param appId 商戶id
* @param publicKey rsa公鑰
* @param privateKey rsa私鑰
* @return
*/
public static String doPostJsonForSign(String url, String json, String appId, String publicKey, String privateKey) {
String aseKey = appId.substring(0, 16);
JsonRequest jsonRequest = new JsonRequest();
jsonRequest.setRequestId(getUUID32());
jsonRequest.setAppId(appId);
jsonRequest.setTimestamp(System.currentTimeMillis());
//aseKey 加密
logger.info("開始aseKey加密....");
byte[] enStr = RSAUtil.encryptByPublicKey(aseKey, publicKey);
String aseKeyStr = HexUtils.bytesToHexString(enStr);
jsonRequest.setAseKey(aseKeyStr);
//請求參數進行加密
String body = "";
try {
logger.info("開始請求參數加密....");
body = AESUtil.encrypt(json, aseKey, appId.substring(16));
} catch (Exception e) {
logger.info("報文加密異常:" + e);
throw new UncheckedException("報文加密異常", e);
}
jsonRequest.setBody(body);
Map<String, Object> paramMap = RSAUtil.bean2Map(jsonRequest);
paramMap.remove("sign");
// 參數排序
Map<String, Object> sortedMap = RSAUtil.sort(paramMap);
// 拼接參數:key1Value1key2Value2
String urlParams = RSAUtil.groupStringParam(sortedMap);
//私鑰簽名
logger.info("開始參數簽名....");
String sign = RSAUtil.sign(HexUtils.hexStringToBytes(urlParams), privateKey);
jsonRequest.setSign(sign);
String requestParams = JacksonUtil.beanToJson(jsonRequest);
logger.info("發起請求....");
String result = doPostJson(url, requestParams);
return result;
}
public static String getUUID32() {
String uuid = UUID.randomUUID().toString();
uuid = uuid.replace("-", "");
return uuid;
}
}
需要傳遞的業務參數對象
@Data
public class RequestDto {
private String name;
private int age;
private String hobby;
}
發送請求
public static void main(String[] args) {
//請求地址
String url = "http://127.0.0.1:8888/test";
RequestDto requestDto = new RequestDto();
requestDto.setAge(100);
requestDto.setName("Longer");
requestDto.setHobby("ball");
String json = JacksonUtil.beanToJson(requestDto);
//appId
String appId = "";
//rsa公鑰
String publicKey = "";
//rsa私鑰
String privateKey = "";
HttpClientUtils.doPostJsonForSign(url, json, appId, publicKey, privateKey)
}
接收方
controller
@Slf4j
@RestController
public class TestController {
@RequestMapping("test")
public String test(RequestDto requestDto){
log.info("接收到的請求參數為:"+ JacksonUtil.beanToJson(requestDto));
return "a";
}
}
因為我們對參數進行了統一處理,所以我們的controller接收參數的對象是RequestDto對象,而不是JsonRequest對象
原文鏈接:
https://www.jianshu.com/p/9061da5e25d1