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 {

    /**
     * 结果加密
     * @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{
    
    @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 cryptSignHandler() default CryptSignHandler.class;
}

RequestDTO 请求参数基类如下

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RequestDTO 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 requestDTO){
        RequestDTO request = CallbackUtil.convertRequestDTO(requestDTO, new TypeReference() {});
        StartChargeResultParamValidator.validate(request);
        return CallbackService.notifyStartChargeResult(request.getData());
    }

总结

在本文中介绍了加密,解密,签名等几本概念,以及介绍了如何使用apo进行统一的参数解密,结果加密等操作。希望对大家有所帮助。

参考

https://www.jianshu.com/p/3840b344b27c?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

你可能感兴趣的:(Spring-boot-手把手教你使用AOP进行加密解密签名验证)