如何在SpringBoot项目上让接口返回数据脱敏,一个注解即可

news2024/9/22 1:57:44

1 背景

需求是某些接口返回的信息,涉及到敏感数据的必须进行脱敏操作

2 思路

①要做成可配置多策略的脱敏操作,要不然一个个接口进行脱敏操作,重复的工作量太多,很显然违背了“多写一行算我输”的程序员规范。思来想去,定义数据脱敏注解和数据脱敏逻辑的接口, 在返回类上,对需要进行脱敏的属性加上,并指定对应的脱敏策略操作。

接下来我只需要拦截控制器返回的数据,找到带有脱敏注解的属性操作即可,一开始打算用 @ControllerAdvice 去实现,但发现需要自己去反射类获取注解。当返回对象比较复杂,需要递归去反射,性能一下子就会降低,于是换种思路,我想到平时使用的 @JsonFormat,跟我现在的场景很类似,通过自定义注解跟字段解析器,对字段进行自定义解析,tql。

3 实现代码

3.1自定义数据注解,并可以配置数据脱敏策略:

package com.wkf.workrecord.tools.desensitization;

import java.lang.annotation.*;

/**
 * 注解类
 * @author wuKeFan
 * @date 2023-02-20 09:36:39
 */

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataMasking {

    DataMaskingFunc maskFunc() default DataMaskingFunc.NO_MASK;

}

3.2 自定义 Serializer,参考 jackson 的 StringSerializer,下面的示例只针对 String 类型进行脱敏

DataMaskingOperation.class:

package com.wkf.workrecord.tools.desensitization;

/**
 * 接口脱敏操作接口类
 * @author wuKeFan
 * @date 2023-02-20 09:37:48
 */
public interface DataMaskingOperation {

    String MASK_CHAR = "*";

    String mask(String content, String maskChar);

}

DataMaskingFunc.class:

package com.wkf.workrecord.tools.desensitization;

import org.springframework.util.StringUtils;

/**
 * 脱敏转换操作枚举类
 * @author wuKeFan
 * @date 2023-02-20 09:38:35
 */
public enum DataMaskingFunc {

    /**
     *  脱敏转换器
     */
    NO_MASK((str, maskChar) -> {
        return str;
    }),
    ALL_MASK((str, maskChar) -> {
        if (StringUtils.hasLength(str)) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < str.length(); i++) {
                sb.append(StringUtils.hasLength(maskChar) ? maskChar : DataMaskingOperation.MASK_CHAR);
            }
            return sb.toString();
        } else {
            return str;
        }
    });

    private final DataMaskingOperation operation;

    private DataMaskingFunc(DataMaskingOperation operation) {
        this.operation = operation;
    }

    public DataMaskingOperation operation() {
        return this.operation;
    }

}

DataMaskingSerializer.class:

package com.wkf.workrecord.tools.desensitization;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer;

import java.io.IOException;
import java.util.Objects;

/**
 * 自定义Serializer
 * @author wuKeFan
 * @date 2023-02-20 09:39:47
 */
public final class DataMaskingSerializer extends StdScalarSerializer<Object> {
    private final DataMaskingOperation operation;

    public DataMaskingSerializer() {
        super(String.class, false);
        this.operation = null;
    }

    public DataMaskingSerializer(DataMaskingOperation operation) {
        super(String.class, false);
        this.operation = operation;
    }


    public boolean isEmpty(SerializerProvider prov, Object value) {
        String str = (String)value;
        return str.isEmpty();
    }

    public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        if (Objects.isNull(operation)) {
            String content = DataMaskingFunc.ALL_MASK.operation().mask((String) value, null);
            gen.writeString(content);
        } else {
            String content = operation.mask((String) value, null);
            gen.writeString(content);
        }
    }

    public final void serializeWithType(Object value, JsonGenerator gen, SerializerProvider provider, TypeSerializer typeSer) throws IOException {
        this.serialize(value, gen, provider);
    }

    public JsonNode getSchema(SerializerProvider provider) {
        return this.createSchemaNode("string", true);
    }

    public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException {
        this.visitStringFormat(visitor, typeHint);
    }
}

3.3 自定义 AnnotationIntrospector,适配我们自定义注解返回相应的 Serializer

package com.wkf.workrecord.tools.desensitization;

import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector;
import lombok.extern.slf4j.Slf4j;

/**
 * @author wuKeFan
 * @date 2023-02-20 09:43:41
 */
@Slf4j
public class DataMaskingAnnotationIntroSpector extends NopAnnotationIntrospector {

    @Override
    public Object findSerializer(Annotated am) {
        DataMasking annotation = am.getAnnotation(DataMasking.class);
        if (annotation != null) {
            return new DataMaskingSerializer(annotation.maskFunc().operation());
        }
        return null;
    }

}

3.4 覆盖 ObjectMapper:

package com.wkf.workrecord.tools.desensitization;

import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

/**
 * 覆盖 ObjectMapper
 * @author wuKeFan
 * @date 2023-02-20 09:44:35
 */
@Configuration(proxyBeanMethods = false)
public class DataMaskConfiguration {

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass({Jackson2ObjectMapperBuilder.class})
    static class JacksonObjectMapperConfiguration {
        JacksonObjectMapperConfiguration() {
        }

        @Bean
        @Primary
        ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
            ObjectMapper objectMapper = builder.createXmlMapper(false).build();
            AnnotationIntrospector ai = objectMapper.getSerializationConfig().getAnnotationIntrospector();
            AnnotationIntrospector newAi = AnnotationIntrospectorPair.pair(ai, new DataMaskingAnnotationIntroSpector());
            objectMapper.setAnnotationIntrospector(newAi);
            return objectMapper;
        }
    }

}

3.5 返回对象加上注解:

package com.wkf.workrecord.tools.desensitization;

import lombok.Data;

import java.io.Serializable;

/**
 * 需要脱敏的实体类
 * @author wuKeFan
 * @date 2023-02-20 09:35:52
 */
@Data
public class User implements Serializable {
    /**
     * 主键ID
     */
    private Long id;

    /**
     * 姓名
     */
    @DataMasking(maskFunc = DataMaskingFunc.ALL_MASK)
    private String name;

    /**
     * 年龄
     */
    private Integer age;

    /**
     * 邮箱
     */
    @DataMasking(maskFunc = DataMaskingFunc.ALL_MASK)
    private String email;

}

4 测试

我们写一个Controller测试一下看是不是我们需要的效果

4.1 测试的Controller类DesensitizationController.class如下:

package com.wkf.workrecord.tools.desensitization;

import com.biboheart.brick.model.BhResponseResult;
import com.wkf.workrecord.utils.ResultVOUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试接口脱敏测试控制类
 * @author wuKeFan
 * @date 2022-06-21 17:23
 */
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/desensitization/")
public class DesensitizationController {

    @RequestMapping(value = "test", method = {RequestMethod.GET, RequestMethod.POST})
    public BhResponseResult<User> test() {
        User user = new User();
        user.setAge(1);
        user.setEmail("123456789@qq.com");
        user.setName("吴名氏");
        user.setId(1L);
        return ResultVOUtils.success(user);
    }

}

4.2 PostMan接口请求,效果符合预期,如图:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/359476.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

关于数字化营销技术实现之【数据埋点】

1.如何实现数据埋点&#xff1f;小程序数据埋点是指在小程序中收集用户行为数据和业务数据的一种技术手段&#xff0c;以便对用户行为和业务运营进行分析和优化。下面是一些实现小程序数据埋点的方法&#xff1a;使用小程序统计分析工具&#xff1a;小程序平台提供了统计分析工…

约束优化:低维线性时间线性规划算法(Seidel算法)、低维线性时间严格凸二次规划算法

文章目录约束优化&#xff1a;低维线性时间线性规划算法&#xff08;Seidel算法&#xff09;、低维线性时间严格凸二次规划算法带约束优化问题的定义带约束优化问题的分类及时间复杂度低维线性规划问题定义Seidel线性规划算法低维严格凸二次规划问题定义低维情况下的精确最小范…

【LeetCode】剑指 Offer 09. 用两个栈实现队列 p68 -- Java Version

题目链接&#xff1a;https://leetcode.cn/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/ 1. 题目介绍&#xff08;09. 用两个栈实现队列&#xff09; 用两个栈实现一个队列。队列的声明如下&#xff0c;请实现它的两个函数 appendTail 和 deleteHead &#xff0c;分别…

【大厂高频必刷真题100题】《是子序列吗?》 真题练习第28题 持续更新~

是子序列吗? 给定字符串 s 和 t ,判断 s 是否为 t 的子序列。 字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。 进阶: 如果有…

火山引擎 DataTester:在广告投放场景下的 A/B 实验实践

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 “我知道在广告上的投资有一半是无用的&#xff0c;但问题是我不知道是哪一半。” ——零售大亨约翰沃纳梅克 这句经典名言&#xff0c;被称为广告界的哥特巴赫猜想…

Python脚本批量下载CDS气象数据

使用Python脚本从 Copernicus Climate Data Store (CDS) 检索气象数据具体地&#xff0c;需要检索变量&#xff08;geopotential、relative_humidity、temperature、u_component_of_wind、v_component_of_wind、vertical_velocity&#xff09;在各种不同的压力水平、不同的日期…

罗克韦尔AB PLC_FactoryTalk无法登录的解决方法

罗克韦尔AB PLC_FactoryTalk无法登录的解决方法 情况说明: 在打开Studio 5000软件时,出现一个弹窗Log On to FactoryTalk - Network,正常情况下输入Windows账户和密码就可以登录成功。 但是却出现了下图所示窗口,其中‘abseme’是Windows账户名,‘WELL’是计算机名称,下图…

SQL零基础入门学习(二)

SQL SELECT 语句 SELECT 语句用于从数据库中选取数据。 结果被存储在一个结果表中&#xff0c;称为结果集。 SQL SELECT 语法 SELECT column1, column2, ... FROM table_name;与 SELECT * FROM table_name;参数说明&#xff1a; column1, column2, …&#xff1a;要选择的…

向上跳空缺口选股公式,选出回补后再启动的标的

一、向上跳空缺口选股公式 思路&#xff1a;先找出缺口&#xff0c;缺口前后有两根K线&#xff0c;缺口低价是前一根K线的最高价&#xff0c;缺口高价是后一根K线的最低价。&#xff08;如上图&#xff09;收盘价低于缺口低价&#xff0c;即实现缺口回补。回补缺口之后&#xf…

“一把梭ViT”来了,谷歌提出可以灵活应对各种图像块尺寸的FlexiViT

原文链接&#xff1a;https://www.techbeat.net/article-info?id4486 作者&#xff1a;seven_ 论文链接&#xff1a; https://arxiv.org/abs/2212.08013 代码链接&#xff1a; https://github.com/google-research/big_vision 视觉Transformer&#xff08;ViT&#xff09;目前…

Linux - 第4节 - Linux进程控制

1.进程创建 1.1.fork函数 在linux中fork函数是非常重要的函数&#xff0c;它从已存在进程中创建一个新进程。新进程为子进程&#xff0c;而原进程为父进程。#include <unistd.h> pid_t fork(void); 返回值&#xff1a;子进程中返回0&#xff0c;父进程返回子进程id&…

考虑泄流效应的光伏并网点电压系统侧增援调控方法matlab

目录 1主要内容 1.1 泄流效应​编辑 1.2 候选无功补偿站优选方法 1.3 算法步骤 2部分代码 3程序结果 4程序链接 1主要内容 程序主要复现《考虑泄流效应的风电场并网点电压系统侧增援调控方法_于其宜》&#xff0c;将光伏取代风电&#xff0c;考虑某时刻光伏并网的电压增…

3年工作之后是不是还在“点点点”,3年感悟和你分享....

经常都有人问我软件测试前景怎么样&#xff0c;每年也都帮助很多朋友做职业分析和学习规划&#xff0c;也很欣慰能够通过自己的努力帮到一些人进入到大厂。 2023年软件测试行业的发展现状以及未来的前景趋势 最近很多测试人在找工作的时候&#xff0c;明显的会发现功能测试很…

死磕JAVA10余年!手写“Java核心技能精选”Github一夜疯涨30w+

写在前面 想在面试、工作中脱颖而出&#xff1f;想在最短的时间内快速掌握 Java 的核心基础知识点&#xff1f;想要成为一位优秀的 Java 工程师&#xff1f;本篇文章能助你一臂之力&#xff01; 很多同学对一些新技术名词都能侃侃而谈&#xff0c;但对一些核心原理理解的不够…

很好用的 UI 调试技巧

文章目录 UI调试效果(一)评论最后UI调试小姑(二)参考文档 很好用的 UI 调试技巧 UI调试效果(一) javascript: (function() {const style = document<

RT-Thread初识学习-02

课程链接 02-RT-Thread介绍_哔哩哔哩_bilibili 学习方法 使用官方资料进行学习&#xff0c;并且在学习的过程中与FreeRTOS进行比较 RT-Thread API参考手册: 基础定义 标准版RTT移植 这里的串口2是由于打印信息的&#xff0c;因此你需要在开发板上选择USB-TTL串口&#xff0…

什么是品牌控价?品牌控价的意义是什么?品牌控价合不合法

很多人不明白为什么要控价&#xff0c;今天我们就来聊一聊品牌控价。 一、 什么是控价 顾名思义&#xff0c;“控价”就是管控价格&#xff0c;将价格控制在合理的范围以内。 品牌方生产出产品&#xff0c;要以一定的价格投入市场。而市场中的实际成交价格会受渠道各因素的影…

Kubernetes二 Kubernetes之实战以及pod详解

Kubernetes入门 一 Kubernetes实战 本章节将介绍如何在kubernetes集群中部署一个nginx服务&#xff0c;并且能够对其进行访问。 1.1 Namespace Namespace是kubernetes系统中的一种非常重要资源&#xff0c;它的主要作用是用来实现多套环境的资源隔离或者多租户的资源隔离。…

pmp考试是什么?适合哪些人学?含金量?(含pmp资料)

先说一下我这个人的理解&#xff0c;PMP就是提高项目管理理论基础和实践能力的考试。 再说说PMP官方一点的说明&#xff1a; PMP证书全称为Project Management Professional&#xff0c;也叫项目管理专业人士资格认证。PMP证书由美国项目管理协会(PMI)发起&#xff0c;是严格…

覃小龙34岁生日记:结合趋势,发挥优势,方能百战不殆

覃小龙34岁生日记:结合趋势&#xff0c;发挥优势&#xff0c;方能百战不殆&#xff01;2023-2-20星期一 覃小龙2023年2月17日&#xff0c;是我34岁生日&#xff0c;1989年出生的我&#xff0c;一晃眼&#xff0c;已经走过第34个年头了&#xff01;从2016年创业到今天&#xff0…