Springboot项目如何设计接口中敏感字段的加密解密

Posted 凡夫贩夫

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Springboot项目如何设计接口中敏感字段的加密解密相关的知识,希望对你有一定的参考价值。

目录

前言

基本概念

敏感数据

数据加密

数据解密

加密方式

解决思路

实现方案

环境配置

依赖配置

代码实现

加密结果

解密结果

总结


前言

相信大家都有这样一个烦恼,就是经常会接到各种推销、广告的电话和短信,如果你没有在他那里留下过联系方式,他又是如何得到了你的联系方式呢?毫无疑问,是个人信息被泄漏了。个人信息的泄漏有人为不合法谋利的因素,也有系统不合理的安全设计造成泄漏的因素。当然系统设计的角度出发,敏感信息需要加密存储的,数据展示的时候也要进行相应的脱敏处理,但是从一些关于个信息泄漏的新闻报道来看,有好多的网站后台竟然是“裸奔”状态,简直太可怕了。其实敏感数据的处理也不复杂,说到底是安全意识不强。当然,这篇文章和大家分享的重点是加密和解密的方法,不是数据安全的重要性。

基本概念

敏感数据

 敏感数据是指那些泄漏后可能会给社会或个人造成严重危害的数据,以个人隐私信息为例,如手机号码、家庭住址、邮箱、身份证号、银行卡帐号、购物网站的支付密码、登陆密码等等。另外从社会的角度出发,也有很多数据是属于敏感数据,如:居民的生物基因信息等等。

数据加密

 数据加密是指对数据重新编码来保护数据,获取实际数据的唯一办法就是使用密钥解密数据;

数据解密

 数据解密与数据加密是相对的,即使用密钥对加密的数据进行解密的过程;

加密方式

 加密的方式,一般是两种:对称加密和非对称加密;

对称加密只有一个秘钥,加密和解密都是用同一个秘钥,如AES、DES等;

非对称加密有两个秘钥,一个是公钥,一个是私钥。使用公钥对数据进行加密,加密后的数据只有私钥可以解密,一般公钥是公开的,私钥是不公开的;如RSA、DSA等;

解决思路

Springboot项目中,客户端通过接口向服务端读取或写入敏感数据时,常会有这样的业务需求:

1、在客户端向服务器端发起写入请求,服务端需要对写入的敏感数据进行加密后存储;

2、在客户端从服务器端向外读取数据的时候,需要对输出的敏感数据里德解密;

显然这种场景,对于加密的方式的选择,对称加密是最好的选择;那么如何实现对写入请求、读取请求的敏感数据的加密、解密处理呢?解决方案如下:

1、自定义两个切面注解,分别是加密切面注解、解密切面注解,作用于需要加密或解密的敏感数据处理的业务处理类的具体业务处理方法上;

2、自定义两个敏感字段处理注解,分别是加密字段注解、解密字段注解,作用于需要输入或输出的对象的敏感字段上;如果输入对象上标记了加密字段注解,则表示该字段在对内写入数据库的时候,需要加密处理;同理,如果输出对象上标记了解密字段注解,则表示该字段在对外输出的时候,需要进行解密;

3、使用面向切面编程,定义两个切面类,分别是加密切面类和解密切面类,选择Spring AOP的环绕通知来具体实现;加密切面类中,以注解的方式定义切入点,用到的注解就是自定义的加密切面注解;

4、如果新增、编辑等写入类的业务请求处理方法上标记了加密切面注解,那么写入请求在正式被业务处理方法处理前,会命中加密切面类,加密切面类的环绕通知方法被触发,然后根据输入的参数对象中的字段是否标记了自定义的加密字段注解,来决定是否对当前字段进行加密处理;

5、同理,如果是查询等读取类的业务请求处理方法上标记了解密切面注解,那么读取请求被业务处理类处理完之后,会命中解密切面类,解密切面类的环绕通知方法被触发,然后根据返回对象的字段是否标记了解密字段注解,来决定是否对当前字段进行解密处理。

实现方案

环境配置

jdk版本:1.8开发工具:Intellij iDEA 2020.1

springboot:2.3.9.RELEASE

mybatis-spring-boot-starter:2.1.4

依赖配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.3.3</version>
</dependency>

示例代码工作时序图

代码实现

1、自定义四个注解:@DecryptField(解密字段注解)、@EncryptField(加密字段注解)、@NeedEncrypt(解密切面注解)、@NeedEncrypt(加密切面注解),其中@DecryptField作用于需要解密的字段上;@EncryptField作用于需要加密的字段上;@NeedEncrypt作用于需要对入参数进行加密处理的方法上;@NeedDecrypt作用于需要对返回值进行解密处理的方法上;

//解密字段注解
@Target(value = ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptField 
//加密字段注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField 
//作用于对返回值进行解密处理的方法上
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedDecrypt 
作用于需要对入参数进行加密处理的方法上
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedEncrypt 

2、把自定义的加密字段注解、解密字段注解标记在需要加密或者解密的字段上;这里表示在写入人员的手机号码、身份证号码、家庭住址门牌号码时,要进行加密处理;在读取人员的手机号码、身份证号码、家庭住址门牌号码时,要进行解密处理;

@Slf4j
@Data
public class Person  
 private Integer id;
 private String userName;
 private String loginNo;
 @EncryptField
 @DecryptField
 private String phoneNumber;
 private String sex;
 @DecryptField
 @EncryptField
 private String IDCard;
 private String address;
 @EncryptField
 @DecryptField
 private String houseNumber;

3、把@NeedEncrypt和@NeedDecrypt标记在需要对入参数、返回值中的敏感字段进行加密、解密处理的业务处理方法上;

@RestController
@RequestMapping("/person")
@Slf4j
public class PersonController 
    @Autowired
    private IPersonService personService;
    //添加人员信息
    @PostMapping("/add")
    @NeedEncrypt
    public Person add(@RequestBody Person person, Model model) 
        Person result = this.personService.registe(person);
        log.info("//增加person执行完成");
        return result;
    
    //人员信息列表查询
    @GetMapping("/list")
    @NeedDecrypt
    public List<Person> getPerson() 
        List<Person> persons = this.personService.getPersonList();
        log.info("//查询person列表执行完成");
        return persons;
    
    //人员信息详情查询
    @GetMapping("/id")
    @NeedDecrypt
    public Person get(@PathVariable Integer id) 
        Person person= this.personService.get(id);
        log.info("//查询person详情执行完成");
        return person;
    

4、自定义加密切面类(EncryptAop)和解密切面类(DecryptAop):用@NeedEncrypt注解定义加密切点,在加密切点的环绕通知方法里执行到具体的业务处理方法之前,判断输入对象的参数字段是否标记了@EncryptField(加密字段注解),如果判断结果为true,则使用java反射对该字段进行加密处理,注意这里引用了hutool的工具包,使用了工具包里的加密和解密方法,这里也可以替换成其他的方式;用@NeedDecrypt注解定义解密切点,在解密切点的环绕通知方法里执行完具体的业务处理方法之后,判断输出对象的参数字段是否标记了@DecryptField(解密字段注解),如果判断结果为true,则使用java反射对该 字段进行解密处理;

@Component
@Aspect
@Slf4j
public class EncryptAop 
    /**
     * 定义加密切入点
     */
    @Pointcut(value = "@annotation(com.fanfu.anno.NeedEncrypt)")
    public void pointcut() 
    

    /**
     * 命中加密切入点的环绕通知
     *
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable 
        log.info("//环绕通知 start");
        //获取命中目标方法的入参数
        Object[] args = proceedingJoinPoint.getArgs();
        if (args.length > 0) 
            for (Object arg : args) 
                //按参数的类型进行判断,如果业务中还有其他的类型,可酌情增加
                if (arg != null) 
                    if (arg instanceof List) 
                        for (Object tmp : ((List) arg)) 
                            //加密处理
                            this.deepProcess(tmp);
                        
                     else 
                        this.deepProcess(arg);
                    
                
            
        
        //对敏感数据加密后执行目标方法
        Object result = proceedingJoinPoint.proceed();
        log.info("//环绕通知 end");
        return result;
    

    public void deepProcess(Object obj) throws IllegalAccessException 
        if (obj != null) 
            //获取对象的所有字段属性并遍历
            Field[] declaredFields = obj.getClass().getDeclaredFields();
            for (Field declaredField : declaredFields) 
                //判断字段属性上是否标记了@EncryptField注解
                if (declaredField.isAnnotationPresent(EncryptField.class)) 
                    //如果判断结果为真,则取出字段属性值,进行加密、重新赋值
                    declaredField.setAccessible(true);
                    Object valObj = declaredField.get(obj);
                    if (valObj != null) 
                        String value = valObj.toString();
                        //开始敏感字段属性值加密
                        String decrypt = this.encrypt(value);
                        //把加密后的字段属性值重新赋值
                        declaredField.set(obj, decrypt);
                    
                
            
        
    

    private String encrypt(String value) 
        //这里特别注意一下,对称加密是根据密钥进行加密和解密的,加密和解密的密钥是相同的,一旦泄漏,就无秘密可言,
        //“fanfu-csdn”就是我自定义的密钥,这里仅作演示使用,实际业务中,这个密钥要以安全的方式存储;
        byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue(), "fanfu-csdn".getBytes()).getEncoded();
        SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.DES, key);
        String encryptValue = aes.encryptBase64(value);
        return encryptValue;
    

@Component
@Aspect
@Slf4j
public class DecryptAop 
    /**
     * 定义需要解密的切入点
     */
    @Pointcut(value = "@annotation(com.fanfu.anno.NeedDecrypt)")
    public void pointcut() 
    

    /**
     * 命中的切入点时的环绕通知
     *
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable 
        log.info("//环绕通知 start");
        //执行目标方法
        Object result = proceedingJoinPoint.proceed();
        //判断目标方法的返回值类型
        if (result instanceof List) 
            for (Object tmp : ((List) result)) 
                //数据脱敏处理逻辑
                this.deepProcess(tmp);
            
         else 
            this.deepProcess(result);
        
        log.info("//环绕通知 end");
        return result;
    

    public void deepProcess(Object obj) throws IllegalAccessException 
        if (obj != null) 
            //取出输出对象的所有字段属性,并遍历
            Field[] declaredFields = obj.getClass().getDeclaredFields();
            for (Field declaredField : declaredFields) 
                //判断字段属性上是否标记DecryptField注解
                if (declaredField.isAnnotationPresent(DecryptField.class)) 
                    //如果判断结果为真,则取出字段属性数据进行解密处理
                    declaredField.setAccessible(true);
                    Object valObj = declaredField.get(obj);
                    if (valObj != null) 
                        String value = valObj.toString();
                        //加密数据的解密处理
                        value = this.decrypt(value);
                        DecryptField annotation = declaredField.getAnnotation(DecryptField.class);
                        boolean open = annotation.open();
                        //把解密后的数据重新赋值
                        declaredField.set(obj, value);
                    
                
            
        
    

    private String decrypt(String value) 
        //这里特别注意一下,对称加密是根据密钥进行加密和解密的,加密和解密的密钥是相同的,一旦泄漏,就无秘密可言,
        //“fanfu-csdn”就是我自定义的密钥,这里仅作演示使用,实际业务中,这个密钥要以安全的方式存储;
        byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue(), "fanfu-csdn".getBytes()).getEncoded();
        SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.DES, key);
        String decryptStr = aes.decryptStr(value);
        return decryptStr;
    

加密结果

解密结果

总结

这篇着重和大家分享的内容如下:

1、敏感数据的一些基础概念;

2、敏感数据处理的解决思路;

3、敏感数据处理的具体实现方式;

希望我的分享可以给大家一些启示,在遇到类似问题上可以有更好的处理,原创不易,欢迎点赞评论,如果我的分享对你有用,收藏起来吧。

「解决方案」SpringBoot项目中如何解决并发导致的重复提交问题

优质文章,第一时间送达

来源:http://suo.im/66liCE

本文前篇是对场景的分析,后篇会有解决方案,读完本篇你将可以仅仅使用两个注解即可解决并发重复提交问题。

可以直接看方案四,直接读推荐解决方案。

场景分析

重复提交问题是一个老生常谈的问题,项目中经常会遇到这种情况,这种情况在查询类接口其实也没有太大问题,但是如果是在设计修改数据的接口就有会严重问题,但是这种情况并也不难处理,因为我们的代码最少会做一个幂等判断,即会先有一个查询动作,查询不到才会放行。但是难就难在假如说是并发加重复提交这种场景就很难处理。这个时候就不得不去思考新的解决方案。

解决方案

方案一、

通过数据库唯一索引来解决,即在数据库创建一张唯一表,在每次数据请求时候将唯一键作为数据插入这张唯一表中,正常情况是可以插入成功的,当出现重复提交情况就会异常提示。

缺点

  1. 数据库性能问题,因为每次操作都设计到数据库的一次插入动作,所以可能会有性能问题

  2. 数据只有一次处理机会,当第一次处理失败,第二次在进来就当重复给拦截了

方案二、

token令牌,后端提供一个生成令牌的接口,前端在每次进行数据访问时候,先拿去token令牌,后端通过对token令牌的生命周期管控,来解决重复提交问题

缺点: 前后端改造大,后端要单独维护一个接口,前端每次请求也要多调一个接口

总结

希望通过查询+修改方式来解决并发和重复提交问题都是不现实的,因为不能保证查询和修改是一个原子性操作,所以只要并发就很容易突破这种方式的防重逻辑。那么如何解决这个问题呢? 其实就是保证防重逻辑的原子性操作。同样也是两种方案。

方案三、

类似于通过数据库唯一索引这种方式,不同的是将数据库换成内存缓存即项目里定义一个Cache集合缓存可以用Guava的缓存框架,设置缓存时间和缓存数量来解决。不过也是有缺点的,缺点就是不满足分布式要求,当请求打到其他应用服务器就突破了这种情况。所以不建议使用这种方案。如果是单机器可以考虑。

方案四、

是对上一种方案的改进,通过Redis实现,每次请求都插入Redis数据库中,并设置过期时间, 既能满足性能需求,同时也满足分布式情况。同时Redis因为是单线程的所以也能保证原子性。综上所述这种方案应该是最好的。

  • 满足原子性

  • 满足分布式环境应用

  • 性能有保证

  • 支持重试(通过设置过期时间)


伪代码如下


「解决方案」SpringBoot项目中如何解决并发导致的重复提交问题

终结解决方案

该方案是对上面方案四的一个实现,感兴趣的同学一个start一下,然后拉下来看看实现原理。

核心原理就是方案四中提的,通过拦截和自动配置无缝整合到SpringBoot项目中使用。

「解决方案」SpringBoot项目中如何解决并发导致的重复提交问题

使用方式

「解决方案」SpringBoot项目中如何解决并发导致的重复提交问题

如何判断是否引用成功

当出现tomato Logo即说明启用成功

「解决方案」SpringBoot项目中如何解决并发导致的重复提交问题

感兴趣的同学可以学习一下代码,提出任何问题小编都会第一时间回复。一起探讨学习。

接下来小编会围绕Redis做更多的实战分享目前定下来的两个议题是:

1.基于Redis原子性操作实战应用一之并发拦截 「Tomato」

2.基于Redis原子性操作实战应用二之防洪限流「Easy-Sentinel」

这两个议题其实实战代码都已经写好了,只是还没有总结成文,感兴趣的同学可以先到github上拉去实战代码。Tomato就是解决并发导致的重复提交,而Easy-Sentinel会更高级一点,利用Redis+Lua脚本实现原子性操作,从而来达到防洪限流的能力。


关注程序员闪充宝后台回复“666”和“111免费领取46阶段以及实战java视频资料



看完本文有收获?请转发分享给更多人


你在看?

以上是关于Springboot项目如何设计接口中敏感字段的加密解密的主要内容,如果未能解决你的问题,请参考以下文章

设计模式-责任链模式在实际项目中的使用

网站是怎么屏蔽脏话的呢:简单学会SpringBoot项目敏感词违规词过滤方案

SpringBoot 如何保证接口安全?老鸟们都是这么玩的

「解决方案」SpringBoot项目中如何解决并发导致的重复提交问题

一个注解(优雅)搞定SpringBoot项目中的身份证号手机号等敏感数据脱敏

一个注解(优雅)搞定SpringBoot项目中的身份证号手机号等敏感数据脱敏