最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

使用Spring和Redis創(chuàng)建處理敏感數(shù)據(jù)的服務(wù)的示例代碼

 更新時(shí)間:2025年04月22日 08:43:47   作者:蟹蟹蟹風(fēng)流  
許多公司處理的用戶敏感數(shù)據(jù)由于法律限制不能永久存儲(chǔ),根據(jù)規(guī)定,這些數(shù)據(jù)的存儲(chǔ)時(shí)間不能超過(guò)預(yù)設(shè)期限,并且最好在用于服務(wù)目的之后就將其刪除,解決這個(gè)問(wèn)題有多種可能的方案,在本文中,我想展示一個(gè)利用 Spring 和 Redis 處理敏感數(shù)據(jù)的應(yīng)用程序的簡(jiǎn)化示例

許多公司(如:金融科技公司)處理的用戶敏感數(shù)據(jù)由于法律限制不能永久存儲(chǔ)。根據(jù)規(guī)定,這些數(shù)據(jù)的存儲(chǔ)時(shí)間不能超過(guò)預(yù)設(shè)期限,并且最好在用于服務(wù)目的之后就將其刪除。解決這個(gè)問(wèn)題有多種可能的方案。在本文中,我想展示一個(gè)利用 Spring 和 Redis 處理敏感數(shù)據(jù)的應(yīng)用程序的簡(jiǎn)化示例。

Redis 是一種高性能的 NoSQL 數(shù)據(jù)庫(kù)。通常,它被用作內(nèi)存緩存解決方案,因?yàn)樗乃俣确浅??。然而,在這個(gè)示例中,我們將把它用作主要的數(shù)據(jù)存儲(chǔ)。它完美地符合我們問(wèn)題的需求,并且與 Spring Data 有很好的集成。

我們將創(chuàng)建一個(gè)管理用戶全名和卡詳細(xì)信息(作為敏感數(shù)據(jù)的示例)的應(yīng)用程序。卡詳細(xì)信息將以加密字符串的形式通過(guò) POST 請(qǐng)求傳遞給應(yīng)用程序。數(shù)據(jù)將僅在數(shù)據(jù)庫(kù)中存儲(chǔ)五分鐘。在通過(guò) GET 請(qǐng)求讀取數(shù)據(jù)之后,數(shù)據(jù)將被自動(dòng)刪除。

該應(yīng)用程序被設(shè)計(jì)為公司內(nèi)部的微服務(wù),不提供公共訪問(wèn)權(quán)限。用戶的數(shù)據(jù)可以從面向用戶的服務(wù)傳遞過(guò)來(lái)。然后,其他內(nèi)部微服務(wù)可以請(qǐng)求卡詳細(xì)信息,確保敏感數(shù)據(jù)保持安全,且無(wú)法從外部服務(wù)訪問(wèn)。

初始化 Spring Boot 項(xiàng)目

讓我們開始使用 Spring Initializr 創(chuàng)建項(xiàng)目。我們需要 Spring Web、Spring Data Redis 和 Lombok。我還添加了 Spring Boot Actuator,因?yàn)樵谡鎸?shí)微服務(wù)中它肯定會(huì)很有用。

在初始化服務(wù)之后,我們應(yīng)該添加其他依賴項(xiàng)。為了能夠在讀取數(shù)據(jù)后自動(dòng)刪除數(shù)據(jù),我們將使用 AspectJ。我還添加了一些其他對(duì)服務(wù)有幫助的依賴項(xiàng),使它看起來(lái)更接近真實(shí)的服務(wù)。

最終的 build.gradle 文件如下所示:

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.3'
    id 'io.spring.dependency-management' version '1.1.6'
    id "io.freefair.lombok" version "8.10.2"
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(22)
    }
}

repositories {
    mavenCentral()
}

ext {
    springBootVersion = '3.3.3'
    springCloudVersion = '2023.0.3'
    dependencyManagementVersion = '1.1.6'
    aopVersion = "1.9.19"
    hibernateValidatorVersion = '8.0.1.Final'
    testcontainersVersion = '1.20.2'
    jacksonVersion = '2.18.0'
    javaxValidationVersion = '3.1.0'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.boot:spring-boot-dependencies:${springBootVersion}"
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation "org.aspectj:aspectjweaver:${aopVersion}"
    implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}"
    implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}"
    implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}"
    implementation "jakarta.validation:jakarta.validation-api:${javaxValidationVersion}"
    implementation "org.hibernate:hibernate-validator:${hibernateValidatorVersion}"
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage'
    }
    testImplementation "org.testcontainers:testcontainers:${testcontainersVersion}"
    testImplementation 'org.junit.jupiter:junit-jupiter'
}

tasks.named('test') {
    useJUnitPlatform()
}

我們需要設(shè)置與 Redis 的連接。application.yml 中的 Spring Data Redis 屬性如下:

spring:
  data:
    redis:
      host: localhost
      port: 6379

領(lǐng)域模型

CardInfo 是我們將要處理的數(shù)據(jù)對(duì)象。為了使其更加真實(shí),我們讓卡詳細(xì)信息作為加密數(shù)據(jù)傳遞到服務(wù)中。我們需要解密、驗(yàn)證,然后存儲(chǔ)傳入的數(shù)據(jù)。領(lǐng)域模型將有三個(gè)層次:

  • DTO:請(qǐng)求級(jí)別,用于控制器
  • Model:服務(wù)級(jí)別,用于業(yè)務(wù)邏輯
  • Entity:持久化級(jí)別,用于倉(cāng)庫(kù)

DTO 和 Model 之間的轉(zhuǎn)換在 CardInfoConverter 中完成。Model 和 Entity 之間的轉(zhuǎn)換在 CardInfoEntityMapper 中完成。我們使用 Lombok 以方便開發(fā)。

DTO

@Builder
@Getter
@ToString(exclude = "cardDetails")
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class CardInfoRequestDto {
    @NotBlank
    private String id;
    @Valid
    private UserNameDto fullName;
    @NotNull
    private String cardDetails;
}

其中 UserNameDto

@Builder
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class UserNameDto {
    @NotBlank
    private String firstName;
    @NotBlank
    private String lastName;
}

這里的卡詳細(xì)信息表示一個(gè)加密字符串,而 fullName 是作為一個(gè)單獨(dú)的對(duì)象傳遞的。注意 cardDetails 字段是如何從 toString() 方法中排除的。由于數(shù)據(jù)是敏感的,不應(yīng)意外記錄。

Model

@Data
@Builder
public class CardInfo {
    @NotBlank
    private String id;
    @Valid
    private UserName userName;
    @Valid
    private CardDetails cardDetails;
}
@Data
@Builder
public class UserName {
    private String firstName;
    private String lastName;
}

CardInfoCardInfoRequestDto 相同,只是 cardDetails 已經(jīng)被轉(zhuǎn)換(在 CardInfoEntityMapper 中完成)。CardDetails 現(xiàn)在是一個(gè)解密后的對(duì)象,它有兩個(gè)敏感字段:pan(卡號(hào))和 CVV(安全碼):

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString(exclude = {"pan", "cvv"})
public class CardDetails {
    @NotBlank
    private String pan;
    private String cvv;
}

再次看到,我們從 toString() 方法中排除了敏感的 pan 和 CVV 字段。

Entity

@Getter
@Setter
@ToString(exclude = "cardDetails")
@NoArgsConstructor
@AllArgsConstructor
@Builder
@RedisHash
public class CardInfoEntity {
    @Id
    private String id;
    private String cardDetails;
    private String firstName;
    private String lastName;
}

為了讓 Redis 為實(shí)體創(chuàng)建哈希鍵,需要添加 @RedisHash 注解以及 @Id 注解。

以下是 DTO 轉(zhuǎn)換為 Model 的方式:

public CardInfo toModel(@NonNull CardInfoRequestDto dto) {
    final UserNameDto userName = dto.getFullName();
    return CardInfo.builder()
            .id(dto.getId())
            .userName(UserName.builder()
                    .firstName(ofNullable(userName).map(UserNameDto::getFirstName).orElse(null))
                    .lastName(ofNullable(userName).map(UserNameDto::getLastName).orElse(null))
                    .build())
            .cardDetails(getDecryptedCardDetails(dto.getCardDetails()))
            .build();
}

private CardDetails getDecryptedCardDetails(@NonNull String cardDetails) {
    try {
        return objectMapper.readValue(cardDetails, CardDetails.class);
    } catch (IOException e) {
        throw new IllegalArgumentException("Card details string cannot be transformed to Json object", e);
    }
}

在這個(gè)例子中,getDecryptedCardDetails 方法只是將字符串映射到 CardDetails 對(duì)象。在真實(shí)的應(yīng)用程序中,解密邏輯將在這個(gè)方法中實(shí)現(xiàn)。

倉(cāng)庫(kù)

使用 Spring Data 創(chuàng)建倉(cāng)庫(kù)。服務(wù)中的 CardInfo 通過(guò)其 ID 檢索,因此不需要定義自定義方法,代碼如下所示:

@Repository
public interface CardInfoRepository extends CrudRepository<CardInfoEntity, String> {
}

Redis 配置

我們需要實(shí)體只存儲(chǔ)五分鐘。為了實(shí)現(xiàn)這一點(diǎn),我們需要設(shè)置 TTL(生存時(shí)間)。我們可以通過(guò)在 CardInfoEntity 中引入一個(gè)字段并添加 @TimeToLive 注解來(lái)實(shí)現(xiàn)。也可以通過(guò)在 @RedisHash 上添加值來(lái)實(shí)現(xiàn):@RedisHash(timeToLive = 5*60)。

這兩種方法都有些缺點(diǎn)。在第一種情況下,我們需要引入一個(gè)與業(yè)務(wù)邏輯無(wú)關(guān)的字段。在第二種情況下,值是硬編碼的。還有另一種選擇:實(shí)現(xiàn) KeyspaceConfiguration。通過(guò)這種方法,我們可以使用 application.yml 中的屬性來(lái)設(shè)置 TTL,如果需要的話,還可以設(shè)置其他 Redis 屬性。

@Configuration
@RequiredArgsConstructor
@EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP)
public class RedisConfiguration {
   private final RedisKeysProperties properties;
  
   @Bean
   public RedisMappingContext keyValueMappingContext() {
       return new RedisMappingContext(
               new MappingConfiguration(new IndexConfiguration(), new CustomKeyspaceConfiguration()));
   }

   public class CustomKeyspaceConfiguration extends KeyspaceConfiguration {
     
       @Override
       protected Iterable<KeyspaceSettings> initialConfiguration() {
           return Collections.singleton(customKeyspaceSettings(CardInfoEntity.class, CacheName.CARD_INFO));
       }

       private <T> KeyspaceSettings customKeyspaceSettings(Class<T> type, String keyspace) {
           final KeyspaceSettings keyspaceSettings = new KeyspaceSettings(type, keyspace);
           keyspaceSettings.setTimeToLive(properties.getCardInfo().getTimeToLive().toSeconds());
           return keyspaceSettings;
       }
   }

   @NoArgsConstructor(access = AccessLevel.PRIVATE)
   public static class CacheName {
       public static final String CARD_INFO = "cardInfo";
   }
}

為了使 Redis 能夠根據(jù) TTL 刪除實(shí)體,需要在 @EnableRedisRepositories 注解中添加 enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP。我引入了 CacheName 類,以便使用常量作為實(shí)體名稱,并反映如果需要的話可以對(duì)多個(gè)實(shí)體進(jìn)行不同的配置。

TTL 的值是從 RedisKeysProperties 對(duì)象中獲取的。

@Data
@Component
@ConfigurationProperties("redis.keys")
@Validated
public class RedisKeysProperties {
   @NotNull
   private KeyParameters cardInfo;
   @Data
   @Validated
   public static class KeyParameters {
       @NotNull
       private Duration timeToLive;
   }
}

這里只有 cardInfo 這個(gè)實(shí)體,但可能還有其他實(shí)體存在。 應(yīng)用.yml 中的 TTL 屬性:

redis:
 keys:
   cardInfo:
     timeToLive: PT5M

Controller

讓我們?yōu)樵摲?wù)添加 API,以便能夠通過(guò) HTTP 存儲(chǔ)和訪問(wèn)數(shù)據(jù)。

@RestController
@RequiredArgsConstructor
@RequestMapping( "/api/cards")
public class CardController {
   private final CardService cardService;
   private final CardInfoConverter cardInfoConverter;
  
   @PostMapping
   @ResponseStatus(CREATED)
   public void createCard(@Valid @RequestBody CardInfoRequestDto cardInfoRequest) {
       cardService.createCard(cardInfoConverter.toModel(cardInfoRequest));
   }
  
   @GetMapping("/{id}")
   public ResponseEntity<CardInfoResponseDto> getCard(@PathVariable("id") String id) {
       return ResponseEntity.ok(cardInfoConverter.toDto(cardService.getCard(id)));
   }
}

基于 AOP 的自動(dòng)刪除功能

我們希望在通過(guò) GET 請(qǐng)求成功讀取該實(shí)體之后立即對(duì)其進(jìn)行刪除。這可以通過(guò) AOP 和 AspectJ 來(lái)實(shí)現(xiàn)。我們需要?jiǎng)?chuàng)建一個(gè) Spring Bean 并用 @Aspect 進(jìn)行注解。

@Aspect
@Component
@RequiredArgsConstructor
@ConditionalOnExpression("${aspect.cardRemove.enabled:false}")
public class CardRemoveAspect {
   private final CardInfoRepository repository;

   @Pointcut("execution(* com.cards.manager.controllers.CardController.getCard(..)) && args(id)")
   public void cardController(String id) {
   }

   @AfterReturning(value = "cardController(id)", argNames = "id")
   public void deleteCard(String id) {
       repository.deleteById(id);
   }
}

@Pointcut 定義了邏輯應(yīng)用的切入點(diǎn)。換句話說(shuō),它決定了觸發(fā)邏輯執(zhí)行的時(shí)機(jī)。deleteCard 方法定義了具體的邏輯,它通過(guò) CardInfoRepository 按照 ID 刪除 cardInfo 實(shí)體。@AfterReturning 注解表明該方法會(huì)在 value 屬性中定義的方法成功返回后執(zhí)行。

此外,我還使用了 @ConditionalOnExpression 注解來(lái)根據(jù)配置屬性開啟或關(guān)閉這一功能。

測(cè)試

我們將使用 MockMvc 和 Testcontainers 來(lái)編寫 test case。

public abstract class RedisContainerInitializer {
   private static final int PORT = 6379;
   private static final String DOCKER_IMAGE = "redis:6.2.6";

   private static final GenericContainer REDIS_CONTAINER = new GenericContainer(DockerImageName.parse(DOCKER_IMAGE))
           .withExposedPorts(PORT)
           .withReuse(true);

   static {
       REDIS_CONTAINER.start();
   }
  
   @DynamicPropertySource
   static void properties(DynamicPropertyRegistry registry) {
       registry.add("spring.data.redis.host", REDIS_CONTAINER::getHost);
       registry.add("spring.data.redis.port", () -> REDIS_CONTAINER.getMappedPort(PORT));
   }
}

通過(guò) @DynamicPropertySource,我們可以從啟動(dòng)的 Redis Docker 容器中設(shè)置屬性。隨后,這些屬性將被應(yīng)用程序讀取,以建立與 Redis 的連接。

以下是針對(duì) POST 和 GET 請(qǐng)求的基本測(cè)試:

public class CardControllerTest extends BaseTest {
   private static final String CARDS_URL = "/api/cards";
   private static final String CARDS_ID_URL = CARDS_URL + "/{id}";

   @Autowired
   private CardInfoRepository repository;
  
   @BeforeEach
   public void setUp() {
       repository.deleteAll();
   }
  
   @Test
   public void createCard_success() throws Exception {
       final CardInfoRequestDto request = aCardInfoRequestDto().build();
       
       mockMvc.perform(post(CARDS_URL)
                       .contentType(APPLICATION_JSON)
                       .content(objectMapper.writeValueAsBytes(request)))
               .andExpect(status().isCreated())
       ;
       assertCardInfoEntitySaved(request);
   }
  
   @Test
   public void getCard_success() throws Exception {
       final CardInfoEntity entity = aCardInfoEntityBuilder().build();
       prepareCardInfoEntity(entity);

       mockMvc.perform(get(CARDS_ID_URL, entity.getId()))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.id", is(entity.getId())))
               .andExpect(jsonPath("$.cardDetails", notNullValue()))
               .andExpect(jsonPath("$.cardDetails.cvv", is(CVV)))
       ;
   }
}

通過(guò) AOP 進(jìn)行自動(dòng)刪除功能測(cè)試:

@Test
@EnabledIf(
       expression = "${aspect.cardRemove.enabled}",
       loadContext = true
)

public void getCard_deletedAfterRead() throws Exception {
   final CardInfoEntity entity = aCardInfoEntityBuilder().build();
   prepareCardInfoEntity(entity);

   mockMvc.perform(get(CARDS_ID_URL, entity.getId()))
           .andExpect(status().isOk());
   mockMvc.perform(get(CARDS_ID_URL, entity.getId()))
           .andExpect(status().isNotFound())
   ;
}

我為這個(gè)測(cè)試添加了 @EnabledIf 注解,因?yàn)?AOP 邏輯可以在配置文件中關(guān)閉,而該注解則用于決定是否要運(yùn)行該測(cè)試。

以上就是使用Spring和Redis創(chuàng)建處理敏感數(shù)據(jù)的服務(wù)的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Spring Redis處理敏感數(shù)據(jù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • JDBC PreparedStatement Like參數(shù)報(bào)錯(cuò)解決方案

    JDBC PreparedStatement Like參數(shù)報(bào)錯(cuò)解決方案

    這篇文章主要介紹了JDBC PreparedStatement Like參數(shù)報(bào)錯(cuò)解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-10-10
  • Springboot+rabbitmq實(shí)現(xiàn)延時(shí)隊(duì)列的兩種方式

    Springboot+rabbitmq實(shí)現(xiàn)延時(shí)隊(duì)列的兩種方式

    這篇文章主要介紹了Springboot+rabbitmq實(shí)現(xiàn)延時(shí)隊(duì)列的兩種方式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2021-05-05
  • 簡(jiǎn)單幾步實(shí)現(xiàn)將Spring security4.x升級(jí)到5.x

    簡(jiǎn)單幾步實(shí)現(xiàn)將Spring security4.x升級(jí)到5.x

    這篇文章主要介紹了簡(jiǎn)單幾步實(shí)現(xiàn)將Spring security4.x升級(jí)到5.x方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-08-08
  • Java編程接口詳細(xì)

    Java編程接口詳細(xì)

    這篇文章主要小編主要給大家講解的是Java編程中的接口,文章會(huì)從抽象類和抽象方法開始展開內(nèi)容,感興趣的小伙伴可以參考下面文章的具體內(nèi)容
    2021-10-10
  • Log4j 配置日志打印時(shí)區(qū)的實(shí)現(xiàn)方法

    Log4j 配置日志打印時(shí)區(qū)的實(shí)現(xiàn)方法

    下面小編就為大家分享一篇Log4j 配置日志打印時(shí)區(qū)的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2017-12-12
  • 阿里、華為、騰訊Java技術(shù)面試題精選

    阿里、華為、騰訊Java技術(shù)面試題精選

    這篇文章主要為大家分享了阿里、華為、騰訊Java技術(shù)面試題精選,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-11-11
  • 解決springdataJPA對(duì)原生sql支持的問(wèn)題

    解決springdataJPA對(duì)原生sql支持的問(wèn)題

    這篇文章主要介紹了解決springdataJPA對(duì)原生sql支持的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-06-06
  • idea搭建SSM框架遇踩的坑(附完整過(guò)程)

    idea搭建SSM框架遇踩的坑(附完整過(guò)程)

    最近準(zhǔn)備搭建一個(gè)SSM框架,由于很久沒有搭建了,一來(lái)就遇到各種問(wèn)題,折騰了一天終于搞定了,特此記錄一下遇到的問(wèn)題,下面這篇文章主要給大家介紹了關(guān)于idea搭建SSM框架遇踩的坑,文中還附完整過(guò)程,需要的朋友可以參考下
    2023-04-04
  • Spring配置中transactionAttributes的使用方法介紹

    Spring配置中transactionAttributes的使用方法介紹

    這篇文章主要介紹了Spring配置中transactionAttributes的使用方法介紹的相關(guān)內(nèi)容,具有一定參考價(jià)值,需要的朋友可以了解下。
    2017-09-09
  • Spring源碼解析容器初始化構(gòu)造方法

    Spring源碼解析容器初始化構(gòu)造方法

    這篇文章主要介紹了Spring源碼解析容器初始化構(gòu)造方法,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下
    2022-07-07

最新評(píng)論

潜江市| 肃北| 黔西| 和静县| 甘洛县| 房山区| 泰安市| 新野县| 奉节县| 苍溪县| 澎湖县| 宝应县| 烟台市| 孝昌县| 揭西县| 宜黄县| 岗巴县| 昆明市| 衡东县| 全南县| 桐柏县| 兴城市| 房产| 永宁县| 利辛县| 罗江县| 普陀区| 广安市| 襄樊市| 定远县| 元朗区| 壤塘县| 兖州市| 乌兰浩特市| 同江市| 平陆县| 陆良县| 封丘县| 贵阳市| 丰县| 剑河县|