Springboot+Jackson自定義注解數(shù)據(jù)脫敏的項(xiàng)目實(shí)踐
數(shù)據(jù)脫敏也叫數(shù)據(jù)的去隱私化,在我們給定脫敏規(guī)則和策略的情況下,對(duì)敏感數(shù)據(jù)比如 手機(jī)號(hào)、銀行卡號(hào) 等信息,進(jìn)行轉(zhuǎn)換或者修改的一種技術(shù)手段,防止敏感數(shù)據(jù)直接在不可靠的環(huán)境下使用。
由于spring 接口返回JSON數(shù)據(jù)是使用的Jackson組件;我們可以利用Jackson+自定義注解的方式去實(shí)現(xiàn)數(shù)據(jù)脫敏;實(shí)現(xiàn)接口返回?cái)?shù)據(jù)自動(dòng)進(jìn)行數(shù)據(jù)脫敏
一、導(dǎo)入相關(guān)依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-core</artifactId> <version>${hutool.version}</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-crypto</artifactId> <version>${hutool.version}</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-system</artifactId> <version>${hutool.version}</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-json</artifactId> <version>5.8.9</version> </dependency>
二、代碼實(shí)現(xiàn)
1、新建一個(gè)頂層的脫敏接口
import java.util.function.Function; /** * 自定義數(shù)據(jù)脫敏可實(shí)現(xiàn)當(dāng)前接口 或直接在 SensitiveStrategy 中添加枚舉 * * @author minjianguo * @date 2023/08/09 */ @FunctionalInterface public interface IDesensitizeRule { /** * 脫敏操作 * * @return {@link String} */ Function<String, String> desensitize(); }
2、創(chuàng)建一個(gè)枚舉類并繼承接口
利用枚舉類+函數(shù)式接口代替普通策略模式更加方便快捷
import cn.hutool.core.util.DesensitizedUtil; import com.poctip.encryption.util.UtilTools; import lombok.AllArgsConstructor; import java.util.function.Function; /** * 敏感戰(zhàn)略 * 脫敏策略 * * @author minjianguo * @version 3.6.0 * @date 2023/08/09 */ @AllArgsConstructor public enum SensitiveStrategy implements IDesensitizeRule { DEFAULT(s -> s), /** * 身份證脫敏 */ ID_CARD(s -> DesensitizedUtil.idCardNum(s, 3, 4)), /** * 手機(jī)號(hào)脫敏 */ PHONE(DesensitizedUtil::mobilePhone), /** * 地址脫敏 */ ADDRESS(s -> UtilTools.encryption(s, 2, 0)), /** * 郵箱脫敏 */ EMAIL(DesensitizedUtil::email), /** * 銀行卡 */ BANK_CARD(DesensitizedUtil::bankCard), /** * 名字 * 適配原有加密組件 */ NAME(s -> UtilTools.encryption(s, 1, 0)), TELEPHONE(s -> UtilTools.encryption(s, 1, 3)); //可自行添加其他脫敏策略 private final Function<String, String> desensitizer; /** * 脫敏操作 * * @return {@link String} */ @Override public Function<String, String> desensitize() { return desensitizer; } }
3、自定義注解
- strategy: 數(shù)據(jù)脫敏策略
- isCustomRule:是否啟用自定義的規(guī)則
- customRule:自定義的實(shí)現(xiàn)類
默認(rèn)使用 strategy 枚舉里面的策略,如果要新增新的數(shù)據(jù)脫敏規(guī)則可直接在 SensitiveStrategy 類新增脫敏規(guī)則枚舉;(如果要把這個(gè)數(shù)據(jù)脫敏組件打成一個(gè)jar包的時(shí)候就沒(méi)辦法在 **SensitiveStrategy** 里面新增枚舉了;只能采用另一種方式新增自定義脫敏規(guī)則)示例如下:
- 新增脫敏規(guī)則類實(shí)現(xiàn) IDesensitizeRule 接口,重寫(xiě) desensitize 方法。
- 在需要脫敏的字段標(biāo)注 @Sensitive(isCustomRule = true,customRule = UnknownDesensitizeRule.class) UnknownDesensitizeRule 為自己的脫敏規(guī)則類 即可。\
@JacksonAnnotationsInside是一個(gè)元注解,它用于注解其他注解,以指示這些注解可以用于Jackson庫(kù)的注解處理器中。
@JsonSerialize(using = SensitiveHandler.class)是一個(gè)用于屬性或字段的注解,它指示Jackson庫(kù)在序列化過(guò)程中使用自定義的SensitiveHandler類進(jìn)行處理。
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.poctip.common.sensitive.core.IDesensitizeRule; import com.poctip.common.sensitive.core.SensitiveStrategy; import com.poctip.common.sensitive.core.UnknownDesensitizeRule; import com.poctip.common.sensitive.handler.SensitiveHandler; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 敏感 * 數(shù)據(jù)脫敏注解 * * @author * @date 2023/08/09 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @JacksonAnnotationsInside @JsonSerialize(using = SensitiveHandler.class) public @interface Sensitive { /** * 策略 * * @return {@link SensitiveStrategy} */ SensitiveStrategy strategy() default SensitiveStrategy.DEFAULT; /** * 是否自定義規(guī)則 * * @return boolean */ boolean isCustomRule() default false; /** * 自定義規(guī)則實(shí)現(xiàn)類 * * @return {@link Class}<{@link ?} {@link extends} {@link IDesensitizeRule}> */ Class<? extends IDesensitizeRule> customRule() default UnknownDesensitizeRule.class; }
- SensitiveMethodMask :標(biāo)注在要數(shù)據(jù)脫敏的接口方法上
- exclude:排除當(dāng)前接口不需要脫敏的字段
/** * 敏感方法面具 * * @author minjianguo * @date 2023/08/03 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface SensitiveMethodMask { String[] exclude() default {}; }
3.1、創(chuàng)建一個(gè)默認(rèn)的脫敏實(shí)現(xiàn)類;不做任何脫敏操作 UnknownDesensitizeRule;目的是為了給注解一個(gè)默認(rèn)值
import java.util.function.Function; /** * 不處理數(shù)據(jù)脫敏 * * @author minjianguo * @date 2023/08/09 */ public class UnknownDesensitizeRule implements IDesensitizeRule{ /** * 脫敏操作 * * @return {@link String} */ @Override public Function<String, String> desensitize() { return s -> s; } }
4、核心邏輯代碼
創(chuàng)建一個(gè)數(shù)據(jù)脫敏上下文SensitiveContextHolder
通過(guò) SensitiveContextHolder和 spring 的 攔截器配合使用實(shí)現(xiàn)在當(dāng)前接口總啟用或關(guān)閉或排除某些字段不進(jìn)行序列化
/** * 上下文敏感持有人 * * @author minjianguo * @date 2023/08/03 */ public class SensitiveContextHolder { private static final ThreadLocal<Boolean> THREAD_LOCAL = ThreadLocal.withInitial(() -> false); private static final ThreadLocal<String[]> FIELD_THREAD_LOCAL = ThreadLocal.withInitial(() -> new String[0]); /** * 指定字段不脫敏 * * @param fields 字段 */ public static void filter(String... fields) { if (Objects.isNull(fields)) return; FIELD_THREAD_LOCAL.set(fields); } /** * 得到過(guò)濾字段 * * @return {@link String[]} */ public static String[] getFilterFields() { return FIELD_THREAD_LOCAL.get(); } public static boolean isDisable() { return THREAD_LOCAL.get(); } public static void enable() { THREAD_LOCAL.set(true); } public static boolean isSensitive() { return THREAD_LOCAL.get(); } /** * 禁用 */ public static void disable() { THREAD_LOCAL.set(true); } /** * 清晰 */ public static void clear() { THREAD_LOCAL.remove(); FIELD_THREAD_LOCAL.remove(); } }
新增一個(gè)Jackson序列化器
- 在Jackson庫(kù)中,JsonSerializer是一個(gè)抽象類,用于自定義對(duì)象的序列化過(guò)程。它將Java對(duì)象轉(zhuǎn)換為JSON字符串的過(guò)程中,允許開(kāi)發(fā)人員對(duì)序列化過(guò)程進(jìn)行自定義控制,并可以處理一些特定的序列化需求。
- ContextualSerializer是Jackson庫(kù)中的一個(gè)接口,用于定義上下文相關(guān)的序列化器。它擴(kuò)展了JsonSerializer接口,并添加了一個(gè)額外的方法createContextual()。
- ContextualSerializer接口允許序列化器在序列化過(guò)程中獲取上下文信息,并根據(jù)上下文進(jìn)行動(dòng)態(tài)配置或自定義序列化邏輯。它提供了一種在序列化過(guò)程中動(dòng)態(tài)決定序列化器行為的機(jī)制。
import cn.hutool.core.util.StrUtil; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonStreamContext; import com.fasterxml.jackson.databind.BeanProperty; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.ContextualSerializer; import com.poctip.common.sensitive.annotation.Sensitive; import com.poctip.common.sensitive.context.SensitiveContextHolder; import com.poctip.common.sensitive.core.IDesensitizeRule; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.util.Arrays; import java.util.Objects; /** * 數(shù)據(jù)脫敏json序列化工具 * * @author Yjoioooo */ @Slf4j public class SensitiveHandler extends JsonSerializer<String> implements ContextualSerializer { private IDesensitizeRule strategy; @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { try { String[] filterFields = SensitiveContextHolder.getFilterFields(); JsonStreamContext outputContext = gen.getOutputContext(); String currentName = outputContext.getCurrentName(); boolean noneMatch = Arrays.stream(filterFields).noneMatch(s -> StrUtil.equals(s, currentName)); boolean isSensitive = SensitiveContextHolder.isSensitive() && noneMatch; if (isSensitive) { gen.writeString(strategy.desensitize().apply(value)); } else { gen.writeString(value); } } catch (Exception e) { log.error("脫敏失敗 => {}", e.getMessage()); gen.writeString(value); } } @SneakyThrows @Override public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { Sensitive annotation = property.getAnnotation(Sensitive.class); if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())) { if (annotation.isCustomRule()) { Class<? extends IDesensitizeRule> rule = annotation.customRule(); this.strategy = rule.getDeclaredConstructor().newInstance(); return this; } this.strategy = annotation.strategy(); return this; } return prov.findValueSerializer(property.getType(), property); } }
實(shí)現(xiàn) spring 攔截器
步驟:
- 在進(jìn)入請(qǐng)求前判斷當(dāng)前接口是否要開(kāi)啟數(shù)據(jù)脫敏功能。
- 在數(shù)據(jù)序列化完成之后 清除上下文,防止內(nèi)存泄露。
import com.poctip.common.sensitive.annotation.SensitiveMethodMask; import com.poctip.common.sensitive.context.SensitiveContextHolder; import lombok.extern.slf4j.Slf4j; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Slf4j public class SensitiveInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 在 MappingJackson2HttpMessageConverter 后執(zhí)行的自定義方法 if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; SensitiveMethodMask annotation = getSensitiveMethodMaskAnnotation(handlerMethod); if (annotation != null) { enableSensitiveContext(annotation); } } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { if (handler instanceof HandlerMethod) { if (getSensitiveMethodMaskAnnotation((HandlerMethod) handler) != null) { SensitiveContextHolder.clear(); } } } /** * 啟用敏感上下文。 * * @param annotation SensitiveMethodMask注解 */ private void enableSensitiveContext(SensitiveMethodMask annotation) { SensitiveContextHolder.enable(); SensitiveContextHolder.filter(annotation.exclude()); } /** * 從處理方法中獲取SensitiveMethodMask注解。 * * @param handlerMethod 處理方法 * @return SensitiveMethodMask注解,如果不存在則返回null */ private SensitiveMethodMask getSensitiveMethodMaskAnnotation(HandlerMethod handlerMethod) { return handlerMethod.getMethod().getAnnotation(SensitiveMethodMask.class); } }
新增一個(gè)手動(dòng)脫敏的工具類
在某些場(chǎng)景下可以手動(dòng)去處理數(shù)據(jù)脫敏。
import cn.hutool.core.annotation.AnnotationUtil; import cn.hutool.core.convert.Convert; import cn.hutool.core.util.ReflectUtil; import com.poctip.common.sensitive.annotation.Sensitive; import com.poctip.common.sensitive.core.SensitiveStrategy; import lombok.SneakyThrows; import org.apache.commons.lang3.StringUtils; import java.lang.reflect.Field; import java.util.List; import java.util.Objects; /** * 脫敏工具類 * * @author minjianguo * @date 2022/12/02 */ public class SensitiveUtils { /** * 數(shù)據(jù)脫敏 */ @SneakyThrows public static <T> void dataDesensitization(T t) { if (Objects.isNull(t)) return; Field[] fields = ReflectUtil.getFields(t.getClass(), field -> field.isAnnotationPresent(Sensitive.class)); for (Field field : fields) { Object fieldValue = ReflectUtil.getFieldValue(t, field); if (Objects.isNull(fieldValue)) { continue; } Sensitive sensitive = AnnotationUtil.getAnnotation(field, Sensitive.class); SensitiveStrategy strategy = sensitive.strategy(); String desensitizationString = strategy.desensitize().apply(Convert.convert(String.class,fieldValue)); if (StringUtils.isNotBlank(desensitizationString)) { ReflectUtil.setFieldValue(t, field, desensitizationString); } } } /** * 數(shù)據(jù)脫敏列表 * * @param list 列表 */ public static <T> void dataDesensitizationList(List<T> list){ list.forEach(SensitiveUtils::dataDesensitization); } @SneakyThrows public static String dataDesensitizationString(String value, SensitiveStrategy strategy) { if (StringUtils.isBlank(value) || Objects.isNull(strategy)) { return value; } return strategy.desensitize().apply(value); } }
三、配置spring 攔截器
配置自定義的攔截器。
@Configuration public class SensitiveConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SensitiveInterceptor()) .addPathPatterns("/**"); // 這里可以指定攔截的路徑 } }
四、使用
在需要數(shù)據(jù)脫敏的實(shí)體類字段標(biāo)注注解 @Sensitive(,并指定脫敏策略
@Sensitive(strategy = SensitiveStrategy.PHONE) private String phone;
在接口方法上標(biāo)注 @SensitiveMethodMask 注解
@SensitiveMethodMask @GetMapping("/test") public UserDemo test() { UserDemo userDemo = new UserDemo(); userDemo.setId(1); userDemo.setName("java"); userDemo.setPhone("18160360561"); return userDemo; }
測(cè)試返回:
{ "id": 1, "name": "java", "phone": "181****0561" }
注意事項(xiàng):
接口的返回值不能是 object 或 Map 等類型,不然在序列化時(shí)無(wú)法找到 **@Sensitive** 注解使數(shù)據(jù)脫敏功能失效,一定要正確指定返回的數(shù)據(jù)類型。
到此這篇關(guān)于Springboot+Jackson自定義注解數(shù)據(jù)脫敏的項(xiàng)目實(shí)踐的文章就介紹到這了,更多相關(guān)Springboot Jackson數(shù)據(jù)脫敏內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot使用自定義注解實(shí)現(xiàn)數(shù)據(jù)脫敏過(guò)程詳細(xì)解析
- SpringBoot實(shí)現(xiàn)返回值數(shù)據(jù)脫敏的步驟詳解
- Springboot+Hutool自定義注解實(shí)現(xiàn)數(shù)據(jù)脫敏
- SpringBoot利用自定義注解實(shí)現(xiàn)隱私數(shù)據(jù)脫敏(加密顯示)的解決方案
- SpringBoot利用自定義json序列化器實(shí)現(xiàn)敏感字段數(shù)據(jù)脫敏詳解
- 淺析如何在SpringBoot中實(shí)現(xiàn)數(shù)據(jù)脫敏
- SpringBoot數(shù)據(jù)脫敏的實(shí)現(xiàn)示例
- SpringBoot接口返回?cái)?shù)據(jù)脫敏(Mybatis、Jackson)
相關(guān)文章
Spring?Boot?ORM?框架JPA使用與連接池?Hikari詳解
這篇文章主要介紹了SpringBoot?ORM框架JPA與連接池Hikari,主要就是介紹JPA?的使用姿勢(shì),本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-08-08springboot3.x版本集成log4j遇到Logging?system?failed?to?initial
使用Springboot?3.x集成Log4j時(shí)可能會(huì)遇到版本沖突的問(wèn)題,這通??梢酝ㄟ^(guò)檢查Maven依賴樹(shù)來(lái)識(shí)別,一旦發(fā)現(xiàn)沖突,將Log4j的版本統(tǒng)一更新到最新的兼容版本,例如2.21.1,即可解決問(wèn)題,此方法有效解決了日志打印錯(cuò)誤,是處理類似問(wèn)題的一個(gè)實(shí)用參考2024-09-09SpringBoot實(shí)現(xiàn)優(yōu)雅停機(jī)的多種方式
優(yōu)雅停機(jī)(Graceful Shutdown)在現(xiàn)代微服務(wù)架構(gòu)中是非常重要的,它幫助我們確保在應(yīng)用程序停止時(shí),不會(huì)中斷正在進(jìn)行的請(qǐng)求或?qū)е聰?shù)據(jù)丟失,讓我們以通俗易懂的方式來(lái)講解這個(gè)概念以及如何在 Spring Boot 中實(shí)現(xiàn)它,需要的朋友可以參考下2025-01-01Java并發(fā)中線程封閉知識(shí)點(diǎn)詳解
在本篇文章里我們給大家整理了關(guān)于Java并發(fā)中線程封閉的知識(shí)點(diǎn)總結(jié)內(nèi)容,需要的朋友們學(xué)習(xí)參考下。2019-07-07mybatis中的if?test判斷入?yún)⒌闹祮?wèn)題
這篇文章主要介紹了mybatis中的if?test判斷入?yún)⒌闹祮?wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06