智慧社区多租户系统Docker与Nginx实战配置
简介本资源是一个基于SpringCloud Alibaba与Nacos构建的智慧社区多租户系统开源项目面向微服务架构开发者、智慧城市领域技术实践者及Java中高级工程师解决多租户场景下的服务治理、数据隔离与配置弹性化等核心问题。压缩包共1387个文件含702个Java业务与微服务模块代码、527个XML配置与Maven依赖定义、53个YML微服务配置文件、8个SQL建表与初始化脚本以及Dockerfile、Nginx配置、Flyway迁移脚本等运维支撑文件整体2.39MB结构完整、开箱即用。已有337人学习下载涵盖从服务注册发现Nacos、熔断限流Sentinel、数据库租户隔离Flyway租户ID字段设计到前端界面定制的全链路实现特别适合深入理解多租户SaaS化落地细节与SpringCloud Alibaba企业级工程实践。1. 智慧社区多租户系统不是“一套系统改个名”而是用隔离复用解决物业、街道、运营方三方共管的真实难题你手头这个smart-city-tenant_智慧社区多租户系统.zip拆开后大概率会看到Dockerfile、nginx.conf、数据库迁移脚本和带tenant_id字段的业务表结构——它不是单体系统的简单包装而是为街道办统建平台、物业公司分区域运营、第三方服务商如停车/门禁/能耗按楼栋接入这三类角色设计的运行底座。核心矛盾在于数据必须物理或逻辑隔离避免A小区业主看到B小区报修记录但UI、权限模型、设备接入协议、通知模板又得高度复用否则100个小区就要维护100套前端。常见误判是直接套用SaaS多租户方案结果在设备状态同步延迟、跨租户告警聚合、区县监管报表穿透等场景崩盘。本文聚焦真实落地链路从Dockerfile如何声明租户感知的构建阶段到nginx.conf怎样通过请求头透传租户上下文再到数据库连接池如何绑定租户标识——不讲抽象概念只拆能进生产环境的配置细节。2. 用 Dockerfile 构建租户感知镜像分阶段编译 环境变量注入 租户配置挂载2.1 为什么不能直接 COPY 整个源码进镜像租户配置必须与镜像解耦很多团队把application-prod.yml直接 COPY 进镜像导致每次新增租户都要重新构建镜像、推送仓库、滚动更新——100个小区意味着每天上百次镜像发布。正确做法是将租户配置如数据库连接串、短信网关密钥、地图API Key完全剥离镜像通过docker run -v /opt/tenants/abc:/app/config/tenant方式挂载。Dockerfile的核心任务变成固化不可变部分代码、基础依赖、通用中间件配置暴露可变部分租户ID、地域策略、服务发现地址的注入入口。2.1.1 多阶段构建分离构建环境与运行时环境# 构建阶段仅用于编译不进入最终镜像 FROM maven:3.8.6-openjdk-17-slim AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests # 运行阶段极简基础镜像仅含JRE和必要工具 FROM openjdk:17-jre-slim LABEL maintaineropssmartcity.local # 创建非root用户提升安全性 RUN groupadd -g 1001 -r spring useradd -s /bin/bash -u 1001 -r spring USER spring:spring # 复制构建产物设置工作目录 WORKDIR /app COPY --frombuilder --chownspring:spring /app/target/smart-city-tenant.jar app.jar COPY --frombuilder --chownspring:spring /app/src/main/resources/static static/ # 声明租户配置挂载点关键 VOLUME [/app/config/tenant] # 暴露租户标识环境变量供应用启动时读取 ENV TENANT_ID ENV REGION_CODE # 启动命令强制要求传入TENANT_ID否则拒绝启动 ENTRYPOINT [sh, -c, if [ -z \$TENANT_ID\ ]; then echo ERROR: TENANT_ID must be set; exit 1; fi; java -Dtenant.id$TENANT_ID -Dregion.code$REGION_CODE -jar app.jar]提示VOLUME [/app/config/tenant]不是可选配置。它明确告诉 Docker Engine“此处必须挂载外部目录”避免因忘记-v参数导致应用读取默认配置而混租户。ENTRYPOINT中的校验逻辑比CMD更可靠——CMD可被docker run覆盖而ENTRYPOINT强制执行校验。2.2 构建与部署实操一次构建百租户复用假设你已解压smart-city-tenant_智慧社区多租户系统.zip得到项目根目录# 1. 进入项目目录确认Dockerfile存在 cd smart-city-tenant/ ls -l Dockerfile pom.xml src/ # 2. 构建基础镜像镜像名带版本号便于回滚 docker build -t smart-city-tenant:v2.3.1 . # 3. 为A小区租户ID: xiaoshan-001创建配置目录 mkdir -p /opt/tenants/xiaoshan-001 cp config-template/application.yml /opt/tenants/xiaoshan-001/application.yml # 4. 启动容器关键参数-e TENANT_ID、-v 挂载、--network 隔离 docker run -d \ --name tenant-xiaoshan-001 \ --network smartcity-bridge \ -e TENANT_IDxiaoshan-001 \ -e REGION_CODEHZ-XIAOSHAN \ -v /opt/tenants/xiaoshan-001:/app/config/tenant \ -p 8081:8080 \ smart-city-tenant:v2.3.12.2.1 关键参数说明表参数作用必填性典型值TENANT_ID应用内唯一租户标识用于路由数据源、过滤日志、生成租户专属URL必填xiaoshan-001,gongshu-002REGION_CODE地域编码影响地图服务坐标系、本地化时间格式、政策合规校验规则推荐填HZ-XIAOSHAN,SH-PUDONG/app/config/tenant挂载提供租户专属配置数据库连接、第三方密钥、UI主题色必填/opt/tenants/xiaoshan-001--network smartcity-bridge自定义网络使同地域租户容器可互通跨地域租户网络隔离生产推荐smartcity-bridge注意-p 8081:8080中的宿主机端口8081必须全局唯一。若租户数超百建议改用反向代理如 Nginx统一入口容器内部始终用8080。3. 用 nginx.conf 实现租户路由基于域名、路径、请求头的三级分流策略3.1 为什么需要 Nginx单容器无法解决租户流量调度与安全边界问题当xiaoshan-001和gongshu-002两个租户容器都监听8080端口时Nginx 是唯一能实现以下能力的组件域名分流xiaoshan.smartcity.gov.cn→xiaoshan-001容器路径分流smartcity.gov.cn/tenant/gongshu-002/→gongshu-002容器请求头透传将X-Tenant-ID: xiaoshan-001注入后端避免应用层重复解析租户级限流对xiaoshan-001单独设置 QPS 限制不影响其他租户nginx.conf不是静态文件而是租户策略的执行引擎。3.1.1 核心配置基于域名的租户路由推荐生产使用# /etc/nginx/conf.d/tenant-routing.conf upstream xiaoshan_backend { server 172.18.0.10:8080; # docker network中xiaoshan-001容器IP keepalive 32; } upstream gongshu_backend { server 172.18.0.11:8080; # docker network中gongshu-002容器IP keepalive 32; } # 租户域名路由规则 server { listen 80; server_name xiaoshan.smartcity.gov.cn; # 强制HTTPS生产必须 return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name xiaoshan.smartcity.gov.cn; ssl_certificate /etc/nginx/ssl/xiaoshan.crt; ssl_certificate_key /etc/nginx/ssl/xiaoshan.key; # 关键透传租户ID到后端 proxy_set_header X-Tenant-ID xiaoshan-001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; location / { proxy_pass http://xiaoshan_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; } } server { listen 443 ssl http2; server_name gongshu.smartcity.gov.cn; ssl_certificate /etc/nginx/ssl/gongshu.crt; ssl_certificate_key /etc/nginx/ssl/gongshu.key; proxy_set_header X-Tenant-ID gongshu-002; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; location / { proxy_pass http://gongshu_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; } }逻辑说明proxy_set_header X-Tenant-ID xiaoshan-001是租户隔离的起点。Spring Boot 应用可通过RequestHeader(X-Tenant-ID)直接获取无需解析域名或路径。此方式比路径前缀如/xiaoshan/更安全——路径可被恶意构造而域名由DNS权威控制。3.2 进阶动态租户路由应对租户数超500的场景当租户数达数百时手动维护server{}块不可持续。此时需启用 Nginx 的map模块做动态映射# /etc/nginx/conf.d/dynamic-tenant.conf # 从域名提取租户IDxiaoshan.smartcity.gov.cn → xiaoshan map $host $tenant_id { default ; ~^(?tenant[a-z0-9-])\.smartcity\.gov\.cn $tenant; } # 动态上游服务器需配合Consul或自定义resolver upstream dynamic_tenant_backend { server 127.0.0.1:8080 resolve; # 实际需对接服务发现 } server { listen 443 ssl http2; server_name *.smartcity.gov.cn; ssl_certificate /etc/nginx/ssl/wildcard.crt; ssl_certificate_key /etc/nginx/ssl/wildcard.key; # 关键动态注入租户ID proxy_set_header X-Tenant-ID $tenant_id; location / { # 验证租户ID有效性防止无效域名攻击 if ($tenant_id ) { return 404 Invalid tenant domain; } proxy_pass http://dynamic_tenant_backend; } }3.2.1 动态路由验证步骤测试域名解析是否生效# 检查xiaoshan.smartcity.gov.cn是否指向Nginx服务器IP nslookup xiaoshan.smartcity.gov.cn验证Nginx能否提取租户ID# 发送测试请求检查响应头是否含X-Tenant-ID curl -H Host: xiaoshan.smartcity.gov.cn http://localhost/health # 应返回X-Tenant-ID: xiaoshan检查租户容器是否收到该Header在xiaoshan-001容器内抓包# 进入容器 docker exec -it tenant-xiaoshan-001 sh # 抓取8080端口HTTP请求头 tcpdump -i any -A port 8080 | grep X-Tenant-ID提示map指令中的正则~^(?tenant[a-z0-9-])\.smartcity\.gov\.cn严格限定租户ID只能含小写字母、数字、短横线杜绝SQL注入或路径遍历风险。if ($tenant_id )是安全兜底避免空租户ID导致应用层异常。4. 数据库租户隔离读写分离动态数据源行级过滤的三层防护4.1 为什么共享数据库必须做行级隔离租户数据泄露常始于一个WHERE条件遗漏smart-city-tenant系统若采用共享数据库Shared Database, Shared Schema所有租户数据存于同一张device_status表仅靠tenant_id字段区分。此时任何漏写WHERE tenant_id ?的SQL都会导致跨租户数据泄露。例如❌ 错误查询管理员想查所有设备在线率SELECT COUNT(*) FROM device_status WHERE status ONLINE;✅ 正确做法强制绑定当前租户SELECT COUNT(*) FROM device_status WHERE status ONLINE AND tenant_id xiaoshan-001;因此数据库层必须有自动化防护机制。4.1.1 MyBatis Plus 多租户插件配置以Spring Boot为例Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 添加多租户插件 TenantLineInnerInterceptor tenantInterceptor new TenantLineInnerInterceptor(); tenantInterceptor.setTenantIdColumn(tenant_id); // 指定租户字段名 tenantInterceptor.setIgnoreTables(Arrays.asList(sys_user, sys_role)); // 系统表不加租户条件 interceptor.addInnerInterceptor(tenantInterceptor); return interceptor; } // 动态数据源配置支持不同租户连不同DB实例 Bean Primary public DataSource dataSource() { DynamicDataSource dynamicDataSource new DynamicDataSource(); MapObject, Object targetDataSources new HashMap(); // xiaoshan-001 租户连接专属数据库 DruidDataSource xiaoshanDs new DruidDataSource(); xiaoshanDs.setUrl(jdbc:mysql://10.0.1.100:3306/smartcity_xiaoshan?useSSLfalse); xiaoshanDs.setUsername(xiaoshan_app); xiaoshanDs.setPassword(xxx); targetDataSources.put(xiaoshan-001, xiaoshanDs); // gongshu-002 租户连接另一数据库 DruidDataSource gongshuDs new DruidDataSource(); gongshuDs.setUrl(jdbc:mysql://10.0.1.101:3306/smartcity_gongshu?useSSLfalse); gongshuDs.setUsername(gongshu_app); gongshuDs.setPassword(xxx); targetDataSources.put(gongshu-002, gongshuDs); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(xiaoshanDs); // 默认数据源 return dynamicDataSource; } }参数说明setTenantIdColumn(tenant_id)告诉插件所有SELECT/UPDATE/DELETE语句自动追加AND tenant_id ?条件setIgnoreTables指定系统管理表如用户、角色不加租户过滤因为这些表是全局共享的。4.2 数据库连接池租户绑定避免连接复用导致的租户上下文污染Druid 连接池默认复用连接若xiaoshan-001的请求使用了刚被gongshu-002用过的连接且连接中残留tenant_id上下文可能引发数据错乱。解决方案是连接池按租户分组# application.yml租户配置文件中 spring: datasource: druid: # 关键为每个租户配置独立连接池 xiaoshan: url: jdbc:mysql://10.0.1.100:3306/smartcity_xiaoshan username: xiaoshan_app password: xxx initial-size: 5 max-active: 20 gongshu: url: jdbc:mysql://10.0.1.101:3306/smartcity_gongshu username: gongshu_app password: xxx initial-size: 5 max-active: 204.2.1 动态数据源路由逻辑关键代码Component public class TenantRoutingDataSource extends AbstractRoutingDataSource { Override protected Object determineCurrentLookupKey() { // 从请求头或ThreadLocal获取当前租户ID String tenantId TenantContext.getTenantId(); // 通常由Filter设置 if (tenantId null) { throw new RuntimeException(Tenant ID not found in request context); } return tenantId; } }注意TenantContext.getTenantId()必须在Filter中初始化public class TenantFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { HttpServletRequest httpRequest (HttpServletRequest) request; String tenantId httpRequest.getHeader(X-Tenant-ID); // 从Nginx透传 TenantContext.setTenantId(tenantId); try { chain.doFilter(request, response); } finally { TenantContext.clear(); // 必须清理避免线程复用污染 } } }5. 租户系统上线前必做的3项验证配置注入、上下文传递、数据隔离5.1 验证 Dockerfile 环境变量注入是否生效容器启动后进入容器检查环境变量是否正确加载# 查看容器环境变量 docker exec tenant-xiaoshan-001 env | grep TENANT # 输出应为 # TENANT_IDxiaoshan-001 # REGION_CODEHZ-XIAOSHAN # 检查租户配置是否挂载成功 docker exec tenant-xiaoshan-001 ls -l /app/config/tenant/ # 应显示 application.yml 等配置文件失败排查若env | grep TENANT无输出检查docker run是否遗漏-e TENANT_ID...若/app/config/tenant/为空确认-v参数路径是否拼写错误或宿主机目录不存在。5.2 验证 Nginx 请求头透传与应用层接收在租户容器内启动一个临时HTTP服务捕获原始请求头# 进入容器用nc监听8080端口绕过Spring Boot docker exec -it tenant-xiaoshan-001 sh apk add netcat-openbsd # Alpine Linux安装nc nc -l -p 8080然后从宿主机发起请求curl -H Host: xiaoshan.smartcity.gov.cn http://localhost/nc将打印原始HTTP请求查找X-Tenant-ID: xiaoshan-001是否存在。若缺失检查nginx.conf中proxy_set_header是否拼写错误或未重载配置nginx -s reload。5.3 验证数据库行级隔离是否触发执行一条故意不带tenant_id条件的SQL观察是否被拦截-- 在MySQL客户端连接到xiaoshan租户数据库 SELECT * FROM device_status WHERE status ONLINE;若配置正确MyBatis Plus 插件会在SQL执行前自动改写为SELECT * FROM device_status WHERE status ONLINE AND tenant_id xiaoshan-001;验证方法开启MySQL慢查询日志执行上述SQL查看日志中记录的实际SQL是否含tenant_id条件。5.3.1 租户隔离验证表生产环境必查验证项检查方法预期结果失败后果租户配置挂载docker exec container ls -l /app/config/tenant/显示租户专属yml文件应用读取默认配置混用密钥Nginx租户头透传nc -l -p 8080捕获请求头包含X-Tenant-ID: xxx应用层无法识别租户路由失败数据库行级过滤执行SELECT * FROM device_status LIMIT 1日志中SQL含AND tenant_id xxx全租户数据可见严重安全事件跨租户网络隔离docker exec xiaoshan-001 ping gongshu-002Destination Host UnreachableA小区可直连B小区数据库绕过所有鉴权提示ping测试必须在自定义Docker网络如smartcity-bridge中进行。默认bridge网络允许容器互通必须显式创建隔离网络docker network create --driver bridge --subnet 172.18.0.0/16 smartcity-bridge docker run --network smartcity-bridge ... # 启动容器时指定本文还有配套的精品资源点击获取