Spring-boot-手把手教你使用AOP进行加密解密签名验证
在上篇文章中,博主介绍了借助Spring拦截器进行token校验。在本文中,将介绍如何通过AOP来进行加密解密,签名验证等操作,来保证接口的数据传输的安全性。
加密算法
为什么需要加密呢?就好比战争时期特工在进行传输情报的时候,如果将情报明文直接通过某种媒介传输给同盟人员,那么一旦情报被地方截取,就会酿成大祸。如果将明文通过某种加密算法加密成杂乱无章的密文,即使被敌方截获,没有对应的解密算法,也很难识别出其中的明文。安全传输领域,加密算法是一种很常用的手段,它可以保证数据不被窃取和泄漏,还可以保证数据的完整性,不被篡改。
常见的加密算法有对称加密,非对称加密,单向加密(签名)等分类。其中对称加密算法,加密密钥和解密密钥是同一个,因此发送发和接收方都需要维护一个相同的密钥,如果密钥要修改,双方都需要同时修改。非对称加密算法中,发送发用公钥进行加密,接收方用私钥进行解密。单向加密算法是对传输的数据生成一个签名,通过这个签名来验证数据在传输过程中是否被篡改过,一般是不可逆的。
常用的对称加密算法有DES, AES, 3DES等, 非对称加密算法有RSA, DSA, ECB等,签名算法有SHA1, MD5, HMAC等。在本文中将使用AES和HMAC-MD5来进行数据加密解密,以及签名验证。
算法分类AES
AES 加密算法是一种对称加密算,加密密钥和解密密钥是同一个。它采用对称分组密码体制,最少支持长度为128位的加密。涉及到分组加密,padding填充,初始向量IV,密钥,四种加密模式。
-
分组加密就是将原文分割成一段段的分别进行加密,每段分组长度为128位16个字节,如果最后一组长度不足128位,则采用padding填充模式将其补齐到128位。然后对每组进行加密,最后组成最终密文。
-
padding填充是为了解决分组后的长度不足128位的场景。填充模式也有多种不同模式,比如PKCS5, PKCS7和NOPADDING。其中PKSC5是指分组后缺少几个字节,就在后面填充几个字节的几,比如缺少2个字节,就在后面填充2个字节的2。PKCS7是指缺少几个字节,就在后面填充几个字节的0,比如缺少5个字节,就填充5个字节的0。NOPADDING模式就是不需要填充。如果最后面刚好是16个字节的16,那么解密方不知道是填充数据还是真实数据,因此会在后面再补16个字节的16来区分。
-
初始向量IV是为了保证数据的安全性,如果我们对同一段内容进行加密后,所生成的密文应该是相同的,那么这样就很容易通过密文分析出哪些段是相同的。比如原文分组后成为ABCADE,加密后的密文是GHIGJK,那么很容易看出那两段内容是相同的。第一个分组在初始加密向量的基础上进行加密,以后的每一个分组都在前一个分组加密的结果为基础进行加密,从而保证了即使相同的原文段,也不会生成相同的密文段。
-
密钥是加密和解密公用的一个,它一般是128位16个字节长度的随机字符串,分组后的原文都用同一个密钥进行加密。
-
加密模式包含ECB,CBC, CFB, OFB等四种模式。ECB分别对每个分组进行加密,相同的明文会被加密成相同的密文。CBC模式会使用上一段的加密结果作为加密向量,相同的原文不会被加密成相同的密文。
MD5
MD5算法是一种不可逆的签名算法,对相同的输入通过MD5散列函数处理后,会输出相同的信息。因此MD5可以验证传输的数据是否有被篡改,但是如果窃密者对明文进行了修改后,再使用MD5算法进行散列,接收方将无法判断明文已经被修改了。一般数据库存储用户密码会将密码使用MD5进行处理。
HMAC-MD5
HMAC-MD5由一个H函数和一个密钥组成,一般我们采用的散列函数为Md5或者SHA-1。HMAC-MD5算法就是采用密钥加密+Md5信息摘要的方式形成新的密文。
AOP
众所周知,AOP(面向切面编程)是Spring一个重要特性,它将核心关注点和业务逻辑进行解耦,将业务无关的逻辑提取出来作为公共模块进行处理。它有切点,切面,连接点,通知的概念。切点就是我们可以织入切面的点,切面就是我们要织入的横切逻辑,通知包含前置通知,后置通知,返回通知,异常通知,环绕通知等。这些aop的概念,可在其它文章中了解。
加密解密接口
定一个加密解密接口,并定义一些操作方法,这样如果要更改加密或者解密算法的话就可有不同实现。
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);
}
加密解密实现
在博主的项目中,采用的是128位,CBC加密链模式,PKCS5填充模式, BASE64编码的AES对称加密算法。使用HMAC-MD5进行签名。算法工具包引入的是Hu-tool,CryptSignHandle接口实现
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();
}
}
自定义注解
如果要对加密解密进行统一处理,需要指定参数的基类,进行加密解密的字段名,响应参数基类,进行签名设置的字段名,实现接口等。在需要进行加密解密操作的方法上加上该注解,表示需要对请求参数和响应结果进行加密,解密,签名验证等。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CryptAndSign {
// 请求参数基类
Class requestVO() default RequestDTO.class;
// 响应参数基类
Class responseVO() default ResultDataDTO.class;
// 进行加密解密的字段名
String cryptFieldName() default "Data";
// 进行签名设置的字段名
String signFieldName() default "Sig";
// 加密,解密,签名
Class<? extends CryptSignHandler> cryptSignHandler() default CryptSignHandler.class;
}
RequestDTO 请求参数基类如下
@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环绕通知操作
新增CryptAndSignAOP定义切面逻辑,在方法执行前拦截请求参数对参数中的data字段进行解密,并校验签名的准确性。在方法执行后对data字段进行加密,并生成签名赋予sig字段。
@Aspect
@Component
@Slf4j
public class CryptAndSignAOP {
/**
* 定义切点
*/
@Pointcut("@within(com.annotation.CryptAndSign) || @annotation(com.annotation.CryptAndSign)")
public void pointcut(){
}
/**
* 定义环绕切面
* @param point
* @return
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint point){
Object result = null;
// 获取被代理的对象
Object target = point.getTarget();
// 获取被代理方法参数
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);
}
// 获取加密解密实现
CryptSignHandler cryptSignObj = null;
if(ObjectUtil.isNotNull(cryptAndSign)){
// 获取参数加密基类
Class clazz = cryptAndSign.requestVO();
cryptSignObj = (CryptSignHandler) cryptAndSign.cryptSignHandler().newInstance();
for(Object arg : args){
if(clazz.isInstance(arg)){
Object cast = clazz.cast(arg);
// 验证请求参数签名
cryptSignObj.checkSign(cast);
// 获取加密解密字段名
String cryptFieldName = cryptAndSign.cryptFieldName();
// 执行方法获取加密数据
String encryptData = (String) getFieldValue(clazz, cast, cryptFieldName);
if(StringUtil.isNotEmpty(encryptData)){
String decryptData = cryptSignObj.decrypt(encryptData);
setFieldValue(clazz, cast, cryptFieldName, decryptData);
}
}
}
}
// 执行请求
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进行统一的参数解密,结果加密等操作。希望对大家有所帮助。