设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间17 现在时间17 缓存数据 这个世界上很多东西会变,很多人会走。 但你在我心里,从开始的那一天,到现在从来没有变过。 我一直在等,等你的消息。

这个世界上很多东西会变,很多人会走。 但你在我心里,从开始的那一天,到现在从来没有变过。 我一直在等,等你的消息。 -- 盛夏的果实

查看: 528|回复: 0

使用Spring和Redis创建处理敏感数据的服务的示例代码

[复制链接]

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
30
主题
26
精华
0
金钱
93
积分
58
注册时间
2023-9-29
最后登录
2025-5-31

发表于 2025-5-31 05:56:21 | 显示全部楼层 |阅读模式

许多公司(如:金融科技公司)处理的用户敏感数据由于法律限制不能永久存储。根据规定,这些数据的存储时间不能超过预设期限,并且最好在用于服务目的之后就将其删除。解决这个问题有多种可能的方案。在本文中,我想展示一个利用 Spring 和 Redis 处理敏感数据的应用程序的简化示例。

Redis 是一种高性能的 NoSQL 数据库。通常,它被用作内存缓存解决方案,因为它的速度非常快。然而,在这个示例中,我们将把它用作主要的数据存储。它完美地符合我们问题的需求,并且与 Spring Data 有很好的集成。

我们将创建一个管理用户全名和卡详细信息(作为敏感数据的示例)的应用程序。卡详细信息将以加密字符串的形式通过 POST 请求传递给应用程序。数据将仅在数据库中存储五分钟。在通过 GET 请求读取数据之后,数据将被自动删除。

该应用程序被设计为公司内部的微服务,不提供公共访问权限。用户的数据可以从面向用户的服务传递过来。然后,其他内部微服务可以请求卡详细信息,确保敏感数据保持安全,且无法从外部服务访问。

初始化 Spring Boot 项目

让我们开始使用 Spring Initializr 创建项目。我们需要 Spring Web、Spring Data Redis 和 Lombok。我还添加了 Spring Boot Actuator,因为在真实微服务中它肯定会很有用。

在初始化服务之后,我们应该添加其他依赖项。为了能够在读取数据后自动删除数据,我们将使用 AspectJ。我还添加了一些其他对服务有帮助的依赖项,使它看起来更接近真实的服务。

最终的

  1. build.gradle
复制代码
文件如下所示:

  1. plugins {
  2. id 'java'
  3. id 'org.springframework.boot' version '3.3.3'
  4. id 'io.spring.dependency-management' version '1.1.6'
  5. id "io.freefair.lombok" version "8.10.2"
  6. }
  7. java {
  8. toolchain {
  9. languageVersion = JavaLanguageVersion.of(22)
  10. }
  11. }
  12. repositories {
  13. mavenCentral()
  14. }
  15. ext {
  16. springBootVersion = '3.3.3'
  17. springCloudVersion = '2023.0.3'
  18. dependencyManagementVersion = '1.1.6'
  19. aopVersion = "1.9.19"
  20. hibernateValidatorVersion = '8.0.1.Final'
  21. testcontainersVersion = '1.20.2'
  22. jacksonVersion = '2.18.0'
  23. javaxValidationVersion = '3.1.0'
  24. }
  25. dependencyManagement {
  26. imports {
  27. mavenBom "org.springframework.boot:spring-boot-dependencies:${springBootVersion}"
  28. mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
  29. }
  30. }
  31. dependencies {
  32. implementation 'org.springframework.boot:spring-boot-starter-data-redis'
  33. implementation 'org.springframework.boot:spring-boot-starter-web'
  34. implementation 'org.springframework.boot:spring-boot-starter-actuator'
  35. implementation "org.aspectj:aspectjweaver:${aopVersion}"
  36. implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}"
  37. implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}"
  38. implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}"
  39. implementation "jakarta.validation:jakarta.validation-api:${javaxValidationVersion}"
  40. implementation "org.hibernate:hibernate-validator:${hibernateValidatorVersion}"
  41. testImplementation('org.springframework.boot:spring-boot-starter-test') {
  42. exclude group: 'org.junit.vintage'
  43. }
  44. testImplementation "org.testcontainers:testcontainers:${testcontainersVersion}"
  45. testImplementation 'org.junit.jupiter:junit-jupiter'
  46. }
  47. tasks.named('test') {
  48. useJUnitPlatform()
  49. }
复制代码

我们需要设置与 Redis 的连接。

  1. application.yml
复制代码
 中的 Spring Data Redis 属性如下:

  1. spring:
  2. data:
  3. redis:
  4. host: localhost
  5. port: 6379
复制代码

领域模型

  1. CardInfo
复制代码
是我们将要处理的数据对象。为了使其更加真实,我们让卡详细信息作为加密数据传递到服务中。我们需要解密、验证,然后存储传入的数据。领域模型将有三个层次:

  • DTO:请求级别,用于控制器
  • Model:服务级别,用于业务逻辑
  • Entity:持久化级别,用于仓库

DTO 和 Model 之间的转换在

  1. CardInfoConverter
复制代码
中完成。Model 和 Entity 之间的转换在
  1. CardInfoEntityMapper
复制代码
中完成。我们使用 Lombok 以方便开发。

DTO

  1. @Builder
  2. @Getter
  3. @ToString(exclude = "cardDetails")
  4. @NoArgsConstructor
  5. @AllArgsConstructor
  6. @JsonIgnoreProperties(ignoreUnknown = true)
  7. public class CardInfoRequestDto {
  8. @NotBlank
  9. private String id;
  10. @Valid
  11. private UserNameDto fullName;
  12. @NotNull
  13. private String cardDetails;
  14. }
复制代码

其中 UserNameDto

  1. @Builder
  2. @Getter
  3. @ToString
  4. @NoArgsConstructor
  5. @AllArgsConstructor
  6. @JsonIgnoreProperties(ignoreUnknown = true)
  7. public class UserNameDto {
  8. @NotBlank
  9. private String firstName;
  10. @NotBlank
  11. private String lastName;
  12. }
复制代码

这里的卡详细信息表示一个加密字符串,而 

  1. fullName
复制代码
 是作为一个单独的对象传递的。注意 
  1. cardDetails
复制代码
 字段是如何从 
  1. toString()
复制代码
 方法中排除的。由于数据是敏感的,不应意外记录。

Model

  1. @Data
  2. @Builder
  3. public class CardInfo {
  4. @NotBlank
  5. private String id;
  6. @Valid
  7. private UserName userName;
  8. @Valid
  9. private CardDetails cardDetails;
  10. }
复制代码
  1. @Data
  2. @Builder
  3. public class UserName {
  4. private String firstName;
  5. private String lastName;
  6. }
复制代码

  1. CardInfo
复制代码
  1. CardInfoRequestDto
复制代码
相同,只是
  1. cardDetails
复制代码
已经被转换(在
  1. CardInfoEntityMapper
复制代码
中完成)。
  1. CardDetails
复制代码
现在是一个解密后的对象,它有两个敏感字段:pan(卡号)和 CVV(安全码):

  1. @Data
  2. @Builder
  3. @NoArgsConstructor
  4. @AllArgsConstructor
  5. @ToString(exclude = {"pan", "cvv"})
  6. public class CardDetails {
  7. @NotBlank
  8. private String pan;
  9. private String cvv;
  10. }
复制代码

再次看到,我们从 

  1. toString()
复制代码
 方法中排除了敏感的 pan 和 CVV 字段。

Entity

  1. @Getter
  2. @Setter
  3. @ToString(exclude = "cardDetails")
  4. @NoArgsConstructor
  5. @AllArgsConstructor
  6. @Builder
  7. @RedisHash
  8. public class CardInfoEntity {
  9. @Id
  10. private String id;
  11. private String cardDetails;
  12. private String firstName;
  13. private String lastName;
  14. }
复制代码

为了让 Redis 为实体创建哈希键,需要添加 

  1. @RedisHash
复制代码
 注解以及 
  1. @Id
复制代码
 注解。

以下是 DTO 转换为 Model 的方式:

  1. public CardInfo toModel(@NonNull CardInfoRequestDto dto) {
  2. final UserNameDto userName = dto.getFullName();
  3. return CardInfo.builder()
  4. .id(dto.getId())
  5. .userName(UserName.builder()
  6. .firstName(ofNullable(userName).map(UserNameDto::getFirstName).orElse(null))
  7. .lastName(ofNullable(userName).map(UserNameDto::getLastName).orElse(null))
  8. .build())
  9. .cardDetails(getDecryptedCardDetails(dto.getCardDetails()))
  10. .build();
  11. }
  12. private CardDetails getDecryptedCardDetails(@NonNull String cardDetails) {
  13. try {
  14. return objectMapper.readValue(cardDetails, CardDetails.class);
  15. } catch (IOException e) {
  16. throw new IllegalArgumentException("Card details string cannot be transformed to Json object", e);
  17. }
  18. }
复制代码

在这个例子中,

  1. getDecryptedCardDetails
复制代码
方法只是将字符串映射到
  1. CardDetails
复制代码
对象。在真实的应用程序中,解密逻辑将在这个方法中实现。

仓库

使用 Spring Data 创建仓库。服务中的

  1. CardInfo
复制代码
通过其 ID 检索,因此不需要定义自定义方法,代码如下所示:

  1. @Repository
  2. public interface CardInfoRepository extends CrudRepository<CardInfoEntity, String> {
  3. }
复制代码

Redis 配置

我们需要实体只存储五分钟。为了实现这一点,我们需要设置 TTL(生存时间)。我们可以通过在

  1. CardInfoEntity
复制代码
中引入一个字段并添加
  1. @TimeToLive
复制代码
注解来实现。也可以通过在
  1. @RedisHash
复制代码
上添加值来实现:
  1. @RedisHash(timeToLive = 5*60)
复制代码

这两种方法都有些缺点。在第一种情况下,我们需要引入一个与业务逻辑无关的字段。在第二种情况下,值是硬编码的。还有另一种选择:实现

  1. KeyspaceConfiguration
复制代码
。通过这种方法,我们可以使用
  1. application.yml
复制代码
中的属性来设置 TTL,如果需要的话,还可以设置其他 Redis 属性。

  1. @Configuration
  2. @RequiredArgsConstructor
  3. @EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP)
  4. public class RedisConfiguration {
  5. private final RedisKeysProperties properties;
  6. @Bean
  7. public RedisMappingContext keyValueMappingContext() {
  8. return new RedisMappingContext(
  9. new MappingConfiguration(new IndexConfiguration(), new CustomKeyspaceConfiguration()));
  10. }
  11. public class CustomKeyspaceConfiguration extends KeyspaceConfiguration {
  12. @Override
  13. protected Iterable<KeyspaceSettings> initialConfiguration() {
  14. return Collections.singleton(customKeyspaceSettings(CardInfoEntity.class, CacheName.CARD_INFO));
  15. }
  16. private <T> KeyspaceSettings customKeyspaceSettings(Class<T> type, String keyspace) {
  17. final KeyspaceSettings keyspaceSettings = new KeyspaceSettings(type, keyspace);
  18. keyspaceSettings.setTimeToLive(properties.getCardInfo().getTimeToLive().toSeconds());
  19. return keyspaceSettings;
  20. }
  21. }
  22. @NoArgsConstructor(access = AccessLevel.PRIVATE)
  23. public static class CacheName {
  24. public static final String CARD_INFO = "cardInfo";
  25. }
  26. }
复制代码

为了使 Redis 能够根据 TTL 删除实体,需要在

  1. @EnableRedisRepositories
复制代码
注解中添加
  1. enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP
复制代码
。我引入了
  1. CacheName
复制代码
类,以便使用常量作为实体名称,并反映如果需要的话可以对多个实体进行不同的配置。

TTL 的值是从

  1. RedisKeysProperties
复制代码
对象中获取的。

  1. @Data
  2. @Component
  3. @ConfigurationProperties("redis.keys")
  4. @Validated
  5. public class RedisKeysProperties {
  6. @NotNull
  7. private KeyParameters cardInfo;
  8. @Data
  9. @Validated
  10. public static class KeyParameters {
  11. @NotNull
  12. private Duration timeToLive;
  13. }
  14. }
复制代码

这里只有 cardInfo 这个实体,但可能还有其他实体存在。 应用.yml 中的 TTL 属性:

  1. redis:
  2. keys:
  3. cardInfo:
  4. timeToLive: PT5M
复制代码

Controller

让我们为该服务添加 API,以便能够通过 HTTP 存储和访问数据。

  1. @RestController
  2. @RequiredArgsConstructor
  3. @RequestMapping( "/api/cards")
  4. public class CardController {
  5. private final CardService cardService;
  6. private final CardInfoConverter cardInfoConverter;
  7. @PostMapping
  8. @ResponseStatus(CREATED)
  9. public void createCard(@Valid @RequestBody CardInfoRequestDto cardInfoRequest) {
  10. cardService.createCard(cardInfoConverter.toModel(cardInfoRequest));
  11. }
  12. @GetMapping("/{id}")
  13. public ResponseEntity<CardInfoResponseDto> getCard(@PathVariable("id") String id) {
  14. return ResponseEntity.ok(cardInfoConverter.toDto(cardService.getCard(id)));
  15. }
  16. }
复制代码

基于 AOP 的自动删除功能

我们希望在通过 GET 请求成功读取该实体之后立即对其进行删除。这可以通过 AOP 和 AspectJ 来实现。我们需要创建一个 Spring Bean 并用 

  1. @Aspect
复制代码
 进行注解。

  1. @Aspect
  2. @Component
  3. @RequiredArgsConstructor
  4. @ConditionalOnExpression("${aspect.cardRemove.enabled:false}")
  5. public class CardRemoveAspect {
  6. private final CardInfoRepository repository;
  7. @Pointcut("execution(* com.cards.manager.controllers.CardController.getCard(..)) && args(id)")
  8. public void cardController(String id) {
  9. }
  10. @AfterReturning(value = "cardController(id)", argNames = "id")
  11. public void deleteCard(String id) {
  12. repository.deleteById(id);
  13. }
  14. }
复制代码

  1. @Pointcut
复制代码
定义了逻辑应用的切入点。换句话说,它决定了触发逻辑执行的时机。
  1. deleteCard
复制代码
方法定义了具体的逻辑,它通过
  1. CardInfoRepository
复制代码
按照 ID 删除
  1. cardInfo
复制代码
实体。
  1. @AfterReturning
复制代码
注解表明该方法会在
  1. value
复制代码
属性中定义的方法成功返回后执行。

此外,我还使用了

  1. @ConditionalOnExpression
复制代码
注解来根据配置属性开启或关闭这一功能。

测试

我们将使用 MockMvc 和 Testcontainers 来编写 test case。

  1. public abstract class RedisContainerInitializer {
  2. private static final int PORT = 6379;
  3. private static final String DOCKER_IMAGE = "redis:6.2.6";
  4. private static final GenericContainer REDIS_CONTAINER = new GenericContainer(DockerImageName.parse(DOCKER_IMAGE))
  5. .withExposedPorts(PORT)
  6. .withReuse(true);
  7. static {
  8. REDIS_CONTAINER.start();
  9. }
  10. @DynamicPropertySource
  11. static void properties(DynamicPropertyRegistry registry) {
  12. registry.add("spring.data.redis.host", REDIS_CONTAINER::getHost);
  13. registry.add("spring.data.redis.port", () -> REDIS_CONTAINER.getMappedPort(PORT));
  14. }
  15. }
复制代码

通过 

  1. @DynamicPropertySource
复制代码
,我们可以从启动的 Redis Docker 容器中设置属性。随后,这些属性将被应用程序读取,以建立与 Redis 的连接。

以下是针对 POST 和 GET 请求的基本测试:

  1. public class CardControllerTest extends BaseTest {
  2. private static final String CARDS_URL = "/api/cards";
  3. private static final String CARDS_ID_URL = CARDS_URL + "/{id}";
  4. @Autowired
  5. private CardInfoRepository repository;
  6. @BeforeEach
  7. public void setUp() {
  8. repository.deleteAll();
  9. }
  10. @Test
  11. public void createCard_success() throws Exception {
  12. final CardInfoRequestDto request = aCardInfoRequestDto().build();
  13. mockMvc.perform(post(CARDS_URL)
  14. .contentType(APPLICATION_JSON)
  15. .content(objectMapper.writeValueAsBytes(request)))
  16. .andExpect(status().isCreated())
  17. ;
  18. assertCardInfoEntitySaved(request);
  19. }
  20. @Test
  21. public void getCard_success() throws Exception {
  22. final CardInfoEntity entity = aCardInfoEntityBuilder().build();
  23. prepareCardInfoEntity(entity);
  24. mockMvc.perform(get(CARDS_ID_URL, entity.getId()))
  25. .andExpect(status().isOk())
  26. .andExpect(jsonPath("$.id", is(entity.getId())))
  27. .andExpect(jsonPath("$.cardDetails", notNullValue()))
  28. .andExpect(jsonPath("$.cardDetails.cvv", is(CVV)))
  29. ;
  30. }
  31. }
复制代码

通过 AOP 进行自动删除功能测试:

  1. @Test
  2. @EnabledIf(
  3. expression = "${aspect.cardRemove.enabled}",
  4. loadContext = true
  5. )
  6. public void getCard_deletedAfterRead() throws Exception {
  7. final CardInfoEntity entity = aCardInfoEntityBuilder().build();
  8. prepareCardInfoEntity(entity);
  9. mockMvc.perform(get(CARDS_ID_URL, entity.getId()))
  10. .andExpect(status().isOk());
  11. mockMvc.perform(get(CARDS_ID_URL, entity.getId()))
  12. .andExpect(status().isNotFound())
  13. ;
  14. }
复制代码

我为这个测试添加了 

  1. @EnabledIf
复制代码
 注解,因为 AOP 逻辑可以在配置文件中关闭,而该注解则用于决定是否要运行该测试。

以上就是使用Spring和Redis创建处理敏感数据的服务的示例代码的详细内容,更多关于Spring Redis处理敏感数据的资料请关注晓枫资讯其它相关文章!


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表