框架
RBAC权限模型
RBAC 基于角色的访问控制
简而言之,共有三个类型(用户、角色、权限),用户和权限通过角色进行交集绑定,也就是某一个用户有哪一些角色,这些角色有哪一些权限。只要用户和角色、角色和权限之间存在交集,就是该用户具有某一个权限。
- 用户(User) —— 可以拥有多个 角色(Role)
- 角色(Role) —— 可以拥有多个 菜单/权限(Menu/Permission)
- 菜单(Menu) —— 包含目录、菜单、按钮(操作权限),支持 权限标识(permission) 如
system:user:query
关系总结:
- 用户 ↔ 角色(多对多)
- 角色 ↔ 菜单(多对多)
- 菜单中定义 按钮/接口权限(如 user:add、user:edit 等)
权限类型
| 权限类型 | 说明 | 实现方式 |
|---|---|---|
| 功能权限 | 控制用户能看到哪些菜单、按钮,能调用哪些接口 | 动态路由 + @PreAuthorize 或自定义注解 |
| 按钮权限 | 细粒度到页面按钮(如新增、删除、导出按钮) | 权限标识(permission) + 前端 v-hasPermi 指令 |
| 数据权限 | 控制用户能看到哪些数据(本部门、本人、自定义部门等) | @DataScope 注解 + MyBatis 拦截器重写 SQL |
| 租户权限 | SaaS 多租户隔离,每个租户可独立配置菜单、角色、数据权限 | Tenant 过滤器 + TenantRedisCacheManager |
权限校验流程
后端:
- 使用 Spring Security + JWT/Token
- 登录后加载用户所有角色和权限,存入 Redis(使用 TimeoutRedisCacheManager 缓存权限信息)
- 接口权限校验:@PreAuthorize("hasPermission('system:user:query')") 或自定义 Security 表达式
- 数据权限:MyBatis 拦截器(DataScopeAspect)在 SQL 执行前动态追加 WHERE 条件
前端:
- 登录后获取用户菜单树(动态路由)
- 使用 v-hasPermi="'system:user:add'" 或 v-hasRole="'admin'" 控制按钮显示
- 路由守卫中进行权限校验,防止未授权页面访问
双Token系统
双Token机制出现的需求是: 需要既能高频使用来保证接口安全(短效Access Token),又能长期保持登录状态从而不打扰用户(长效Refresh Token)
设置访问令牌,先将访问令牌设置到数据库中,之后设置到Redis中。
刷新令牌可以不需要设置到Redis中,原因是这个刷新令牌的过期时间很长30天。
如果用户注销登录,删除访问令牌(删除数据库中的和Redis中的),之后删除刷新令牌。
开发测试Mock Token
virgo:
security:
mock-enable: true # 是否开启Mock功能
mock-secret: test # 设置Mock密钥 不设置默认为test
# 请求头示例
# Token必须以{{mock-secret}}开头, 后面跟用户编号, 如test1
Authorization: Bearer test1
相关类:
- com.virgo.framework.security.config.SecurityProperties 配置文件
- com.virgo.framework.security.core.filter.TokenAuthenticationFilter#mockLoginUser() Mock实现逻辑
实现URL是否需要登录
com.virgo.framework.security.config.VirgoWebSecurityConfigurerAdapter
filterChain(HttpSecurity httpSecurity) URL 安全配置
1. getPermitAllUrlsFromAnnotations() 获取所有@PermitAll注解的接口
2. com.virgo.module.system.framework.security.config.SecurityConfiguration 业务模块自定义安全配置
3. 配置文件配置免登录URL
virgo:
security:
permit-all-urls:
- /admin-api/system/auth/login
- /admin-api/system/auth/logout
- /admin-api/system/auth/refresh-token
框架组件
操作日志、访问日志、错误日志处理
访问日志
com.virgo.framework.apilog.core.annotation.ApiAccessLog 注解定义
com.virgo.framework.apilog.core.filter.ApiAccessLogFilter 拦截器处理
@ApiAccessLog
enable: 是否记录访问日志
requestEnable: 是否记录请求参数
responseEnable: 是否记录响应结果
sanitizeKeys: 敏感参数数组, 配置后不会输出到日志
本地开发调试
spring-boot-starter-env组件,通过tag给服务打标签,实现在同一个注册中心的情况下,本地只需要正常启动服务,保证自己的请求只会打到自己的服务。
实现方法
com.virgo.framework.env.config.EnvEnvironmentPostProcessor
将 virgo.env.tag 设置到 nacos 等组件对应的 tag 配置项,当且仅当它们不存在时
virgo:
env: # 多环境的配置项
tag: ${HOSTNAME}
EnvironmentPostProcessor是一个用于在Spring环境准备完成之后、应用上下文创建之前,对配置环境进行自定义处理。
启动流程:
- SpringApplication.run();
- 环境准备时期:创建ConfigurableEnvironment对象。
- 属性加载:加载application.yaml文件配置。
- EnvironmentPostProcessor执行-自定义环境处理。
- 应用上下文创建-创建ApplicationContext。
- Bean加载和初始化-完成应用的启动。
由于这个EnvironmentPostProcessor的执行时机是在nacos执行之前,所以在nacos加载的时候可以做到侵入式的添加metadata = {tag}。
需要内网穿透访问本机, 否则请求会报错, 如:
io.netty.channel.ConnectTimeoutException: connection timed out after 30000 ms: /198.18.0.1:48087
负载均衡
com.virgo.gateway.filter.grey.GrayLoadBalancer 灰度负载均衡, 支持按version, tag调用请求
1. 服务发布时设置version, 自动设置到Nacos的元数据metadata中, 如version:1.0.0, 需在本地application.yaml中配置
2. 可以同时发布同一服务不同版本
3. 请求时未带version, 所有实例中选择
4. 请求时带了version, 根据version匹配实例进行选择
5. version未匹配到实例, 兜底在所有实例中选择
6. tag一般用于本地开发调试, 所以请求未带tag时,过滤所有tag实例, 以免打到本地环境.
SaaS多租户
技术组件
多租户DB实现
实现方案:
| 方案一: 分库 | 方案二: 同库分表 | 方案三: 同库同表字段区分 | |
|---|---|---|---|
| 维护成本 | 高 | 中 | 低 |
| 隔离性 | 高 | 高 | 低 |
| 性能 | 强 | 中 | 低 |
| 硬件成本 | 高 | 中 | 低 |
| 备份/还原 | 简单 | 中 | 困难 |
方案一二: 采用Mycat, Sharding-Sphere分库分表
方案三: 基于MyBatis-Plus的tenant_id字段,自动进行过滤 WHERE tenant_id = ?。 Mybatis-plus插件。
多租户实现
virgo-spring-boot-starter-biz-tenant
com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler
1. getTenantId(获取租户ID值的表达式)
2. getTenantIdColumn(获取租户的字段名,默认是:tenant_id)
3. ignoreTable(根据表名判断是否需要忽略拼接多租户条件)
4. ignoreInsert(忽略插入租户字段逻辑)
com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor
大概就是两种,一种是关于租户ID值的表达式,另一种是是否忽略掉一些表结构。
com.virgo.framework.tenant.config.TenantProperties
com.virgo.framework.tenant.config.VirgoTenantAutoConfiguration
com.virgo.framework.tenant.core.security.TenantSecurityWebFilter
com.virgo.framework.tenant.core.db.TenantDatabaseInterceptor
com.virgo.framework.tenant.core.web.TenantVisitContextInterceptor 跨租户访问
com.virgo.framework.tenant.core.redis.TenantRedisCacheManager 多租户缓存实现
virgo:
tenant: # 多租户相关配置项
enable: true # 租户是否开启
ignore-urls: # 忽略多租户的请求
- /jmreport/* # 积木报表,无法携带租户编号
ignore-visit-urls: # 忽略跨租户的请求
- /admin-api/system/user/profile/**
- /admin-api/system/auth/**
ignore-tables: # 忽略多租户的表
ignore-caches: # 忽略多租户的Spring Cache缓存
- user_role_ids
- permission_menu_ids
- oauth_client
缓存扩展
使Spring Cache支持失效时间
com.virgo.framework.redis.core.TimeoutRedisCacheManager
@Cacheable(cacheNames = "name 0X1 苹果CMS接口说明以及采集源搜索 d", key = "key")。
cacheNames格式为 "key#ttl" 时,# 后面的 ttl 为过期时间。
单位为最后一个字母(支持的单位有:d 天,h 小时,m 分钟,s 秒),默认单位为 s 秒
Lock4j 分布式锁/限流
默认配置项
lock4j:
acquire-timeout: 3000 #默认值3s,可不设置
expire: 30000 #默认值30s,可不设置
primary-executor: com.baomidou.lock.executor.RedisTemplateLockExecutor #默认redisson>redisTemplate>zookeeper,可不设置
lock-key-prefix: lock4j #锁key前缀, 默认值lock4j,可不设置
// 默认获取锁超时3秒,30秒锁过期
// redisKey: "redisson_unlock_latch:{lock4j:com.xxx.Lock4jControllertest#}:a2a4ace0773a9209027618d3acfbe502"
@Lock4j
// 指定获取锁超时1秒, 60秒锁过期
// "redisson_unlock_latch:{lock4j:com.xxx.Lock4jControllertest1 [0X1 苹果CMS接口说明以及采集源搜索](https://anaer.github.io/blog/post/1.html) .2}:7c9e1923f21038141ec9fc93172f80e5"
@Lock4j(keys = {"#user.id", "#user.name"}, expire = 60000, acquireTimeout = 1000)
// 用户在5秒内只能访问1次
// "lock4j:com.xxx.Lock4jControllertest2 [0X1 苹果CMS接口说明以及采集源搜索](https://anaer.github.io/blog/post/1.html) "
@Lock4j(keys = {"#user.id"}, acquireTimeout = 0, expire = 5000, autoRelease = false)
// 默认获取锁超时3秒,30秒锁过期 指定锁名称
// "lock4j:lockName [0X1 苹果CMS接口说明以及采集源搜索](https://anaer.github.io/blog/post/1.html) "
@Lock4j(name = "lockName")
// 用户在5秒内只能访问1次 指定锁名称
// "redisson_unlock_latch:{lock4j:lockName#}:fd80f8c0116e2c3af7b20e6cee85467e"
@Lock4j(keys = {"#user.id"}, name = "lockName", acquireTimeout = 0, expire = 5000, autoRelease = false)
-
Lock4j未指定name时, 会根据包名+类名+方法名生成name, 如: com.xxx.Lock4jControllertest, 类名与方法名间没有分隔符
-
keys会以.拼接, 如1.2, name与keys以#分隔
-
key前缀通过配置lock-key-prefix获取
https://github.com/baomidou/lock4j/blob/381551f5fe07f546addafff25f4e02cebe60bdc5/lock4j-core/src/main/java/com/baomidou/lock/aop/LockInterceptor.java#L74-L77
- 自释放key因为基于redisson锁释放通知机制, 会再包一层 redisson_unlock_latch:{lock_name}:{requestId}
https://github.com/redisson/redisson/blob/2edc2bb807afe73f2997777716f6d61274bc7264/redisson/src/main/java/org/redisson/RedissonBaseLock.java#L213
- Lock4j注解可以指定keyBuilderStrategy key生成策略和 failStrategy 失败策略
默认Key生成策略
https://github.com/baomidou/lock4j/blob/381551f5fe07f546addafff25f4e02cebe60bdc5/lock4j-core/src/main/java/com/baomidou/lock/DefaultLockKeyBuilder.java
默认失败策略
https://github.com/baomidou/lock4j/blob/381551f5fe07f546addafff25f4e02cebe60bdc5/lock4j-core/src/main/java/com/baomidou/lock/DefaultLockFailureStrategy.java
全局修改默认失败策略
package com.virgo.framework.lock4j.config;
import com.virgo.framework.lock4j.core.DefaultLockFailureStrategy;
import com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
@AutoConfiguration(before = LockAutoConfiguration.class)
@ConditionalOnClass(name = "com.baomidou.lock.annotation.Lock4j")
public class VirgoLock4jConfiguration {
@Bean
public DefaultLockFailureStrategy lockFailureStrategy() {
return new DefaultLockFailureStrategy();
}
}
Idempotent 幂等控制
保证一次请求只生效一次(业务幂等)
// "idempotent:f89875e7eb300d1fc65ea9d6e439153b"
@Idempotent
@Idempotent(timeout = 10, keyResolver = UserIdempotentKeyResolver.class)
DefaultIdempotentKeyResolver 默认全局Key规则, 根据MD5(方法名+参数)生成Key
UserIdempotentKeyResolver 用户基本Key规则, 根据MD5(方法名+参数+userId+userType)生成Key
ExpressionIdempotentKeyResolver 自定义表达式, 结合 keyArg 参数使用
key前缀在IdempotentRedisDAO定义
RateLimiter 流控
// 默认1秒100次
// 1) "{rate_limiter:a06d9953d9dd6ef9179aa0febb238975}:value" String 剩余量
// 2) "{rate_limiter:a06d9953d9dd6ef9179aa0febb238975}:permits" Zset 已分配量
// 3) "rate_limiter:a06d9953d9dd6ef9179aa0febb238975" Hash 流控信息
@RateLimiter
// 10分钟3次
@RateLimiter(time = 10, timeUnit = TimeUnit.MINUTES, count = 3, keyResolver = DefaultRateLimiterKeyResolver.class)
DefaultRateLimiterKeyResolver 全局级别 根据MD5(方法名+参数)生成Key
UserRateLimiterKeyResolver 用户 ID 级别 根据MD5(方法名+参数+userId+userType)生成Key
ClientIpRateLimiterKeyResolver 用户 IP 级别 根据MD5(方法名+参数+clientIp)生成Key
ServerNodeRateLimiterKeyResolver 服务器 Node 级别 根据MD5(方法名+参数+host+pid)生成Key
ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg()} 计算
版本更新
v2026.01 -> v2026.03