在上篇文章中,博主介紹了借助Spring攔截器進行token校驗妥凳。在本文中贝攒,將介紹如何通過AOP來進行加密解密,簽名驗證等操作圣猎,來保證接口的數(shù)據(jù)傳輸?shù)陌踩浴?/p>
加密算法
為什么需要加密呢士葫?就好比戰(zhàn)爭時期特工在進行傳輸情報的時候,如果將情報明文直接通過某種媒介傳輸給同盟人員送悔,那么一旦情報被地方截取慢显,就會釀成大禍。如果將明文通過某種加密算法加密成雜亂無章的密文欠啤,即使被敵方截獲荚藻,沒有對應的解密算法,也很難識別出其中的明文跪妥。安全傳輸領域鞋喇,加密算法是一種很常用的手段,它可以保證數(shù)據(jù)不被竊取和泄漏眉撵,還可以保證數(shù)據(jù)的完整性侦香,不被篡改。
常見的加密算法有對稱加密纽疟,非對稱加密罐韩,單向加密(簽名)等分類。其中對稱加密算法污朽,加密密鑰和解密密鑰是同一個散吵,因此發(fā)送發(fā)和接收方都需要維護一個相同的密鑰,如果密鑰要修改蟆肆,雙方都需要同時修改矾睦。非對稱加密算法中,發(fā)送發(fā)用公鑰進行加密炎功,接收方用私鑰進行解密枚冗。單向加密算法是對傳輸?shù)臄?shù)據(jù)生成一個簽名,通過這個簽名來驗證數(shù)據(jù)在傳輸過程中是否被篡改過蛇损,一般是不可逆的赁温。
常用的對稱加密算法有DES, AES, 3DES等坛怪, 非對稱加密算法有RSA, DSA, ECB等,簽名算法有SHA1, MD5, HMAC等股囊。在本文中將使用AES和HMAC-MD5來進行數(shù)據(jù)加密解密袜匿,以及簽名驗證。
AES
AES 加密算法是一種對稱加密算,加密密鑰和解密密鑰是同一個。它采用對稱分組密碼體制濒翻,最少支持長度為128位的加密。涉及到分組加密穆壕,padding填充,初始向量IV其屏,密鑰,四種加密模式缨该。
分組加密就是將原文分割成一段段的分別進行加密偎行,每段分組長度為128位16個字節(jié),如果最后一組長度不足128位贰拿,則采用padding填充模式將其補齊到128位蛤袒。然后對每組進行加密,最后組成最終密文膨更。
padding填充是為了解決分組后的長度不足128位的場景妙真。填充模式也有多種不同模式,比如PKCS5, PKCS7和NOPADDING荚守。其中PKSC5是指分組后缺少幾個字節(jié)珍德,就在后面填充幾個字節(jié)的幾,比如缺少2個字節(jié)矗漾,就在后面填充2個字節(jié)的2锈候。PKCS7是指缺少幾個字節(jié),就在后面填充幾個字節(jié)的0敞贡,比如缺少5個字節(jié)泵琳,就填充5個字節(jié)的0。NOPADDING模式就是不需要填充誊役。如果最后面剛好是16個字節(jié)的16获列,那么解密方不知道是填充數(shù)據(jù)還是真實數(shù)據(jù),因此會在后面再補16個字節(jié)的16來區(qū)分蛔垢。
初始向量IV是為了保證數(shù)據(jù)的安全性击孩,如果我們對同一段內容進行加密后,所生成的密文應該是相同的啦桌,那么這樣就很容易通過密文分析出哪些段是相同的溯壶。比如原文分組后成為ABCADE,加密后的密文是GHIGJK,那么很容易看出那兩段內容是相同的及皂。第一個分組在初始加密向量的基礎上進行加密,以后的每一個分組都在前一個分組加密的結果為基礎進行加密且改,從而保證了即使相同的原文段验烧,也不會生成相同的密文段。
密鑰是加密和解密公用的一個又跛,它一般是128位16個字節(jié)長度的隨機字符串碍拆,分組后的原文都用同一個密鑰進行加密。
加密模式包含ECB慨蓝,CBC, CFB, OFB等四種模式感混。ECB分別對每個分組進行加密,相同的明文會被加密成相同的密文礼烈。CBC模式會使用上一段的加密結果作為加密向量弧满,相同的原文不會被加密成相同的密文。
MD5
MD5算法是一種不可逆的簽名算法此熬,對相同的輸入通過MD5散列函數(shù)處理后庭呜,會輸出相同的信息。因此MD5可以驗證傳輸?shù)臄?shù)據(jù)是否有被篡改犀忱,但是如果竊密者對明文進行了修改后募谎,再使用MD5算法進行散列,接收方將無法判斷明文已經(jīng)被修改了阴汇。一般數(shù)據(jù)庫存儲用戶密碼會將密碼使用MD5進行處理数冬。
HMAC-MD5
HMAC-MD5由一個H函數(shù)和一個密鑰組成,一般我們采用的散列函數(shù)為Md5或者SHA-1搀庶。HMAC-MD5算法就是采用密鑰加密+Md5信息摘要的方式形成新的密文拐纱。
AOP
眾所周知,AOP(面向切面編程)是Spring一個重要特性地来,它將核心關注點和業(yè)務邏輯進行解耦戳玫,將業(yè)務無關的邏輯提取出來作為公共模塊進行處理。它有切點未斑,切面咕宿,連接點,通知的概念蜡秽。切點就是我們可以織入切面的點府阀,切面就是我們要織入的橫切邏輯,通知包含前置通知芽突,后置通知试浙,返回通知,異常通知寞蚌,環(huán)繞通知等田巴。這些aop的概念钠糊,可在其它文章中了解。
加密解密接口
定一個加密解密接口壹哺,并定義一些操作方法抄伍,這樣如果要更改加密或者解密算法的話就可有不同實現(xiàn)。
public interface CryptSignHandler<T, R> {
/**
* 結果加密
* @param data
* @return
*/
String encrypt(Object data);
/**
* 請求解密
* @param data
* @return
*/
String decrypt(String data);
/**
* 校驗請求簽名
* @return
*/
void checkSign(T req);
/**
* 結果生成簽名
* @param res
* @return
*/
String sign(R res);
}
加密解密實現(xiàn)
在博主的項目中管宵,采用的是128位截珍,CBC加密鏈模式,PKCS5填充模式, BASE64編碼的AES對稱加密算法箩朴。使用HMAC-MD5進行簽名岗喉。算法工具包引入的是Hu-tool,CryptSignHandle接口實現(xiàn)
public class CryptSignHandler implements CryptSignHandler<RequestDTO, ResultDataDTO>{
@Override
public String encrypt(Object data) {
return encryptData(JSONUtil.toJsonStr(data));
}
@Override
public String decrypt(String data) {
return decryptData(data);
}
@Override
public void checkSign(RequestDTO req) {
String requestStr = req.getOperatorID() + req.getData() + req.getTimeStamp() + req.getSeq();
String sign = sign(requestStr);
if(!StrUtil.equals(sign, req.getSig())){
throw Exceptions.fail(ErrorMessage.errorMessage(RetCodeEnum.SIG_ERROR.getCode(),RetCodeEnum.SIG_ERROR.getName()));
}
}
@Override
public String sign(ResultDataDTO result) {
String sign = sign(result);
return sign;
}
/**
* 獲取AES對象
* @return
*/
public static AES getAes(){
return new AES(Mode.CBC, Padding.PKCS5Padding, getAesSecretKey().getBytes(), getAesIv().getBytes());
}
/**
* 加密
* @param data
* @return
*/
public String encryptData(Object data){
if(ObjectUtil.isNull(data)){
return "";
}
return getAes().encryptBase64(JSONUtil.toJsonStr(data));
}
/**
* 解密
* @param encryptData
* @return
*/
public static String decryptData(String encryptData){
if(StrUtil.isEmpty(encryptData)){
return "";
}
return getAes().decryptStr(encryptData);
}
/**
* 獲取hmac對象
* @return
*/
public static HMac getHMac(){
return new HMac(HmacAlgorithm.HmacMD5, getHmacMd5SignKey().getBytes());
}
/**
* 生成簽名
* @param str
* @return
*/
public static String sign(String str){
return getHMac().digestHex(str).toUpperCase();
}
}
自定義注解
如果要對加密解密進行統(tǒng)一處理炸庞,需要指定參數(shù)的基類钱床,進行加密解密的字段名,響應參數(shù)基類埠居,進行簽名設置的字段名诞丽,實現(xiàn)接口等。在需要進行加密解密操作的方法上加上該注解拐格,表示需要對請求參數(shù)和響應結果進行加密,解密刑赶,簽名驗證等捏浊。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CryptAndSign {
// 請求參數(shù)基類
Class requestVO() default RequestDTO.class;
// 響應參數(shù)基類
Class responseVO() default ResultDataDTO.class;
// 進行加密解密的字段名
String cryptFieldName() default "Data";
// 進行簽名設置的字段名
String signFieldName() default "Sig";
// 加密,解密撞叨,簽名
Class<? extends CryptSignHandler> cryptSignHandler() default CryptSignHandler.class;
}
RequestDTO 請求參數(shù)基類如下
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RequestDTO<T> implements Serializable {
@JsonProperty("OperatorID")
private String OperatorID;
@JsonProperty("Data")
private T Data;
@JsonProperty("TimeStamp")
private String TimeStamp;
@JsonProperty("Sig")
private String Sig;
@JsonProperty("Seq")
private String Seq;
}
ResultDataDTO 響應結果基類如下
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ResultDTO implements Serializable {
private String Ret;
private String Msg;
private String Data;
private String Sig;
}
AOP環(huán)繞通知操作
新增CryptAndSignAOP定義切面邏輯金踪,在方法執(zhí)行前攔截請求參數(shù)對參數(shù)中的data字段進行解密,并校驗簽名的準確性牵敷。在方法執(zhí)行后對data字段進行加密胡岔,并生成簽名賦予sig字段。
@Aspect
@Component
@Slf4j
public class CryptAndSignAOP {
/**
* 定義切點
*/
@Pointcut("@within(com.annotation.CryptAndSign) || @annotation(com.annotation.CryptAndSign)")
public void pointcut(){
}
/**
* 定義環(huán)繞切面
* @param point
* @return
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint point){
Object result = null;
// 獲取被代理的對象
Object target = point.getTarget();
// 獲取被代理方法參數(shù)
Object[] args = point.getArgs();
// 獲取通知簽名
MethodSignature signature = (MethodSignature) point.getSignature();
try {
// 獲取被代理方法
Method pointMethod = target.getClass().getMethod(signature.getName(), signature.getParameterTypes());
// 獲取被代理方法上的@CryptAndSign注解
CryptAndSign cryptAndSign = pointMethod.getAnnotation(CryptAndSign.class);
// 獲取被代理類上的@CryptAndSign注解
if(ObjectUtil.isNull(cryptAndSign)){
cryptAndSign = target.getClass().getAnnotation(CryptAndSign.class);
}
// 獲取加密解密實現(xiàn)
CryptSignHandler cryptSignObj = null;
if(ObjectUtil.isNotNull(cryptAndSign)){
// 獲取參數(shù)加密基類
Class clazz = cryptAndSign.requestVO();
cryptSignObj = (CryptSignHandler) cryptAndSign.cryptSignHandler().newInstance();
for(Object arg : args){
if(clazz.isInstance(arg)){
Object cast = clazz.cast(arg);
// 驗證請求參數(shù)簽名
cryptSignObj.checkSign(cast);
// 獲取加密解密字段名
String cryptFieldName = cryptAndSign.cryptFieldName();
// 執(zhí)行方法獲取加密數(shù)據(jù)
String encryptData = (String) getFieldValue(clazz, cast, cryptFieldName);
if(StringUtil.isNotEmpty(encryptData)){
String decryptData = cryptSignObj.decrypt(encryptData);
setFieldValue(clazz, cast, cryptFieldName, decryptData);
}
}
}
}
// 執(zhí)行請求
log.info("----[" + pointMethod.getName() + "]---> requestDTO = [{}]", JSONUtil.toJsonStr(args));
result = point.proceed(args);
log.info("----[" + pointMethod.getName() + "]---> responseDTO = [{}]", JSONUtil.toJsonStr(result));
if(ObjectUtil.isNotNull(cryptAndSign)){
Class clazz = cryptAndSign.responseVO();
String cryptFieldName = cryptAndSign.cryptFieldName();
String signName = cryptAndSign.signFieldName();
Object resultObj = clazz.cast(result);
// 加密
Object resultData = getFieldValue(clazz, resultObj, cryptFieldName);
String encryptData = cryptSignObj.encrypt(resultData);
setFieldValue(clazz, resultObj, cryptFieldName, encryptData);
// 生成簽名
String sign = cryptSignObj.sign(resultObj);
setFieldValue(clazz, resultObj, signName, sign);
}
} catch (OptimusExceptionBase e){
throw e;
} catch (Exception e) {
log.error("occur an exception, errMsg = [{}]", e.getMessage(), e);
throw Exceptions.fail(ErrorMessage.errorMessage(RetCodeEnum.INTERNAL_ERROR.getCode(), RetCodeEnum.INTERNAL_ERROR.getName()));
} catch (Throwable throwable) {
log.error("occur an exception, errMsg = [{}]", throwable.getMessage(), throwable);
throw Exceptions.fail(ErrorMessage.errorMessage(RetCodeEnum.INTERNAL_ERROR.getCode(), RetCodeEnum.INTERNAL_ERROR.getName()));
}
return result;
}
/**
* 獲取字段值
* @param clazz
* @param obj
* @param fieldName
* @return
*/
public static Object getFieldValue(Class clazz, Object obj, String fieldName){
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
} catch (NoSuchFieldException | IllegalAccessException e) {
log.error("get field value occur an exception, errMsg = [{}]", e.getMessage(), e);
}
return null;
}
/**
* 設置字段值
* @param clazz
* @param obj
* @param fieldName
* @param value
*/
public static void setFieldValue(Class clazz, Object obj, String fieldName, Object value){
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
} catch (NoSuchFieldException | IllegalAccessException e) {
log.error("set field value occur an exception, errMsg = [{}]", e.getMessage(), e);
}
}
}
定義方法
在controller中新增方法枷餐,加上@CryptAndSign注解靶瘸,標示需要加密解密,簽名驗證等操作毛肋。
@CryptAndSign
@PostMapping("/api/callback/notification_start_charge_result")
public ResultDataDTO notifyStartChargeResult(@RequestBody RequestDTO<String> requestDTO){
RequestDTO<StartChargeNotifyRequestDTO> request = CallbackUtil.convertRequestDTO(requestDTO, new TypeReference<StartChargeNotifyRequestDTO>() {});
StartChargeResultParamValidator.validate(request);
return CallbackService.notifyStartChargeResult(request.getData());
}
總結
在本文中介紹了加密怨咪,解密,簽名等幾本概念润匙,以及介紹了如何使用apo進行統(tǒng)一的參數(shù)解密诗眨,結果加密等操作。希望對大家有所幫助孕讳。