admin与company模块SaaS改造说明.md 6.3 KB

admin 与 company 模块 SaaS 改造说明

本文档说明 fs-adminfs-company 两模块在已有部分改造基础上的补充与完善点,以及本次已完成的改动。


一、改造前已有能力

fs-admin

  • 依赖 fs-framework,登录走 SysLoginService.login(..., tenantCode),支持传 tenantCode 查租户、切库、LoginUser.setTenantId
  • 请求链使用 framework 的 JwtAuthenticationTokenFilter:按 tenantId 切数据源、设置 TenantConfigContextProjectConfig.loadTenantConfigsFromContext()
  • 租户管理入口:TenantInfoController(租户 CRUD)。

fs-company

  • 自有 CompanyLoginService:支持 tenantCode 登录、主库查租户、TenantDataSourceManager.switchTenant、登录成功后 loginUser.setTenantId
  • 自有 JwtAuthenticationTokenFilter:按 tenantId 切库、设置 TenantConfigContextProjectConfig.loadTenantConfigsFromContext()
  • 自有 TenantDataSourceManagerDynamicDataSource,数据源配置含 master + sop。

二、本次补充与完善

1. fs-common:SecurityUtils.getTenantId() 兼容 company

  • 问题:company 使用的 LoginUserframework.security.LoginUser,不是 common 的 LoginUser。Redis 的 TenantKeyRedisSerializer 依赖 SecurityUtils.getTenantId() 给 key 加租户前缀,若只判断 principal instanceof LoginUser(common),company 登录后 principal 不匹配,导致 Redis key 无租户前缀。
  • 改动:在 fs-commonSecurityUtils.getTenantId() 中,除 LoginUser 外,对任意 principal 用反射调用 getTenantId(),若返回 Long 则使用,从而兼容 framework 的 LoginUser。
  • 文件fs-common/.../SecurityUtils.java

2. fs-admin:租户管理接口强制走主库

  • 问题:租户表 tenant_info 只在主库;若管理员以某租户身份登录,当前数据源为租户库,访问「租户管理」接口会查租户库,可能无该表或数据错误。
  • 改动:在 TenantInfoController 上增加 @DataSource(DataSourceType.MASTER),该类所有接口强制走主库。
  • 文件fs-admin/.../tenant/TenantInfoController.java

3. fs-company:数据源映射统一为字符串 key

  • 问题DataSourceConfigtargetDataSources 使用 DataSourceType.MASTER(枚举)为 key,而 DynamicDataSourceContextHolder 使用 MASTER.name()(字符串)。为与 framework 及后续维护一致,建议统一为字符串 key。
  • 改动:将 targetDataSources.put(DataSourceType.MASTER, ...) 改为 put(DataSourceType.MASTER.name(), ...)
  • 文件fs-company/.../config/DataSourceConfig.java

4. fs-company:登录后 Redis Token 按租户隔离

  • 问题:Token 缓存的 Redis key 为 company_login_tokens:uuid,多租户下不同租户相同 uuid 会互相覆盖;且 TenantKeyRedisSerializerlogin_tokens 有特殊处理,而 company 使用 company_login_tokens,存/取时若未带租户信息,无法按租户隔离。
  • 改动
    1. TokenService
      • 生成 token 时若 loginUser.getTenantId() != null,将 tenantId 写入 JWT claims
      • getTokenKey 改为 getTokenKey(Long tenantId, String uuid):tenantId 非空时返回 tenantid:{tenantId}:company_login_tokens:{uuid}(与 TenantKeyRedisSerializer 约定一致,以 tenantid: 开头不再加前缀)。
      • refreshToken 使用 getTokenKey(loginUser.getTenantId(), loginUser.getToken()) 写入 Redis。
      • getLoginUser 从 JWT 解析出 uuid 与 tenantId,用 getTokenKey(tenantId, uuid) 取 Redis。
      • delLoginUser 先解析 token 得到 tenantId 与 uuid,再按 getTokenKey(tenantId, uuid) 删除。
    2. CompanyLoginService:在调用 tokenService.createToken(loginUser) 前,将当前 LoginUser 设置到 SecurityContext,便于其他依赖 getTenantId() 的逻辑(如 Redis 序列化)在需要时能拿到租户。
  • 文件fs-company/.../service/TokenService.javaCompanyLoginService.java

三、改造后行为小结

模块 能力 说明
fs-admin 登录带 tenantCode 与原有一致,framework 的 SysLoginService 处理。
fs-admin 请求按 tenantId 切库 + 租户配置 与原有一致,framework 的 JwtAuthenticationTokenFilter 处理。
fs-admin 租户管理接口 强制主库,避免租户身份下误查租户库。
fs-company 登录带 tenantCode 与原有一致,CompanyLoginService 处理。
fs-company 请求按 tenantId 切库 + 租户配置 与原有一致,company 的 JwtAuthenticationTokenFilter 处理。
fs-company Redis Token 按租户隔离 Token 存/取/删均带 tenantId,多租户下互不覆盖。
fs-common getTenantId() 兼容 admin(common LoginUser)与 company(framework LoginUser)。

四、建议自测点

  1. admin

    • 带 tenantCode 登录 → 后续请求是否走对应租户库、租户配置是否生效。
    • 以租户身份登录后访问「租户管理」列表/编辑 → 是否始终查主库、数据是否正确。
  2. company

    • 不同 tenantCode 登录 → 两户的 token 是否互不影响(Redis key 不同、互不踢出)。
    • 带 tenantCode 登录后请求业务接口 → 是否走对应租户库、租户配置是否生效。
    • 登出 → 对应 Redis key 是否按租户正确删除。
  3. Redis

    • 多租户下 Redis 缓存(含 token、业务 key)是否均带租户前缀或按租户隔离,无串户。

五、涉及文件清单

文件 改动说明
fs-common/.../SecurityUtils.java getTenantId() 增加反射兼容 framework LoginUser
fs-admin/.../tenant/TenantInfoController.java 类上增加 @DataSource(DataSourceType.MASTER)
fs-company/.../config/DataSourceConfig.java MASTER 使用 .name() 作为 map key
fs-company/.../service/TokenService.java JWT 带 tenantId、getTokenKey(tenantId,uuid)、存/取/删按租户 key
fs-company/.../service/CompanyLoginService.java 生成 token 前设置 SecurityContext

与整体 SaaS 方案的关系见 SaaS改造方案.md