Series Article

Day08 · 权限安全 + SaaS 运营核心业务面试准备

核心业务流程

用户登录认证完整流程

业务背景

登录认证解决的是“你是谁”。供应链系统里有采购价格、库存成本、平台订单、财务账单、银行账号等敏感信息,用户进入系统前必须先完成身份认证。

登录不是简单查用户名密码。真实系统还要处理租户编码、账号状态、密码加密、登录失败锁定、Token 签发、权限缓存、前端菜单渲染、RefreshToken 续签等问题。

完整流程

sequenceDiagram
    participant FE as 前端
    participant BE as 认证服务
    participant DB as 数据库
    participant Redis as Redis

    FE->>BE: 提交 tenantCode、username、password
    BE->>DB: 查询租户和用户
    DB-->>BE: 返回用户记录
    BE->>BE: 检查账号状态
    BE->>BE: BCrypt 校验密码
    alt 密码错误
        BE->>DB: 登录失败次数加一
        BE-->>FE: 返回登录失败
    else 密码正确
        BE->>DB: 更新登录时间、IP、失败次数清零
        BE->>DB: 查询角色和权限
        BE->>Redis: 缓存用户权限列表
        BE->>BE: 签发 AccessToken 和 RefreshToken
        BE-->>FE: 返回 Token、用户信息、权限列表
    end

关键步骤

登录时必须带 tenantCode。同一个用户名可能在不同租户下存在,例如多个公司都有 admin 账号,所以查询用户时不能只按用户名查,必须按租户和用户名一起查。

密码不能明文存储,也不建议使用普通 MD5。BCrypt 每次加密都会生成不同结果,但可以通过 checkpw 验证输入密码是否正确。它自动加盐并且计算成本较高,可以降低暴力破解风险。

登录失败次数要记录。比如连续失败 5 次后锁定账号 30 分钟,防止攻击者反复猜密码。返回错误时不要暴露“用户不存在”或“密码错误”的细节,避免攻击者枚举账号。

登录成功后,系统签发 AccessToken 和 RefreshToken。AccessToken 用于访问业务接口,生命周期短;RefreshToken 用于无感续签,生命周期长,适合放在 HttpOnly Cookie 中。

权限列表会缓存到 Redis,前端根据权限渲染菜单和按钮,后端根据权限注解做真实拦截。

面试时怎么讲

“登录认证这块,我不会只说查用户名密码。我会从多租户登录、密码安全、Token 和权限缓存四个点讲。

首先,我们是 SaaS 系统,同一个用户名可能在不同租户里都存在,所以登录时除了 username 和 password,还要传 tenantCode。后端先根据 tenantCode 找到租户,再在这个租户下查询用户。

第二,密码存储用 BCrypt,不存明文,也不用普通 MD5。BCrypt 是单向哈希,不能解密,只能校验。它每次加密结果不同,内部有盐值和计算成本,可以防彩虹表和暴力破解。

第三,登录失败要有保护。密码输错后会增加失败次数,连续失败 5 次就锁定账号一段时间。错误提示不会直接告诉用户是账号不存在还是密码错误,避免被恶意枚举账号。

第四,登录成功后用 Sa-Token 生成 Token。我们设计成 AccessToken + RefreshToken,AccessToken 生命周期短,用来访问接口;RefreshToken 生命周期长,用来刷新 AccessToken。前端拿到 Token 后,后续请求带 Authorization 头。

第五,登录成功时会查询用户角色和权限,把权限列表缓存到 Redis。前端用这个权限列表渲染菜单和按钮,后端接口通过权限注解做校验。前端隐藏按钮只是体验优化,真正的安全一定在后端。“


权限验证与数据隔离完整流程

业务背景

认证解决“你是谁”,授权解决“你能做什么”,数据权限解决“你能看哪些数据”。

比如采购专员可以查看供应商,但不能审核供应商;运营专员可以查看订单,但只能看自己负责店铺的订单;供应商 Portal 用户只能看自己的采购单和对账信息。

完整流程

flowchart TD
    A[请求到达网关或服务] --> B[Sa-Token 解析 Token]
    B --> C{Token 是否有效}
    C -->|否| D[返回 401]
    C -->|是| E[提取 userId 和 tenantId]
    E --> F[写入 TenantContext 和 UserContext]
    F --> G[读取接口权限注解]
    G --> H[从 Redis 查询用户权限]
    H --> I{是否有功能权限}
    I -->|否| J[返回 403]
    I -->|是| K[检查租户套餐功能]
    K --> L{套餐是否支持}
    L -->|否| M[返回套餐升级提示]
    L -->|是| N[追加 tenant_id 和数据权限条件]
    N --> O[执行业务查询或写操作]

权限分层

权限系统至少分四层:

  • 登录认证:用户是否登录,Token 是否有效。
  • 功能权限:用户能不能访问某个菜单、按钮或接口。
  • 数据权限:用户能看哪些店铺、仓库、供应商、部门或本人数据。
  • 套餐权限:租户购买的套餐是否包含这个功能。

401 和 403 要区分清楚。401 表示没登录或 Token 失效,需要重新登录;403 表示已经登录,但没有权限或套餐不支持。

数据隔离

租户隔离通过 tenant_id 实现,MyBatis-Plus 多租户插件自动给业务 SQL 追加 tenant_id = 当前租户。更细粒度的数据权限通过 @DataScope、AOP 和查询条件追加实现。

供应商 Portal 是特殊场景。供应商用户必须强制追加 supplier_id = 当前供应商ID,不能只依赖通用数据权限配置。

面试时怎么讲

“权限验证我会分层讲。

第一层是登录认证,请求进来以后,Sa-Token 先解析 Token。Token 无效就返回 401。

第二层是功能权限。Controller 方法上会用 @SaCheckPermission 声明需要的权限,比如 srm:supplier:audit。后端从 Redis 拿用户权限列表,如果没有这个权限,就返回 403。

第三层是套餐权限。我们是 SaaS 产品,不同租户买的套餐不一样。即使某个用户有高级 BI 的权限,如果租户套餐没开通,也不能访问。所以会查 sys_plan_feature 判断当前套餐是否支持这个功能。

第四层是数据权限。所有业务表都有 tenant_id,MyBatis-Plus 多租户插件会自动追加租户条件,保证 A 公司看不到 B 公司数据。对于店铺、仓库、本人数据这种更细的范围,我们用 @DataScope 注解和 AOP 构建过滤条件。

这里我会强调一点,前端权限不可信。前端可以隐藏菜单和按钮,但用户仍然可以用 Postman 调接口。所以后端权限、套餐、租户、数据范围都必须校验。“


SaaS 租户注册与套餐生命周期流程

业务背景

SaaS 系统和普通后台系统最大的区别是它要服务很多企业客户。每个企业客户就是一个租户,每个租户有自己的员工、角色、套餐、数据、配置和使用量。

所以租户运营不只是注册账号,还包括试用、开通、套餐限制、续费提醒、到期只读、数据保留和注销。

完整流程

flowchart TD
    A[企业注册] --> B[邮箱或短信验证]
    B --> C[创建租户记录]
    C --> D[创建租户管理员账号]
    D --> E[初始化角色菜单字典配置]
    E --> F[选择套餐并进入试用期]
    F --> G{是否续费}
    G -->|是| H[进入正式使用期]
    G -->|否| I[试用结束转只读]
    H --> J[套餐到期前提醒]
    J --> K{是否续费}
    K -->|是| H
    K -->|否| L[到期后写操作拦截]
    L --> M[数据保留期]
    M --> N{是否恢复}
    N -->|是| H
    N -->|否| O[归档或清理]

初始化数据

新租户创建后,要初始化一套基础数据:

  • 租户管理员账号
  • 预置角色
  • 菜单权限
  • 系统字典
  • 编号规则
  • 默认仓库或引导配置
  • KPI 阈值
  • 套餐功能和用量记录

这些初始化操作要放在事务里,避免出现租户创建成功但管理员账号创建失败的半成品数据。

套餐限制

套餐限制和权限限制不一样。

权限限制是用户有没有资格做某个动作。套餐限制是这个租户购买的版本是否允许使用某功能或使用到多少数量。

比如基础版最多 20 个供应商、1 个仓库、每月 500 单;专业版支持更多供应商、更多平台和供应商 Portal;企业版支持开放 API 和更多定制能力。

面试时怎么讲

“SaaS 租户运营这块,我会从生命周期讲。

企业客户注册后,系统先做邮箱或短信验证,然后创建租户记录,再创建租户管理员账号。创建完不是就结束了,还要初始化角色、菜单、字典、编号规则、默认配置和套餐功能,否则用户登录后会看到一个空系统。

租户创建和初始化要放在同一个事务里。如果租户表插入成功,但管理员账号或角色初始化失败,这个租户就无法正常登录使用。

套餐管理是 SaaS 商业化的核心。权限控制的是用户能不能做某个操作,套餐控制的是租户是否购买了这个功能。比如用户有新增供应商权限,但基础版最多 20 家供应商,如果已经达到 20 家,接口就要被套餐限制拦截。

套餐到期后,我不会直接删除数据,而是先转只读。只读状态允许登录和查看历史数据,但所有新增、修改、删除操作都拦截。这样既保护平台权益,也避免客户因为忘记续费导致数据突然不可用。超过保留期后,再进入归档或清理流程。“


生产部署与上线完整流程

业务背景

一个项目能在本地跑起来,不代表它能上线。上线要解决环境一致、配置安全、镜像构建、服务编排、HTTPS、健康检查、日志、监控、回滚等问题。

Day08 的部署部分可以讲成一条完整链路:代码提交后通过 CI/CD 构建镜像,服务器用 Docker Compose 编排服务,Nginx 作为入口提供 HTTPS,Prometheus 和 Grafana 做监控。

完整流程

flowchart TD
    A[开发提交代码] --> B[GitHub Actions 触发]
    B --> C[代码检查和单元测试]
    C --> D{测试是否通过}
    D -->|否| E[流水线失败]
    D -->|是| F[构建 Docker 镜像]
    F --> G[推送镜像仓库]
    G --> H[部署测试环境]
    H --> I[冒烟测试]
    I --> J{是否发布生产}
    J -->|否| K[等待下一次发布]
    J -->|是| L[负责人审批]
    L --> M[生产服务器拉取镜像]
    M --> N[Docker Compose 更新服务]
    N --> O[健康检查和监控]

部署关键点

Dockerfile 负责把应用打成镜像。后端使用多阶段构建,先用 Maven 打包,再用 JRE 镜像运行,减少镜像体积。

docker-compose 负责启动 MySQL、Redis、RocketMQ、Nacos、网关、业务服务、前端和 Nginx。容器之间通过内部网络和服务名通信。

.env 保存数据库密码、Redis 密码、OSS 密钥等环境变量,不能提交到 Git。

Nginx 负责 HTTPS、静态资源、API 反向代理、安全响应头和访问日志。

面试时怎么讲

“部署上线这块,我会讲完整链路。

首先,代码提交后触发 GitHub Actions。流水线会先跑代码检查和测试,测试通过后再构建 Docker 镜像,并推送到镜像仓库。镜像用 commit sha 打标签,这样线上版本能追溯到具体代码。

服务器上用 docker-compose 编排服务,包括 MySQL、Redis、RocketMQ、Nacos、网关、业务服务、前端和 Nginx。服务之间通过 Docker 内部网络通信,比如后端访问 Redis 就用服务名 redis

配置通过 .env 注入,数据库密码、Redis 密码、OSS 密钥都不写死在代码里,也不提交 Git。

Nginx 是统一入口,负责 HTTPS、前端静态资源、API 反向代理和安全响应头。后端服务不直接暴露到公网,外部请求先到 Nginx,再转到网关。

部署完成后必须做健康检查,比如访问 /actuator/health,确认数据库、Redis 等依赖可用。上线后还要接入 Prometheus、Grafana 和告警,监控接口错误率、P99 延迟、JVM、数据库、Redis 和业务失败率。“


技术亮点

Sa-Token 登录认证与双 Token 机制

为什么这是亮点

登录认证是所有后台系统的入口。面试官不只关心你会不会调 login 方法,还会追问密码怎么存、Token 怎么续期、Token 泄露怎么办、用户禁用后 Token 怎么失效。

核心实现

@Service
public class AuthService {

    public LoginVO login(LoginCommand command) {
        Tenant tenant = tenantService.getByCode(command.getTenantCode());
        if (tenant == null || !tenant.isNormal()) {
            throw new BusinessException("用户名或密码错误");
        }

        SysUser user = userMapper.selectByTenantAndUsername(
            tenant.getId(), command.getUsername()
        );
        if (user == null || !user.isEnabled()) {
            throw new BusinessException("用户名或密码错误");
        }

        if (!BCrypt.checkpw(command.getPassword(), user.getPassword())) {
            loginFailService.recordFail(user.getId());
            throw new BusinessException("用户名或密码错误");
        }

        StpUtil.login(user.getId());
        String accessToken = StpUtil.getTokenValue();
        String refreshToken = refreshTokenService.create(user.getId());

        List<String> permissions = permissionService.listPermissionCodes(user.getId());
        permissionCacheService.cachePermissions(user.getId(), permissions);

        loginLogService.recordSuccess(user.getId());
        return LoginVO.of(accessToken, refreshToken, user, permissions);
    }
}

面试时怎么讲

“认证这个点,我会强调安全细节。

我们不是简单查用户名密码,而是先根据 tenantCode 定位租户,再在租户下查用户。密码用 BCrypt 校验,不存明文,也不用 MD5。登录失败会记录次数,多次失败锁定账号。

登录成功后用 Sa-Token 生成 AccessToken,同时生成 RefreshToken。AccessToken 生命周期短,用于访问接口;RefreshToken 生命周期长,用于刷新 AccessToken。用户退出、修改密码、账号禁用时,要让 RefreshToken 和登录态失效。

登录成功后还会查询权限列表并缓存到 Redis。前端用权限列表渲染菜单,后端接口用权限注解做真正的安全拦截。“


RBAC 权限模型与权限缓存

业务问题

系统里有采购、仓库、运营、物流、财务、供应商、租户管理员等角色。如果直接给用户分配权限,用户多了会非常混乱。

RBAC 用“用户 - 角色 - 权限”三层关系解决这个问题。

权限缓存

public List<String> getUserPermissions(Long userId) {
    String cacheKey = "auth:perm:" + userId;
    List<String> cached = redisTemplate.opsForList().range(cacheKey, 0, -1);
    if (cached != null && !cached.isEmpty()) {
        return cached;
    }

    List<String> permissions = menuMapper.selectPermissionCodesByUserId(userId);
    if (!permissions.isEmpty()) {
        redisTemplate.opsForList().rightPushAll(cacheKey, permissions);
        redisTemplate.expire(cacheKey, Duration.ofHours(2));
    }
    return permissions;
}

缓存失效

角色权限变更、用户角色变更、用户禁用、租户套餐变更时,都要删除相关权限缓存。否则管理员刚取消权限,用户还可能继续访问。

面试时怎么讲

“RBAC 我会讲用户、角色、权限三层。

用户不直接绑定大量权限,而是分配角色。角色再绑定菜单和按钮权限。比如采购专员角色有供应商列表、新增供应商、采购单列表权限,但没有供应商审核和财务账单权限。

权限标识用三段式命名:模块、资源、动作,例如 srm:supplier:audit。这种命名方式很适合微服务项目,因为一看就知道权限属于哪个模块、哪个资源、哪个操作。

为了避免每个请求都查数据库,用户登录后或首次访问时会把权限列表缓存到 Redis。接口上用 @SaCheckPermission 声明所需权限,后端从缓存里查用户权限。

这里要注意缓存失效。管理员调整角色权限后,必须删除相关用户的权限缓存,否则权限修改不会立即生效。“


多租户隔离与数据权限设计

业务问题

多租户隔离解决 A 公司不能看到 B 公司数据的问题。数据权限解决同一个公司内部,不同员工只能看自己负责范围的数据。

这两个不是一回事。租户隔离是底线,数据权限是租户内部的精细化控制。

租户插件

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor();

    tenantInterceptor.setTenantLineHandler(new TenantLineHandler() {
        @Override
        public Expression getTenantId() {
            Long tenantId = TenantContext.getTenantId();
            return new LongValue(tenantId);
        }

        @Override
        public boolean ignoreTable(String tableName) {
            return Set.of("sys_tenant", "sys_plan_feature", "finance_exchange_rate")
                .contains(tableName);
        }
    });

    interceptor.addInnerInterceptor(tenantInterceptor);
    return interceptor;
}

数据权限注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {
    DataScopeType type() default DataScopeType.ALL;
    String field() default "";
}

面试时怎么讲

“多租户和数据权限我会分开讲。

多租户隔离是 SaaS 的底线。所有业务表都有 tenant_id,用户登录后从 Token 里取出 tenantId 放到 ThreadLocal,MyBatis-Plus 多租户插件会自动给 SQL 加 tenant_id = 当前租户。这样 A 租户查订单、供应商、库存时,不可能查到 B 租户的数据。

数据权限是在租户内部继续过滤。比如同一个租户里,美国站运营只能看美国店铺订单,广州仓管理员只能看广州仓库存。我们用 @DataScope 注解标记需要数据权限的接口,再通过 AOP 计算当前用户允许访问的店铺、仓库或本人数据范围,最后追加到查询条件里。

供应商 Portal 要特别处理。供应商不是租户员工,它只能看自己的采购单和对账信息,所以除了 tenant_id,还必须强制追加 supplier_id。这个不能完全依赖前端参数,必须后端硬性限制。“


SaaS 套餐限制与租户生命周期管理

业务问题

SaaS 平台要支持基础版、专业版、企业版,不同套餐能用的功能和数量不同。如果没有套餐限制,商业化就无法落地。

套餐拦截

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PlanLimit {
    String feature();
}
@Aspect
@Component
public class PlanLimitAspect {

    @Around("@annotation(planLimit)")
    public Object checkLimit(ProceedingJoinPoint point, PlanLimit planLimit) throws Throwable {
        Long tenantId = TenantContext.getTenantId();
        Tenant tenant = tenantService.getById(tenantId);

        PlanFeature feature = planFeatureService.getFeature(
            tenant.getPlanType(), planLimit.feature()
        );
        if (feature == null || !feature.isEnabled()) {
            throw new BusinessException("当前套餐不支持该功能");
        }

        if (feature.hasLimit()) {
            int used = usageService.getUsedCount(tenantId, planLimit.feature());
            if (used >= feature.getLimitValue()) {
                throw new BusinessException("当前套餐用量已达上限,请升级套餐");
            }
        }

        return point.proceed();
    }
}

面试时怎么讲

“SaaS 套餐管理我会讲两个点:功能开关和数量限制。

比如基础版只能有 20 家供应商、1 个仓库、每月 500 单;专业版支持更多供应商和平台;企业版支持开放 API 和更高用量。

实现上,我用 sys_plan_feature 保存每个套餐的功能开关和限制值。接口上加 @PlanLimit(feature = "supplier.max"),AOP 在业务执行前检查当前租户套餐是否支持这个功能,再检查当前用量是否超过限制。

租户生命周期方面,新租户注册后要创建租户、管理员账号、默认角色、菜单权限、字典和配置。试用到期或套餐到期后,不会立刻删除数据,而是转成只读状态,所有写操作被拦截。保留期内续费可以恢复,超过保留期再归档或清理。“


接口安全防护与审计日志

安全防护层次

接口安全不能只靠登录。常见防护包括:

  • SQL 注入防护
  • XSS 防护
  • 接口限流
  • 幂等防重复提交
  • 敏感数据脱敏
  • 操作审计日志

Redis 滑动窗口限流

@Around("@annotation(rateLimiter)")
public Object limit(ProceedingJoinPoint point, RateLimiter rateLimiter) throws Throwable {
    String key = "rate:" + UserContext.getUserId() + ":" + requestPath();
    long now = System.currentTimeMillis();
    long windowStart = now - rateLimiter.windowSeconds() * 1000L;

    Long allowed = redisTemplate.execute(RATE_LIMIT_LUA, List.of(key),
        String.valueOf(windowStart),
        String.valueOf(now),
        String.valueOf(rateLimiter.count()));

    if (allowed == null || allowed == 0) {
        throw new BusinessException("请求过于频繁,请稍后再试");
    }
    return point.proceed();
}

审计日志

审计日志重点记录高风险写操作,比如供应商审核、库存调整、付款审批、套餐变更、角色授权、用户禁用。

面试时怎么讲

“接口安全我会按分层防护讲。

第一层是 SQL 注入防护。MyBatis 里必须用 #{} 参数绑定,不能用字符串拼接。动态排序这种特殊场景也要做字段白名单。

第二层是 XSS 防护。商品名称、供应商备注、公告内容都可能被写入脚本。后端要过滤危险标签,前端展示文本时用 v-text,不要随便用 v-html

第三层是限流。登录、短信、导出、上传这些接口都要单独限流。我用 Redis 滑动窗口,比固定窗口更平滑,不容易出现边界突刺。

第四层是幂等。创建订单、付款申请、库存调整这类接口要防重复提交,可以用 requestId + Redis SETNX 做幂等。

第五层是审计日志。关键写操作要记录操作人、租户、IP、请求参数、执行结果和失败原因。审计日志最好异步写,避免影响主业务,但敏感字段必须脱敏。“


Docker 容器化与 CI/CD 自动化部署

为什么这是亮点

很多后台项目只停留在本地运行,缺少上线部署、配置管理和回滚方案。如果能讲清楚 Docker、Nginx、CI/CD、健康检查和监控,就能体现项目是真正考虑上线的。

Dockerfile 设计重点

  • 多阶段构建,减少最终镜像体积
  • 普通用户运行,避免 root 权限
  • JVM 参数通过环境变量配置
  • 日志目录挂载到宿主机或日志系统

CI/CD 流程

flowchart TD
    A[提交代码] --> B[运行测试]
    B --> C{测试通过}
    C -->|否| D[阻止合并]
    C -->|是| E[构建镜像]
    E --> F[推送镜像仓库]
    F --> G[部署测试环境]
    G --> H[冒烟测试]
    H --> I[生产审批]
    I --> J[生产部署]
    J --> K[健康检查]

面试时怎么讲

“部署这块我会从环境一致和交付自动化讲。

我们用 Docker 把应用和运行环境打成镜像,避免开发、测试、生产环境不一致。后端 Dockerfile 用多阶段构建,第一阶段 Maven 打包,第二阶段只保留 JRE 和 jar 包,镜像更小。

docker-compose 用来编排 MySQL、Redis、RocketMQ、Nacos、网关、业务服务、前端和 Nginx。容器之间走内部网络,通过服务名访问。MySQL、Redis 这类有状态服务要挂载 volume,避免容器删除后数据丢失。

CI/CD 用 GitHub Actions。代码提交后自动测试,测试通过后构建镜像并推送镜像仓库。部署生产前需要负责人审批,部署后执行健康检查。敏感配置全部放 GitHub Secrets 或 .env,不写死在代码里。

Nginx 作为统一入口,负责 HTTPS、前端静态资源、API 反向代理和安全响应头。后端服务不直接暴露公网。“


监控告警与生产可运维性

监控层次

上线后必须知道系统是否健康,出了问题能不能定位。

监控分四类:

  • 应用监控:接口延迟、错误率、JVM、线程池
  • 基础设施监控:CPU、内存、磁盘、网络
  • 中间件监控:MySQL、Redis、RocketMQ、Nacos
  • 业务监控:订单失败率、库存同步失败、账单解析失败、物流轨迹失败

面试时怎么讲

“监控这块我会强调技术指标和业务指标都要有。

技术指标包括 JVM 内存、GC、接口 P99、错误率、数据库连接池、Redis 内存、磁盘使用率。业务指标包括订单处理失败率、库存同步失败、账单解析失败、物流轨迹拉取失败。

技术指标正常不代表业务正常。比如 CPU 不高,但订单同步一直失败,业务仍然是不可用的。

我们可以用 Spring Boot Actuator 暴露健康检查和指标,Prometheus 采集,Grafana 展示,AlertManager 做告警。生产环境的 Actuator 端点不能随便暴露,要加 IP 白名单或独立管理端口。

告警要分级,不能所有问题都一股脑通知。普通预警发给开发,严重告警发给负责人。告警内容要包含服务名、实例、指标值、阈值、时间和排查链接。“


简历怎么写

项目描述

项目:跨境电商 SaaS 供应链管理平台 - 权限安全与 SaaS 运营部署模块
时间:2024.06 - 2024.12
角色:核心开发
技术:Spring Boot 3.2、Sa-Token、MyBatis-Plus、MySQL 8.0、Redis、RocketMQ、Docker、GitHub Actions、Nginx、Prometheus、Grafana

这是一个面向跨境电商卖家的 SaaS 供应链平台,我负责权限安全、租户运营和部署上线相关模块。
系统支持 RBAC 权限控制、数据权限、多租户隔离、套餐限制、租户生命周期、接口限流、
幂等防重放、敏感数据脱敏、操作审计、Docker 容器化部署、CI/CD 自动化流水线和监控告警。

日均处理接口请求 100 万+,支持 500+ 租户,覆盖采购、仓储、订单、物流、财务等多个业务模块的权限和安全控制。

核心亮点

亮点1:Sa-Token 登录认证 + 双 Token 机制

问题:SaaS 后台涉及大量敏感数据,需要安全可靠的登录认证和 Token 续期机制。

方案:使用 Sa-Token + Redis 管理分布式登录态,配合 BCrypt、账号锁定、随机 AccessToken + RefreshToken。

具体实现:

  • 登录时按 tenantCode + username 查询用户
  • BCrypt 校验密码,连续失败锁定账号
  • 生成 AccessToken 访问接口
  • 生成 RefreshToken 做无感续签
  • 登录成功后缓存用户权限到 Redis

效果:

  • 认证逻辑清晰,支持多租户登录
  • 降低密码泄露和暴力破解风险
  • 用户短期 Token 过期后可无感续签

亮点2:RBAC 权限模型 + Redis 权限缓存

问题:不同岗位权限不同,直接给用户分配权限难以维护。

方案:设计用户、角色、权限三层 RBAC 模型,权限标识采用模块、资源、动作三段式。

具体实现:

  • 用户绑定角色,角色绑定菜单和按钮权限
  • Controller 使用 @SaCheckPermission 做接口鉴权
  • 用户权限列表缓存到 Redis
  • 角色权限变更时主动删除权限缓存

效果:

  • 权限控制精确到按钮和接口级别
  • 权限查询性能提升
  • 角色权限集中管理,维护成本低

亮点3:多租户隔离 + 数据权限自动过滤

问题:SaaS 系统要保证租户之间数据隔离,租户内部还要按店铺、仓库、本人数据过滤。

方案:MyBatis-Plus 多租户插件 + @DataScope 注解 + AOP 数据权限。

具体实现:

  • 所有业务表带 tenant_id
  • 登录后将 tenantId 放入 ThreadLocal
  • MyBatis-Plus 自动追加 tenant_id
  • @DataScope 控制本人、部门、自定义数据范围
  • 供应商 Portal 强制追加 supplier_id

效果:

  • 租户数据完全隔离
  • 运营、仓库、采购、供应商等角色只能看授权范围
  • 数据权限逻辑统一,减少业务代码重复

亮点4:SaaS 套餐限制与租户生命周期

问题:SaaS 产品需要按套餐控制功能和用量,支持试用、续费、到期和数据保留。

方案:设计套餐功能开关表和套餐限制 AOP。

具体实现:

  • sys_plan_feature 配置套餐功能和限制值
  • @PlanLimit 注解拦截新增供应商、仓库、平台等操作
  • 租户注册后初始化管理员、角色、菜单、字典和默认配置
  • 到期后切换只读,拦截写操作
  • 数据保留期后归档或清理

效果:

  • 支持基础版、专业版、企业版差异化售卖
  • 套餐限制在后端强制执行
  • 租户试用、续费、到期流程闭环

亮点5:接口安全防护体系

问题:后台接口面临 SQL 注入、XSS、恶意刷接口、重复提交和越权操作风险。

方案:构建限流、输入校验、幂等、脱敏、审计多层防护。

具体实现:

  • MyBatis 参数绑定防 SQL 注入
  • XSS Filter 过滤危险脚本
  • Redis 滑动窗口实现接口限流
  • Redis SETNX 实现幂等防重复提交
  • 敏感字段返回脱敏、存储加密
  • AOP 记录高风险操作审计日志

效果:

  • 登录、短信、导出等敏感接口被有效限流
  • 重复提交不会产生重复业务数据
  • 高风险操作可追溯

亮点6:Docker + CI/CD + Nginx 生产部署体系

问题:手动部署容易环境不一致、漏执行测试、配置出错。

方案:Docker 容器化 + GitHub Actions 自动化流水线 + Nginx HTTPS 接入。

具体实现:

  • 后端 Dockerfile 使用多阶段构建
  • docker-compose 编排 MySQL、Redis、RocketMQ、Nacos、业务服务、Nginx
  • GitHub Actions 自动测试、构建镜像、推送镜像、部署测试环境
  • 生产环境使用审批保护
  • Nginx 配置 HTTPS、反向代理、安全响应头和静态资源缓存

效果:

  • 部署流程标准化
  • 环境一致性更好
  • 发布过程可追溯、可回滚

亮点7:监控告警与生产可运维性

问题:系统上线后需要及时发现故障,否则接口慢、订单失败、库存同步失败很难第一时间定位。

方案:Spring Boot Actuator + Prometheus + Grafana + AlertManager。

具体实现:

  • 采集 JVM、接口、数据库、Redis、磁盘等技术指标
  • 采集订单失败率、库存同步失败、账单解析失败等业务指标
  • 设置分级告警阈值
  • 告警通过邮件、企业微信或钉钉通知
  • Actuator 端点加白名单或独立管理端口

效果:

  • 技术故障和业务异常都能及时发现
  • 线上问题定位效率更高
  • 系统具备基础生产运维能力

面试高频问题

Q1:什么是 RBAC?为什么不用直接给用户分配权限?

RBAC 是基于角色的访问控制,核心关系是用户绑定角色,角色绑定权限。

如果直接给用户分配权限,用户数量一多就很难维护。比如采购专员有 20 个权限,新来 10 个采购就要配置 200 次。RBAC 把权限收敛到角色上,新员工只要分配采购角色即可。

追问:权限标识怎么设计?

答:用三段式:模块、资源、动作,比如 srm:supplier:audit,表示 SRM 模块供应商审核权限。

Q2:登录时为什么要传 tenantCode?

因为这是 SaaS 系统,同一个用户名可能存在于不同租户。

比如 A 公司和 B 公司都可以有 admin 账号。如果只按 username 查询,就可能查错租户用户。正确做法是先根据 tenantCode 定位租户,再按 tenantId + username 查询用户。

Q3:BCrypt 和 MD5 有什么区别?

MD5 速度快,相同密码结果相同,容易被彩虹表和暴力破解攻击。

BCrypt 会自动加盐,并且可以设置计算成本。相同密码每次加密结果不同,但可以验证是否匹配。它比 MD5 更适合密码存储。

Q4:AccessToken 和 RefreshToken 为什么要分开?

AccessToken 用于访问业务接口,生命周期短,降低泄露风险。

RefreshToken 用于刷新 AccessToken,生命周期长,但不能直接访问业务接口。这样既保证安全,也避免用户 Token 短期过期后频繁登录。

Q5:权限缓存到 Redis 后,权限变更怎么生效?

角色权限变更、用户角色变更、用户禁用、套餐变更时,要删除相关用户的权限缓存。

下次请求时缓存未命中,系统重新从数据库加载权限。这样既提升正常访问性能,也能保证权限变更及时生效。

Q6:多租户隔离怎么实现?

所有业务表都有 tenant_id 字段。用户登录后,从 Token 中取出 tenantId 放入 ThreadLocal。

MyBatis-Plus 多租户插件执行 SQL 前自动追加 tenant_id = 当前租户,保证不同租户数据隔离。

请求结束后必须清理 ThreadLocal,避免线程复用导致租户串用。

Q7:数据权限和租户隔离有什么区别?

租户隔离解决公司之间不能互相看数据。

数据权限解决同一公司内部不同员工看不同范围的数据。比如美国站运营只能看美国店铺订单,广州仓管理员只能看广州仓库存。

租户隔离是底线,数据权限是更细粒度控制。

Q8:供应商 Portal 为什么要强制 supplier_id 隔离?

供应商不是租户内部员工,它只能看自己的采购单、送货单、对账单。

如果只靠前端传 supplierId,供应商可以篡改参数查看其他供应商数据。所以后端必须根据当前登录账号绑定的 supplierId 强制追加过滤条件。

Q9:套餐权限和功能权限有什么区别?

功能权限看的是用户有没有被授权使用某功能。

套餐权限看的是租户购买的版本是否包含这个功能。

比如财务专员有 BI 查看权限,但租户是基础版,没有开通高级 BI,那么仍然不能访问。

Q10:套餐数量限制怎么实现?

sys_plan_feature 配置每个套餐的功能和限制值,比如基础版最多 20 家供应商。

接口上加 @PlanLimit(feature = "supplier.max"),AOP 执行前查询当前租户套餐、限制值和当前使用量。如果达到上限,就返回升级套餐提示。

Q11:如何防止 SQL 注入?

核心是不要拼接 SQL。

MyBatis 中使用 #{} 参数绑定,而不是 ${} 字符串替换。动态排序字段必须使用白名单映射,不能直接把前端传来的字段拼进 SQL。

Q12:如何防止 XSS?

后端对用户输入做过滤或转义,前端展示文本时使用 v-text 或普通插值,不随便使用 v-html

富文本场景必须做白名单过滤,只允许安全标签和属性。RefreshToken 放 HttpOnly Cookie,也能降低 XSS 窃取风险。

Q13:接口限流怎么实现?

可以用 Redis 滑动窗口。

Key 可以按用户 ID、IP、接口路径组合。每次请求记录当前时间戳,删除窗口外的旧时间戳,再统计窗口内请求数。超过阈值就返回 429。

登录、短信、导出、上传等接口要单独配置更严格的阈值。

Q14:如何防重复提交?

前端生成 requestId,后端用 Redis SETNX 做幂等。

第一次请求设置成功,执行业务;重复请求设置失败,直接返回“请勿重复提交”。适合创建订单、付款申请、库存调整等接口。

Q15:审计日志记录哪些内容?

审计日志主要记录高风险写操作。

字段包括租户、操作人、IP、模块、动作、请求参数、结果状态、错误信息、耗时、操作时间。密码、银行卡号、Token 等敏感字段必须脱敏或不记录。

Q16:Docker 和虚拟机有什么区别?

虚拟机有完整操作系统,隔离强,但资源占用大、启动慢。

Docker 容器共享宿主机内核,镜像轻、启动快、部署方便。对于 Java 微服务项目,Docker 更适合交付和弹性扩展。

Q17:docker-compose 里为什么要用 volume?

容器本身是可以删除重建的。如果 MySQL、Redis、RocketMQ 数据只存在容器内部,容器删除后数据就没了。

volume 用来持久化数据。即使容器重建,数据库文件、Redis 数据、消息存储和日志仍然保留。

Q18:CI/CD 流程怎么设计?

代码提交后触发 GitHub Actions。

先运行代码检查和测试,测试通过后构建 Docker 镜像,推送镜像仓库。测试环境可以自动部署,生产环境需要负责人审批。部署后做健康检查,失败则回滚或告警。

Q19:Nginx 在系统里起什么作用?

Nginx 是接入层。

它负责 HTTPS、前端静态资源、API 反向代理、安全响应头、上传大小限制、访问日志。后端服务不直接暴露公网,外部请求先进 Nginx,再转发到网关。

Q20:系统上线后监控哪些指标?

技术指标包括接口 P99、错误率、JVM 内存、GC、数据库连接池、Redis 内存、磁盘。

业务指标包括订单处理失败率、库存同步失败、账单解析失败、物流轨迹拉取失败。

技术指标正常不代表业务正常,所以业务指标也必须监控。


面试准备建议

必须能画的图

登录认证流程:

flowchart TD
    A[提交登录] --> B[校验租户和用户]
    B --> C[BCrypt 校验密码]
    C --> D[生成 Token]
    D --> E[缓存权限]
    E --> F[返回用户信息和权限列表]

权限验证流程:

flowchart TD
    A[请求到达] --> B[解析 Token]
    B --> C[检查功能权限]
    C --> D[检查套餐权限]
    D --> E[追加租户和数据权限]
    E --> F[执行业务]

SaaS 租户生命周期:

flowchart TD
    A[注册租户] --> B[初始化数据]
    B --> C[试用期]
    C --> D[正式订阅]
    D --> E[到期提醒]
    E --> F[只读状态]
    F --> G[续费恢复或归档清理]

部署流水线:

flowchart TD
    A[提交代码] --> B[自动测试]
    B --> C[构建镜像]
    C --> D[推送镜像]
    D --> E[部署环境]
    E --> F[健康检查]

必须能讲清楚的

  • 登录为什么要带 tenantCode
  • BCrypt 为什么比 MD5 更适合密码存储
  • RBAC 用户、角色、权限的关系
  • 权限缓存如何失效
  • 多租户隔离和数据权限的区别
  • 供应商 Portal 为什么要强制 supplier_id
  • 套餐限制和功能权限的区别
  • 限流、幂等、审计日志分别解决什么问题
  • Dockerfile、docker-compose、Nginx、CI/CD 各自负责什么
  • 监控为什么要同时看技术指标和业务指标

必须能写的 SQL 和代码

  • Sa-Token 登录认证代码
  • BCrypt 密码校验代码
  • 权限缓存读取和失效代码
  • MyBatis-Plus 租户插件配置
  • @DataScope 注解和 AOP
  • @PlanLimit 套餐限制 AOP
  • Redis 滑动窗口限流伪代码
  • Redis SETNX 幂等伪代码
  • Dockerfile 和 docker-compose 核心配置
  • GitHub Actions 核心流水线

可能的追问和回答

追问:为什么不用 Spring Security?

回答:Spring Security 功能强,但配置复杂。中后台场景用 Sa-Token 能更快落地登录、权限、踢人下线、多端登录。真正的安全还要靠密码策略、数据权限、接口防护和审计日志配合。

追问:如果管理员改了某个角色权限,用户需要重新登录吗?

回答:不一定。可以在角色权限变更后删除相关用户的权限缓存。用户下次请求时重新加载权限,就能生效。如果是特别敏感的权限变更,也可以强制用户下线。

追问:为什么套餐限制不能只在前端做?

回答:前端限制不可信,用户可以绕过页面直接调接口。套餐限制必须后端拦截,前端只负责展示升级引导。

追问:为什么 Actuator 不能直接暴露外网?

回答:Actuator 可能暴露健康状态、指标、环境信息和内部结构。如果暴露外网,攻击者可以获取系统信息。生产环境要加白名单、认证或独立管理端口。


完整简历版

项目经历

项目名称:跨境电商 SaaS 柔性供应链管理平台
负责模块:权限安全系统 + SaaS 租户运营 + 项目部署上线
项目时间:2024.06 - 2024.12
技术栈:Spring Boot 3.2、Sa-Token、MyBatis-Plus、MySQL 8.0、Redis、RocketMQ、Docker、GitHub Actions、Nginx、Prometheus、Grafana

项目描述:
该项目面向跨境电商卖家,提供供应商、采购、仓储、商品、订单、物流、财务和 BI 的一体化管理能力。
我负责权限安全、SaaS 租户运营和部署上线模块,设计并实现登录认证、RBAC 权限、数据权限、
套餐限制、租户生命周期、接口安全防护、审计日志、Docker 容器化部署、CI/CD 自动化流水线和监控告警。

业务规模:
系统支持 500+ 租户,日均处理接口请求 100 万+,覆盖 8 大业务模块和多角色权限控制。

核心业绩

使用 Sa-Token + BCrypt 实现了多租户登录认证体系,提升账号安全性。

  • 登录时根据 tenantCode 定位租户
  • BCrypt 存储密码,避免明文和普通 MD5 风险
  • 连续登录失败自动锁定账号
  • AccessToken + RefreshToken 支持安全访问和无感续签

使用 RBAC 权限模型实现了菜单、按钮和接口级权限控制。

  • 设计用户、角色、权限三层关系
  • 权限标识采用模块、资源、动作三段式
  • Controller 使用权限注解拦截接口
  • 权限列表缓存 Redis,角色变更后主动失效

使用 MyBatis-Plus 多租户插件和数据权限注解实现了数据隔离。

  • 所有业务表通过 tenant_id 做行级隔离
  • ThreadLocal 保存当前租户上下文
  • @DataScope 支持本人、部门、自定义数据范围
  • 供应商 Portal 强制 supplier_id 隔离

使用套餐功能开关和 AOP 拦截实现了 SaaS 商业化限制。

  • 设计 sys_plan_feature 配置套餐功能和用量
  • @PlanLimit 拦截新增供应商、仓库、平台等操作
  • 支持基础版、专业版、企业版差异化功能
  • 到期租户自动切换只读状态

使用 Redis 实现接口限流和幂等防重复提交。

  • 登录、短信、上传、导出接口配置不同限流策略
  • Redis 滑动窗口限制访问频率
  • Redis SETNX 防止订单、付款、库存调整重复提交
  • 恶意请求和误重复点击都能被拦截

使用 AOP 实现操作审计日志,保证高风险操作可追溯。

  • 供应商审核、库存调整、付款审批、套餐变更、授权操作全部记录
  • 记录操作人、租户、IP、请求参数、结果和耗时
  • 敏感字段脱敏
  • 异步写入审计日志,降低主流程耗时

使用 Docker 和 docker-compose 实现了项目容器化部署。

  • 后端 Dockerfile 使用多阶段构建
  • docker-compose 编排 MySQL、Redis、RocketMQ、Nacos、业务服务和 Nginx
  • 使用 volume 持久化数据库和中间件数据
  • 使用 .env 管理环境变量和敏感配置

使用 GitHub Actions 实现了自动测试、构建镜像和部署流水线。

  • 代码提交后自动运行测试
  • 测试通过后构建 Docker 镜像
  • 镜像使用 commit sha 标记,便于追溯
  • 生产部署使用审批保护和健康检查

使用 Nginx + HTTPS 实现统一接入和传输安全。

  • Nginx 统一代理前端和后端 API
  • 配置 HTTPS、HSTS、安全响应头
  • 后端服务不直接暴露公网
  • 访问日志保留真实客户端 IP

使用 Prometheus + Grafana + AlertManager 建立生产监控告警体系。

  • 监控 JVM、接口延迟、错误率、数据库、Redis、磁盘
  • 监控订单失败率、库存同步失败等业务指标
  • 告警按等级通知对应负责人
  • 提升线上问题发现和定位效率