Spring Boot REST API:如何用UUID作为@PathVariable隐藏敏感数据?
用UUID解决删除接口ID暴露风险的两种实现(Spring Data JPA + MariaDB)
你的删除接口用自增Long ID当路径参数,确实存在风险——攻击者可以通过遍历连续ID(比如1、2、3...)批量删除数据,UUID就是用来解决这个问题的。具体实现分两种思路,看你的场景选:
思路一:直接把UUID设为主键
这种是把原来的Long主键彻底换成UUID,对外暴露的就是UUID,从根源上避免自增ID的遍历问题。
1. 修改实体类
把主键字段改成UUID类型,用JPA注解自动生成:
import jakarta.persistence.*; import java.util.UUID; @Entity @Table(name = "subscriptions") public class Subscription { @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; // 其他业务字段... // 补全Getter、Setter }
2. 更新Repository
把Repository的主键泛型换成UUID:
import org.springframework.data.jpa.repository.JpaRepository; import java.util.UUID; public interface SubscriptionRepository extends JpaRepository<Subscription, UUID> { }
3. 调整接口和Service
把接口的路径参数改成UUID,Service层对应调整:
@DeleteMapping("/{uuid}") public ResponseEntity<String> deleteSubscription(@PathVariable UUID uuid) { subscriptionService.deleteSubscription(uuid); return ResponseEntity.noContent().build(); }
数据库适配(MariaDB)
MariaDB可以用CHAR(36)存储UUID,10.7+版本支持原生UUID类型,建表语句示例:
CREATE TABLE subscriptions ( id CHAR(36) NOT NULL PRIMARY KEY, -- 其他字段... );
思路二:保留Long主键,新增UUID作为对外标识
如果你的项目已经有大量历史数据,不想改动原有主键结构,可以新增一个UUID字段专门对外暴露,内部还是用Long ID操作数据库。
1. 实体类加UUID字段
新增一个唯一、不可修改的UUID字段,用@PrePersist自动生成:
import jakarta.persistence.*; import java.util.UUID; @Entity @Table(name = "subscriptions") public class Subscription { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false, updatable = false) private UUID publicId; // 其他业务字段... // 保存前自动生成UUID @PrePersist public void generatePublicId() { if (publicId == null) { publicId = UUID.randomUUID(); } } // 补全Getter、Setter }
2. Repository加查询方法
新增根据publicId查找实体的方法:
import org.springframework.data.jpa.repository.JpaRepository; import java.util.UUID; import java.util.Optional; public interface SubscriptionRepository extends JpaRepository<Subscription, Long> { Optional<Subscription> findByPublicId(UUID publicId); }
3. Service层调整删除逻辑
先通过publicId找到实体,再用内部Long ID删除:
@Service public class SubscriptionService { private final SubscriptionRepository subscriptionRepository; // 构造注入 public SubscriptionService(SubscriptionRepository subscriptionRepository) { this.subscriptionRepository = subscriptionRepository; } public void deleteSubscription(UUID publicId) { Subscription subscription = subscriptionRepository.findByPublicId(publicId) .orElseThrow(() -> new RuntimeException("订阅不存在")); subscriptionRepository.deleteById(subscription.getId()); } }
4. 接口修改
路径参数换成UUID:
@DeleteMapping("/{publicId}") public ResponseEntity<String> deleteSubscription(@PathVariable UUID publicId) { subscriptionService.deleteSubscription(publicId); return ResponseEntity.noContent().build(); }
两种方案怎么选
- 选思路一:适合新项目或者能接受数据迁移的场景,结构更简洁,不用额外字段。
- 选思路二:适合已有大量历史数据的老项目,兼容原有主键,不用改动旧数据。
核心逻辑都是对外暴露无规律、不可遍历的UUID,让攻击者没法靠猜ID恶意操作。
内容的提问来源于stack exchange,提问作者madalin1Q
相关产品推荐
相关产品推荐

