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进行统一的参数解密,结果加密等操作。希望对大家有所帮助。

参考

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容