美文网首页
Spring-boot-手把手教你使用AOP进行加密解密签名验证

Spring-boot-手把手教你使用AOP进行加密解密签名验证

作者: Renaissance_ | 来源:发表于2020-11-23 17:26 被阅读0次

    在上篇文章中,博主介绍了借助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进行统一的参数解密,结果加密等操作。希望对大家有所帮助。

    参考

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

    相关文章

      网友评论

          本文标题:Spring-boot-手把手教你使用AOP进行加密解密签名验证

          本文链接:https://www.haomeiwen.com/subject/mfimiktx.html