Skip to main content Skills Marketplace Descubre y explora habilidades de IA creadas por la comunidad.
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Copiar promptMostrar detalles del prompt Un comando directo omite el prompt de revisión. Revisa el origen antes de ejecutarlo.
npx skills add https://github.com/doccker/cc-use-exp --skill multi-tenant-safetyEl comando permanece en una sola línea. Desplázate horizontalmente para revisarlo antes de copiarlo.
¿Prefieres una copia local? Descarga los archivos que SkillsMP tiene disponibles ahora.
Descargar Zip Descargando... Ocupaciones relacionadas SOC
Basado en la clasificación ocupacional SOC
Explorador de archivos
2 archivos name multi-tenant-safety description 当代码涉及多租户隔离(TenantContext、tenantId、租户拦截器/过滤器、X-Tenant-Code)时触发。 防止租户越权访问、数据串租户等安全问题。
多租户隔离安全规范
当系统涉及多租户架构时,防止租户间数据越权访问。
陷阱 #1: 租户上下文来源信任错误
场景 : 拦截器/过滤器从请求头(如 X-Tenant-Code)设置租户上下文,但未与认证 token 中的 tenantId 做一致性校验
问题根因
请求头可以被客户端任意伪造。如果后端只信任请求头中的租户标识,攻击者只需修改 header 就能访问其他租户的数据。
错误示例
@Override
public boolean preHandle (HttpServletRequest request, ...) {
String tenantCode = request.getHeader("X-Tenant-Code" );
TenantMiniAppConfig config = configRepository.findByTenantCode(tenantCode);
TenantContext.setTenantId(config.getTenantId());
return true ;
}
正确做法
@Override
public boolean preHandle (HttpServletRequest request, ...) {
request.getHeader( );
configRepository.findByTenantCode(tenantCode);
(Long) request.getAttribute( );
(tokenTenantId != && !tokenTenantId.equals(config.getTenantId())) {
response.setStatus( );
response.getWriter().write( );
;
}
TenantContext.setTenantId(config.getTenantId());
;
}
String
tenantCode
=
"X-Tenant-Code"
TenantMiniAppConfig
config
=
Long
tokenTenantId
=
"tokenTenantId"
if
null
403
"{\"code\":403,\"message\":\"租户信息不匹配\"}"
return
false
return
true
检查清单
陷阱 #1.5: Service 层调用 Repository.findById(id) 缺租户过滤
与陷阱 #2 并行:陷阱 #2 是「全局过滤兜底」,本陷阱是「显式深度防御」。即使项目已用 Hibernate @Filter / MyBatis 拦截器,仍建议 Service 层显式调用 findByTenantIdAndId——主键直查在二级缓存命中、getReferenceById 等路径上常常绕过全局过滤。两层一起用,可读性也更好(看 Service 代码就知道隔离了 tenantId)。
场景 : 即使有了全局过滤机制,开发者在 Service 里直接 repo.findById(id) 仍可能绕过过滤——主键查询常常被 JPA/Hibernate 当成"按 ID 直查",跳过 entity filter
问题根因
JPA 二级缓存 / findById 走持久化上下文,会跳过 @Filter
攻击者拿到任意 ID(订单号、用户 ID 可能从其他渠道枚举)就能跨租户读
单个漏点就足以泄露:N+1 优化时常常出现 for (X x : list) { ... repo.findById(x.foreignKey) ... },每次都漏 tenantId
错误示例
WxUser buyer = wxUserRepository.findById(buyerUserId).orElse(null );
List<User> users = userIds.stream()
.map(id -> userRepository.findById(id).orElse(null ))
.toList();
List<User> users = userRepository.findAllById(userIds);
正确做法 Repository 必须提供「带 tenantId 的主键查询」方法,Service 一律调用它:
public interface WxUserRepository extends JpaRepository <WxUser, Long> {
Optional<WxUser> findByTenantIdAndId (Long tenantId, Long id) ;
List<WxUser> findByTenantIdAndIdIn (Long tenantId, Collection<Long> ids) ;
}
WxUser buyer = wxUserRepository.findByTenantIdAndId(tenantId, buyerUserId).orElse(null );
Map<Long, WxUser> userMap = wxUserRepository
.findByTenantIdAndIdIn(tenantId, userIds)
.stream()
.collect(Collectors.toMap(WxUser::getId, u -> u));
嗅探信号(review/审计时按这些 grep)
grep -rn "Repository.findById(" src/main/java/**/service/
grep -rnE "(findById|getOne|getById|getReferenceById|findAllById)\(" src/main/java/
grep -L "findByTenantIdAndId" src/main/java/**/repository/*Repository.java
检查清单
陷阱 #2: 数据查询层缺少全局租户过滤 场景 : 部分查询绕过了租户过滤,导致跨租户数据泄露
问题根因 依赖开发者在每个查询中手动加 WHERE tenant_id = ?,容易遗漏。
错误示例
@Query("SELECT p FROM Product p WHERE p.categoryId = :categoryId")
List<Product> findByCategoryId (@Param("categoryId") Long categoryId) ;
正确做法
@Entity
@FilterDef(name = "tenantFilter", parameters = @ParamDef(name = "tenantId", type = Long.class))
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public class Product {
private Long tenantId;
}
public abstract class TenantAwareEntity {
@Column(name = "tenant_id", nullable = false)
private Long tenantId;
}
@Intercepts(@Signature(type = Executor.class, method = "query", ...))
public class TenantInterceptor implements Interceptor {
}
检查清单
陷阱 #3: 前端未处理租户不匹配的 403 场景 : 后端返回 403(租户不匹配),但前端没有正确处理,用户看到空白页或无提示
错误示例
request.interceptors .response .use (
response => response,
error => {
if (error.response ?.status === 401 ) {
clearAuth ();
redirectToLogin ();
}
return Promise .reject (error);
}
);
正确做法
request.interceptors .response .use (
response => response,
error => {
const status = error.response ?.status ;
const message = error.response ?.data ?.message || '' ;
if (status === 401 ) {
clearAuth ();
redirectToLogin ();
} else if (status === 403 && message.includes ('租户' )) {
clearAuth ();
redirectToLogin ();
showToast ('登录状态异常,请重新登录' );
}
return Promise .reject (error);
}
);
检查清单
陷阱 #4: 租户 ID 输入框允许手动输入 场景 : 管理后台的配置表单中,租户 ID 使用手动输入框,容易输错
错误示例
<InputNumber placeholder="请输入租户ID" />
正确做法
<Select
placeholder="请选择租户"
onChange={(value ) => {
form.setFieldsValue ({ tenantId : value });
const tenant = tenants.find (t => t.id === value);
form.setFieldsValue ({ tenantCode : tenant?.tenantCode });
}}
>
{tenants.map (t => (
<Option key ={t.id} value ={t.id} >
{t.tenantName} / {t.tenantCode}
</Option >
))}
</Select >
检查清单
陷阱 #5: 功能级授权 ≠ 数据级隔离 场景 : 仅对特定租户/角色/订阅级开放的功能,只在前端用 isYmhwTenant / hasPermission / isPaid 隐藏入口,后端 endpoint 没有独立的功能授权校验
问题根因 前端 UI 控制(按钮隐藏、菜单过滤)只是用户体验优化 ,不是安全边界。任意已登录用户只要知道 endpoint 路径,绕过 UI 直接调用接口,就能使用本不该有的能力。这与"数据层租户隔离"是两个不同维度:
数据层(陷阱 #1/#2) :访问的数据范围(你的数据 vs 他人的数据)
业务层(本陷阱) :可以使用的功能(你能用什么功能 vs 别人能用什么功能)
租户专属功能(如 YMHW 小程序发货模板导出,仅鱼米好物租户可用)
角色专属功能(仅管理员可批量删除/批量导出)
订阅/版本专属功能(付费版 AI 分析、企业版高级报表)
功能开关(feature flag 灰度发布)
错误示例
@PostMapping("/orders/miniapp-shipping-template/export")
public ResponseEntity<byte []> export(@RequestBody ExportRequest req) {
Long tenantId = TenantContext.getTenantId();
return service.export(tenantId, req.getOrderIds());
}
{isYmhwTenant && <Button onClick ={handleExport} > 导出小程序发货模板</Button > }
正确做法
@PostMapping("/orders/miniapp-shipping-template/export")
public ResponseEntity<byte []> export(@RequestBody ExportRequest req) {
Long tenantId = TenantContext.getTenantId();
TenantMiniAppConfig config = configRepository.findByTenantId(tenantId)
.orElseThrow(() -> new BusinessException (403 , "未启用小程序发货模板导出" ));
if (!"YMHW" .equalsIgnoreCase(config.getTenantCode())) {
throw new BusinessException (403 , "仅鱼米好物租户支持导出小程序发货模板" );
}
return service.export(tenantId, req.getOrderIds());
}
实现策略 授权依据 实现方式 适用场景 租户编码白名单 endpoint 入口 if 校验 / @RequireTenantCode 注解 + AOP 单个/少量租户专属 角色权限 @PreAuthorize("hasRole('ADMIN')") / Spring Security / CasbinRBAC 体系内 订阅状态 endpoint 入口校验当前订阅是否覆盖该功能 SaaS 分版本
优先级 :1-2 个 endpoint 用直接 if 校验(最简、可读性高);3 个以上同样限制再考虑 AOP/middleware 抽象,避免过度工程化。
检查清单
多语言示例 完整的 Java(Spring Boot 注解 AOP)/ Go(Gin middleware)/ TypeScript(Express middleware + NestJS Guard)实现示例见 references/multi-lang-examples.md。
检查清单(多租户隔离)
适用范围
Java: Spring Boot + JPA/Hibernate / MyBatis
Go: Gin + GORM / sqlx
Node.js: Express + Prisma / TypeORM
Python: FastAPI + SQLAlchemy
规则溯源 > 📋 本回复遵循:`multi-tenant-safety` - [章节名]