ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

GoFly双端架构实战:SAAS多租户数据分离与隔离验证

GoFly双端架构实战:SAAS多租户数据分离与隔离验证 简介GoFly快速开发后台管理系统框架是一套面向中后台系统开发者的前后端分离解决方案基于Go语言与Vue.js技术栈构建集成总管理系统admin端与业务管理系统business端并支持SAAS多账号数据分离适合需要快速搭建云服务或软件服务平台的技术团队尤其在医疗信息化场景中具备应用价值。压缩包共211个文件约3.27MB以141个Go源码文件为核心辅以14个TypeScript、8个Vue组件、3个SQL脚本及多个yml配置、html页面与zip依赖包另附说明文档与附赠资源便于安装配置与二次开发。目前已有181人学习关注。资源涵盖框架核心代码、数据库脚本、配置示例与生成代码工具读者可据此理解SAAS多租户数据隔离的实现思路掌握admin与business双端协作的目录结构并参考医疗行业相关模块进行系统扩展与排错实践。1. GoFly 中后台框架到底解决了什么问题从 admin 端到 business 端的双端拆分很多团队做中后台系统时第一版往往是一个 admin 端打天下用户管理、角色权限、业务数据、报表统计全塞在一个工程里。等到第二个客户、第三个客户进来要求数据隔离、要求独立配置、要求按套餐区分功能代码就开始失控了。GoFly 这个框架的核心思路就是把「总管理系统 admin 端」和「业务管理系统 business 端」拆成两个独立进程admin 端负责租户开通、套餐配置、全局字典和系统级监控business 端负责具体业务逻辑和租户内的数据操作两者通过共享的数据库和一套租户标识串联起来。它基于 Go 做后端、Vue 做前端前后端分离天然支持 SAAS 多账号数据分离。适合谁适合正在做医疗、餐饮、教育这类需要多租户隔离的中后台团队尤其是那些已经感受到「单 admin 端撑不住多客户」的开发者。这一章先把双端架构和 SAAS 数据分离的边界讲清楚后面再落到具体跑通步骤。2. 双端架构的选型理由与最小可跑通环境2.1 为什么 admin 端和 business 端必须拆成两个服务单服务做 SAAS 最直接的坑是权限模型会打架。admin 端的超级管理员需要跨租户查看所有数据business 端的租户管理员只能看自己租户内的数据。如果放在同一个进程里你会在每个接口里写if isSuperAdmin { ... } else { ... }时间一长漏掉一个判断就是越权漏洞。拆成两个服务后admin 端只连「平台库」business 端只连「租户库」或带租户过滤的同一套表权限边界在进程级别就隔开了。另一个理由是部署弹性。admin 端访问量低但权限高business 端访问量随租户数量线性增长。拆开后business 端可以水平扩展多个实例admin 端保持单实例或双实例热备即可。GoFly 的常见做法是 admin 端和 business 端共用一套 Go module 里的 model 和 service 基础层但各自有独立的 main 入口和路由注册文件。从技术栈看Go 侧一般用 Gin 或 GoFrame 做 HTTP 层Vue 侧用 Vue3 Vite Pinia Element Plus。数据库常见 MySQL 8缓存 Redis权限用 JWT Casbin 或框架自带的 RBAC。这些不是 GoFly 强制的但属于这个方向里最稳的组合。2.2 本地跑通 admin 端和 business 端的最小命令先确认 Go 和 Node 环境。Go 建议 1.20 以上Node 建议 18 LTS。下面是一套通用的初始化流程具体仓库结构以你拿到的代码为准但命令顺序和参数含义是通用的。# 1. 确认基础环境 go version # 期望 go1.20 node -v # 期望 v18.x npm -v # 期望 9.x # 2. 后端依赖拉取在项目根目录 go mod tidy # 拉取 go.mod 里声明的所有依赖 go mod download # 预下载到本地模块缓存 # 3. 前端依赖安装admin 端和 business 端各一次 cd web/admin npm install cd ../business npm install # 4. 数据库初始化先建库再导入 SQL mysql -u root -p -e CREATE DATABASE gofly_admin DEFAULT CHARSET utf8mb4; mysql -u root -p -e CREATE DATABASE gofly_business DEFAULT CHARSET utf8mb4; mysql -u root -p gofly_admin deploy/sql/admin.sql mysql -u root -p gofly_business deploy/sql/business.sql逻辑说明go mod tidy会补全缺失依赖并删除未使用的npm install在两个前端目录分别执行因为 admin 端和 business 端的依赖清单是独立的。数据库分两个库是 SAAS 数据分离的物理基础admin 库存平台级配置business 库存租户业务数据。参数说明DEFAULT CHARSET utf8mb4必须显式指定否则中文和 emoji 会出问题。SQL 文件路径按实际项目调整常见放在deploy/sql或docs/sql下。导入顺序不能反admin 库里的租户表要先于 business 库的业务表存在。2.3 配置文件里必须改的四个参数后端启动前配置文件常见为config.yaml或manifest/config/config.yaml里有四个参数不改一定翻车。参数含义常见错误值正确做法database.admin.dsnadmin 库连接串指向 business 库指向 gofly_admindatabase.business.dsnbusiness 库连接串与 admin 相同指向 gofly_businessjwt.secret双端共享的签名密钥两端不一致两端填同一个值tenant.mode租户隔离模式留空填column或databasejwt.secret两端必须一致因为 admin 端签发的 token 有时需要被 business 端校验比如单点登录场景。tenant.mode决定数据分离是「同库加 tenant_id 列」还是「一租户一库」医疗类项目因为合规要求常见用database模式普通项目用column模式性能更好。启动命令# 启动 admin 端默认 8080 go run cmd/admin/main.go # 另开终端启动 business 端默认 8081 go run cmd/business/main.go # 前端分别启动 cd web/admin npm run dev # 默认 5173 cd web/business npm run dev # 默认 5174两个后端端口不能冲突两个前端 dev server 端口也不能冲突。Vite 的代理配置里要把/api/admin转发到 8080/api/business转发到 8081。3. SAAS 多账号数据分离的落地方式与租户上下文传递3.1 两种数据分离模式的取舍SAAS 数据分离常见三种模式独立数据库、共享数据库独立 schema、共享表加 tenant_id 列。GoFly 这类框架通常支持前两种或第一种加第三种。医疗项目因为数据敏感一般选独立数据库普通中后台选共享表加 tenant_id 列成本低、运维简单。共享表模式的核心是每个业务表都有tenant_id字段所有查询必须带这个条件。ORM 层可以用钩子自动注入但钩子不是万能的手写 SQL 的地方必须自己加。独立数据库模式则是每个租户一个库连接池按租户切换好处是隔离彻底坏处是租户多了连接数爆炸需要连接池复用策略。选型建议租户数少于 50 且数据敏感用独立数据库租户数多且数据敏感度一般用共享表加 tenant_id。混合模式也有核心表独立库日志表共享但复杂度高不建议第一版就上。3.2 租户上下文从登录到查询的完整链路租户上下文传递是 SAAS 最容易出 bug 的地方。完整链路是用户登录时后端根据用户名查出所属 tenant_id签发 JWT 时把 tenant_id 写进 claims后续请求经过中间件解析 JWT把 tenant_id 塞进context.Contextservice 层从 context 取出 tenant_id拼进查询条件。// 中间件从 JWT 解析 tenant_id 并注入 context func TenantMiddleware() gin.HandlerFunc { return func(c *gin.Context) { token : c.GetHeader(Authorization) claims, err : ParseToken(token) if err ! nil { c.AbortWithStatusJSON(401, gin.H{msg: token invalid}) return } // 关键tenant_id 必须从 claims 取不能从请求参数取 tenantID : claims.TenantID if tenantID 0 { c.AbortWithStatusJSON(403, gin.H{msg: tenant missing}) return } ctx : context.WithValue(c.Request.Context(), tenant_id, tenantID) c.Request c.Request.WithContext(ctx) c.Next() } }逻辑说明tenant_id 只能从服务端签发的 token 里取绝不能信任前端传的 tenant_id 参数否则租户 A 可以伪造请求查租户 B 的数据。这是 SAAS 系统最基础的安全底线。参数说明claims.TenantID的类型要和数据库tenant_id字段一致常见是 int64。context.WithValue的 key 建议用自定义类型避免冲突这里用字符串是为了演示简洁。3.3 业务查询里 tenant_id 的注入与校验service 层拿到 tenant_id 后查询必须显式带上。以 GORM 为例func GetPatientList(ctx context.Context, page, size int) ([]Patient, int64, error) { tenantID, ok : ctx.Value(tenant_id).(int64) if !ok || tenantID 0 { return nil, 0, errors.New(tenant_id missing in context) } var list []Patient var total int64 db : global.DB.Model(Patient{}).Where(tenant_id ?, tenantID) db.Count(total) err : db.Offset((page - 1) * size).Limit(size).Find(list).Error return list, total, err }逻辑说明先校验 context 里的 tenant_id 存在且非零再拼查询条件。Count和Find用同一个db会话避免条件丢失。分页参数 page 从 1 开始offset 计算是(page-1)*size。参数说明tenant_id字段建议加索引联合索引(tenant_id, created_at)对列表查询最有效。如果用了软删除GORM 会自动加deleted_at IS NULL不用手写。提示任何绕过 service 层直接写 SQL 的地方都要人工检查 tenant_id 条件。这是 SAAS 项目 code review 的必查项。4. 避坑与排查双端部署和 SAAS 隔离里最容易翻车的五件事4.1 现象business 端能登录但查不到任何数据原因JWT 里没有 tenant_id或者中间件没生效。常见于 admin 端和 business 端用了不同的 jwt.secrettoken 解析失败后走了匿名分支。解决先确认两端jwt.secret完全一致再在中间件里打日志确认 claims 内容。如果 claims 里确实没有 tenant_id检查登录接口签发 token 时有没有把用户的 tenant_id 写进去。4.2 现象租户 A 能看到租户 B 的数据原因某个查询漏了 tenant_id 条件或者用了Unscoped()绕过了 GORM 的默认作用域。也可能是缓存 key 没带 tenant_id导致跨租户命中同一份缓存。解决全局搜索所有Find、First、Scan调用逐个确认 tenant_id 条件。缓存 key 统一加tenant:{id}:前缀。建议写一个单元测试用两个租户的数据交叉查询断言结果不重叠。4.3 现象admin 端改了套餐配置business 端不生效原因admin 端和 business 端各自缓存了套餐配置admin 端更新后没有通知 business 端刷新。或者两端读的不是同一张表。解决套餐配置这类低频变更数据建议 business 端每次请求都从库里读或者用 Redis 发布订阅做失效通知。不要用本地内存缓存多实例下必然不一致。4.4 现象独立数据库模式下新增租户后连接超时原因每个租户一个库连接池没有上限租户多了之后 MySQL 的 max_connections 被打满。解决给租户连接池设上限比如每个租户最多 5 个连接用 LRU 策略回收空闲租户的连接。或者改用共享表加 tenant_id 模式从根上避免连接数问题。4.5 现象前端切换租户后页面数据没变原因Vue 的 Pinia store 里缓存了上一个租户的数据切换租户后没有清空。或者 axios 的请求头里还带着旧 token。解决切换租户时强制刷新页面或者手动重置所有 store 并重新拉取用户信息。axios 拦截器里统一从 store 取 token不要从 localStorage 直接读避免多标签页不同步。5. 用一套最小验证脚本确认 SAAS 隔离真的生效最后一章给一个可复现的验证方法。不要靠肉眼看页面写一个脚本用两个租户的账号分别登录交叉请求对方的数据断言返回为空或 403。这个脚本跑通SAAS 隔离才算真正落地。#!/bin/bash # 用法./verify_tenant.sh BASEhttp://localhost:8081/api/business # 租户 A 登录 TOKEN_A$(curl -s -X POST $BASE/login \ -H Content-Type: application/json \ -d {username:userA,password:passA} | jq -r .data.token) # 租户 B 登录 TOKEN_B$(curl -s -X POST $BASE/login \ -H Content-Type: application/json \ -d {username:userB,password:passB} | jq -r .data.token) # 用 A 的 token 查列表记录第一条数据的 id ID_A$(curl -s $BASE/patient/list?page1size1 \ -H Authorization: $TOKEN_A | jq -r .data.list[0].id) # 用 B 的 token 去查 A 的那条数据期望 403 或空 RESULT$(curl -s $BASE/patient/detail?id$ID_A \ -H Authorization: $TOKEN_B | jq -r .code) if [ $RESULT 403 ] || [ $RESULT 404 ]; then echo PASS: tenant isolation works else echo FAIL: tenant B can access tenant A data, code$RESULT fi逻辑说明脚本先拿两个租户的 token再用 A 的 token 取一条数据 id最后用 B 的 token 去访问这条数据。如果隔离生效应该返回 403 或 404如果返回 200说明隔离有漏洞。参数说明jq需要提前安装。BASE地址按实际部署调整。code字段是后端统一响应格式里的状态码常见 200 成功、403 无权限、404 不存在。如果你的项目响应格式不同改 jq 的取值路径即可。这个脚本建议加到 CI 里每次改完权限相关代码都跑一遍。我自己踩过的坑是某次重构把中间件顺序调了tenant 中间件跑在鉴权中间件后面导致 tenant_id 一直是空的所有租户看到的是全量数据。那个 bug 上线两天才发现血泪经验就是——隔离验证不能靠人点页面必须自动化。希望帮到你。本文还有配套的精品资源点击获取
返回列表