授权机制全链路架构
OptiCloud 平台授权机制运行时全链路 · 载体 → 内存 → 业务校验 → 事件传播 → 前端联动
后端开发、前端开发、架构评审。本文聚焦授权机制的运行时全链路架构(当前已实现):授权如何加载、如何作用于业务(请求 / 方法 / 菜单 / 按钮)、授权变化事件如何传播(进程内事件 + 缓存)、前端如何感知联动。
与《软硬授权使用指南》互补——后者是面向运维 / 使用者的 API 使用与 FAQ;本文是面向开发的机制与代码链路。
CallbackImpl.invoke() 写 hardSortCertMaprefreshSoftCertification() 写 softSortCertMapsuperDog+featureId | 点位授权 → superDogPoint&&+featureIdWatchDog /** · EnergyRoute /energy /viewGET /system/certification/getCert@ModuleAuth + CheckModuleAuth —— 计费(51)/大屏(52)/组态(26) 方法级 → 无权限抛 467hasAuth() —— 定时任务无 EMS 权限则跳过SysMenuBizImpl —— authorizeFeatureId 显式匹配 + menuSource 间接匹配 → 过滤菜单树 + 按钮权限串ModuleAuthActivatedEvent (soft/hard)
发布 →
ModuleAuthEventListener
差集 →
ModuleAuthExpiredEvent(失效时)
→
ActiveFeatureHolder.refresh()
→
GET /system/feature/active → 前端 authStore
授权从「载体 → 内存 → 业务校验 / 缓存 / 前端」是一条完整的单向链路。
common/auth/enums/FeatureEnum.java。每个枚举值 = 一个可独立授权的功能模块。
| 枚举常量 | featureId | featureName | show | 说明 |
|---|---|---|---|---|
EASY_PRO_SERVICE | 99 | 基础平台授权 | true | 主授权,必须拥有,否则全平台不可用 |
MODULE_ENERGY_SERVICE | 21 | 能耗模块授权 | true | EMS 能耗管理 |
MODULE_ENERGY_BILL_SERVICE | 51 | 能耗计费模块授权 | true | 能耗计费 |
MODULE_VIEW_SERVICE | 26 | 监控系统授权 | true | 应用组态 |
MODULE_BASE_VIEW_SERVICE | 61 | 可视化基础授权 | false | 可视化基础(不展示) |
MODULE_BIG_SCREEN_SERVICE | 52 | 大屏设计模块授权 | true | 大屏设计器 |
MODULE_DEVICE_SCREEN_SERVICE | 53 | 设备组态模块授权 | false | 设备组态(不展示) |
MODULE_APP_SERVICE | 54 | APP客户端模块授权 | true | 移动端 APP |
MODULE_ZHIWEN_SERVICE | 55 | AI智问模块授权 | true | AI 智能问答 |
POINTS_LIMIT_BEGIN | 100 | 点位授权 | true | 点位授权区间起点 |
POINTS_LIMIT_END | 201 | 点位授权 | true | 终点;= 201 表示无点位限制 |
featureId < 100为模块/主授权;100 ≤ featureId ≤ 201为点位授权档位。show=false的模块(61/53)不出现在授权信息列表(FeatureEnum.isShow()过滤),但仍参与权限判断。- 前端判断权限统一用枚举常量名
name(如"MODULE_ZHIWEN_SERVICE"),而非中文featureName(文案可能变动)。FeatureEnum.getName()返回该常量名。
common/auth/enums/BitFeatureEnum.java。授权码密文末尾的「授权组合数字」用二进制位编码各模块:
| 位 | 含义 |
|---|---|
| bit 12 | 主授权 |
| bit 3~11 | 各模块(大屏 / 能耗 / 能耗计费 / 监控 / APP / AI智问…) |
| bit 0~2(3 位) | 点位档位(映射 3000 / 5000 / 10000 / 20000 / 50000 / 无限) |
getFeatureIdsByAuthNum(authNumber):把十进制授权数按位拆解,返回 FeatureEnum ID 集合。getAllAuthNum():反向枚举所有组合,用于硬狗初始化(SuperDog.init()按每个组合调用 DLLInit)。
common/auth/domain/Certification.java。@Data @Builder。
| 字段 | 含义 |
|---|---|
featureId | 特征 ID |
machineCode | 机器码(CPU / 主板 / 硬盘序列号 MD5) |
type | "hard" 硬狗 / "cloud" 软授权 |
beginTime | 开始时间 yyyy-MM-dd HH:mm:ss |
endTime | 结束时间;"infinite" 表示永久 |
status | "yes" 已授权 / "no" 禁用 |
certificationName | 授权名(来自 FeatureEnum.featureName) |
cipherText | 授权密文(硬狗为描述文字;软授权原文存 DB) |
common/auth/domain/DogEncryptionStorage.java。静态类,两个 ConcurrentHashMap:
public static final Map<String, Certification> hardSortCertMap = new ConcurrentHashMap<>(); // 硬件狗 public static final Map<String, Certification> softSortCertMap = new ConcurrentHashMap<>(); // 软授权
| 授权类别 | key 格式 | 示例 |
|---|---|---|
| 主/模块授权 | superDog + featureId | superDog99、superDog21 |
| 点位授权 | superDogPoint&& + featureId | superDogPoint&&105 |
hasAuth(featureId)/hasAuth(FeatureEnum):是否拥有某模块授权hasAuthFeatureIdList():合并 hard+soft 两个 Map,经auth()时间过滤,返回当前有效 featureId 集合auth(certification, now):点位授权(featureId≥100)直接放行不参与时间判定;模块授权校验 begin/endTime 有效期authPoint():计算允许点位数(详见§5)
系统启动入口:SuperDog.execute()(common/auth/dog/SuperDog.java:141),由 InitializationManager 在 Spring 启动阶段调用,顺序执行三件事:
public void execute(ApplicationArguments args) { sysCertificationBiz.refreshSoftCertification(); // 1. 软授权 init(); // 2. 硬狗(try-catch 不阻塞启动) TimeTamperDetector.init(); // 3. 时间防篡改 }
softSortCertMap → 读 DB 主授权密文 → RSA 公钥解密 → 解析明文 → 逐条写入 Map → finally 发 ModuleAuthActivatedEvent("soft")SuperDog.dll;枚举所有授权组合调 DLL Init → SetInterval(dogID, 20*1000) + SetCallBack- 平台限制:仅 Windows 加载
SuperDog.dll(JNA),Linux 不加载。 - 初始化(
SuperDog.init()):用BitFeatureEnum.getAllAuthNum()枚举所有授权组合,对每个组合调DLL.Init(featureId, vendor)拿到 dogID,再SetInterval(dogID, 20*1000)+SetCallBack(dogID, callback)。 - 轮询周期:20 秒(注释写"5 分钟"但代码是
20*1000)。DLL 内部按此周期轮询硬件狗,仅在状态码变化时回调。 - 回调:
CallbackImpl.invoke(featureNum, code, type, datetime)BitFeatureEnum.getFeatureIdsByAuthNum(featureNum)按位拆出 featureId 集合- 按
code分支:code=0/45(成功)→ 按type(1=永久,2/3=限期)构造 Certification 写入hardSortCertMap;异常码 →remove对应 key(拔狗即移除授权) - 末尾发布
ModuleAuthActivatedEvent("hard")
- 要点:硬件狗不经 RSA 解密,狗本身就是授权载体,DLL 直接返回
(code, type, datetime)。
SysCertificationBizImpl.refreshSoftCertification()(modules/system/biz/impl/SysCertificationBizImpl.java:117):
softSortCertMap.clear()清空- 从 DB 读主授权记录(
featureId=99且type=software_cert)的cipherText RsaUtil.rsaDecToBean(cipherText)—— RSA 公钥解密(授权工具用私钥加密,后台用公钥解密)- 解密明文格式:
机器码#cloud#开始时间#结束时间#授权组合数字 - 解析出主授权 + 各模块/点位,逐条写入
softSortCertMap finally块发布ModuleAuthActivatedEvent("soft")
addCertification(),行 64)完整链路:前端提交 cipherText → RSA 解密 → 校验含主授权(99) → 校验机器码匹配 → 校验时间合法 → 覆盖写 DB → refreshSoftCertification() 重建内存 → 发事件
common/auth/time/TimeTamperDetector.java:
- 启动一个名为
"time-check"的虚拟线程,每 5 秒校验一次系统时间 - 用 AES 加密的时间戳日志文件
log.bin记录上次时间;若发现时间回退超过 1 小时 → 判定篡改 →timeCheckEnabled.set(false) - 每个请求由
WatchDogInterceptor先查isTimeCheckEnabled(),被判定篡改后所有请求被拦截(抛 code 452)
时间防篡改是授权有效期校验的前置保障,防止用户通过回拨系统时间绕过授权过期。一旦触发,整个平台请求被阻断,需排查 log.bin 后人工恢复。
授权状态写入 DogEncryptionStorage 后,通过以下四层作用于业务运行时:
主授权 99 全平台/能源路径
@ModuleAuth 模块级hasAuth() 定时任务SysMenuBizImpl
| 拦截器 | 拦截路径 | 校验对象 | 失败处理 |
|---|---|---|---|
WatchDogInterceptor | /**(全平台) | EASY_PRO_SERVICE(99) | 先做时间防篡改检测;无授权则 logout() + 抛 code 451 |
EnergyRouteInterceptor | /energy/**、/view/** | 同样校验 99 | 含 10s 启动宽限期 lazyBoot()(启动后 10s 内直接放行,等加密狗初始化);无授权则 logout() + 抛 451 |
两个拦截器的 isHasAuth() 逻辑一致:endTime="infinite" 或 now<endTime 有效;过期会把软授权 DB 记录置为 EXPIRED;status="no" 强制无效。
注册位置:AuthInterceptorConfig.addInterceptors()。排除路径含登录、swagger、/system/certification/**、/energy/calc/**、apk 下载等(getExcludePatterns())。
注 1:MonitorRouteInterceptor 虽是 @Component 但未注册,监控路径 /view/** 实际由 EnergyRouteInterceptor 兼管。
注 2:WatchDogInterceptor 虽定义了 lazyBoot() 方法,但其 preHandle 未调用它——宽限期仅在 EnergyRouteInterceptor 生效。
/energy、/view 主授权校验(本节)/wx/** 与平台端)project_id)授权校验(前 3 个)是整条链的前置门槛:主授权失效时后续拦截器根本不会执行(已 logout + 抛异常)。
注解 @ModuleAuth(FeatureEnum.XXX) + 切面 CheckModuleAuth:
@Around("@annotation(moduleAuth)") public Object around(ProceedingJoinPoint joinPoint, ModuleAuth moduleAuth) { if (!DogEncryptionStorage.hasAuth(moduleAuth.value())) { throw new AuthException(NOT_HAS_THIS_FUNCTION_ACCESS); // code 467 } return joinPoint.proceed(); }
- 能耗计费(51):
modules/bill/**下所有 Controller - 监控系统(26):
modules/optiview/controller/AppsController.java:76 - 大屏设计(52):
modules/view/controller/YcviewScreenController.java:345
modules/energy/task/EnergyCalculateTask.java:103—— 无 EMS 权限则不执行能耗计算modules/energy/task/ScheduleTask.java:60—— 无 EMS 权限则跳过阈值告警
SysMenu 实体有 authorizeFeatureId 字段(菜单/按钮绑定的 featureId)和 menuSource(菜单类型)。
getMenuListForCurUser()(SysMenuBizImpl.java:86)两个过滤维度:
- 显式匹配:菜单
authorizeFeatureId必须在当前激活集合内 - 间接匹配(
isHasPermission,按 menuSource):APP 类型需 26、SCREEN 需 52、SYSTEM 需 53
getCurUserPermissions()(行 1480)同理过滤按钮,返回 perms 权限标识串供前端做按钮级控制。
后端已按授权过滤菜单树 / 路由 / 按钮权限,前端拿到的即是过滤后的结果。
这是「授权变化 → 业务 / 前端感知」的核心机制。
定义在 event/auth/ModuleAuthEventListener.java 内部:
| 事件 | 携带数据 | 发布时机 |
|---|---|---|
ModuleAuthActivatedEvent | source(soft/hard) + activeFeatures(构造时快照) | 软授权刷新后(SysCertificationBizImpl:176);硬狗回调后(CallbackImpl:137) |
ModuleAuthExpiredEvent | expiredFeatures(失效差集) | 仅由 Listener 内部计算差集后派发 |
- A. 软授权:
SysCertificationBizImpl.refreshSoftCertification()的finally(行 176)—— 无论成功 / 清空 / 失败都发一次 - B. 硬狗:
CallbackImpl.invoke()末尾(行 137)—— 每次硬狗状态回调解析完都发
- C. 失效差集:
ModuleAuthEventListener.onModuleAuthActivated()内部(行 119)—— 对比lastActiveFeatureIds求差集,差集非空才发。失效事件不会被业务方直接发布,统一由 Listener 计算后转发。
@EventListener public synchronized void onModuleAuthActivated(ModuleAuthActivatedEvent event) { handleModuleAuthActivated(...); // → activeFeatureHolder.refresh() Set<Integer> expiredIds = new HashSet<>(lastActiveFeatureIds); expiredIds.removeAll(currentIds); // 差集 = 上次有、本次无 if (!expiredIds.isEmpty()) { publisher.publishEvent(new ModuleAuthExpiredEvent(expiredFeatures)); } lastActiveFeatureIds = currentIds; // 更新基准 }
lastActiveFeatureIds:上次激活快照,失效差集计算的唯一基准;初始为空,故首次激活不产生失效事件。synchronized保证并发安全。
common/auth/holder/ActiveFeatureHolder.java:
private volatile List<FeatureEnum> activeFeatures—— volatile 引用 + 不可变 List,读无锁、写synchronized@PostConstruct init()—— 启动预热一次(幂等)refresh()—— 从hasAuthFeatureIdList()重算并整体替换引用- 触发方:
ModuleAuthEventListener.handleModuleAuthActivated/Expired()两处
FeatureController(GET /system/feature/active)直接读 ActiveFeatureHolder 缓存返回 List<FeatureEnum>(序列化为对象数组,含 name/url/featureId/featureName/show)。不实时计算,返回事件刷新后的快照。
本节说明授权激活 / 失效事件在当前实现中如何传播,以及前端如何感知。
授权事件是纯进程内 Spring 事件,完整传播路径如下:
ModuleAuthActivatedEvent("soft"/"hard")refreshSoftCertification finally | 硬狗 CallbackImpl.invoke() 末尾ModuleAuthEventListener.onModuleAuthActivated()synchronizedhandleModuleAuthActivated() → ActiveFeatureHolder.refresh()刷新本地缓存lastActiveFeatureIds 差集ModuleAuthExpiredEventonModuleAuthExpired() → ActiveFeatureHolder.refresh()lastActiveFeatureIdsgetFeatureList() → GET /system/feature/active 读 ActiveFeatureHolder 缓存 → 更新按钮/菜单显隐- 事件的唯一副作用是刷新
ActiveFeatureHolder本地缓存(ModuleAuthEventListener.java:141-154)。 - 前端不会被实时通知,需等到下一次路由切换(路由守卫调
getFeatureList())或用户主动操作(如授权成功回调里立即调getFeatureList(),见§6.4)才会拉取最新状态。 - 事件不跨进程 / 实例传播(授权状态本身存于 JVM 静态
DogEncryptionStorage,是单进程模型)。
授权事件基于 Spring 的 ApplicationEvent 机制。两个事件类型定义在 ModuleAuthEventListener 内部:
| 事件类型 | 触发时机 | 携带数据 |
|---|---|---|
ModuleAuthActivatedEvent | 软授权刷新后 / 硬狗状态回调后 | source(soft/hard) + activeFeatures(当前激活全集) |
ModuleAuthExpiredEvent | 激活事件触发后,检测到有失效项时 | expiredFeatures(本次失效差集) |
ModuleAuthEventListener 自身在 handleModuleAuthActivated/Expired 中调用 activeFeatureHolder.refresh() 刷新缓存(ModuleAuthEventListener.java:141-154)。这是事件的默认消费者。
授权事件最常见的实战用途是:某个模块被激活 / 失效时,自动启停该模块的后台任务。以下示例取自 carbon 分支的账单模块(MODULE_ENERGY_BILL_SERVICE 被激活时启动自动账单 / 归档 / 关单 / 欠费告警定时任务,失效时移除)。
在 ModuleAuthEventListener 的两个 handle* 方法里,按 FeatureEnum 判断并联动业务:
@Resource private BillAutoBillTaskManager billAutoBillTaskManager; // 注入被联动的业务管理器 /** * 处理激活的模块授权 */ private void handleModuleAuthActivated(List<FeatureEnum> activeFeatures) { activeFeatureHolder.refresh(); // 当前 2.1.1:刷新激活功能缓存 // 业务联动:账单授权被激活 → 重启账单自动任务 if (activeFeatures.contains(FeatureEnum.MODULE_ENERGY_BILL_SERVICE)) { billAutoBillTaskManager.reStartAutoTask(); } } /** * 处理失效的模块授权 */ private void handleModuleAuthExpired(List<FeatureEnum> expiredFeatures) { activeFeatureHolder.refresh(); // 当前 2.1.1:刷新激活功能缓存 // 业务联动:账单授权失效 → 移除账单自动任务 if (expiredFeatures.contains(FeatureEnum.MODULE_ENERGY_BILL_SERVICE)) { billAutoBillTaskManager.removeAutoTask(); } }
被联动的业务管理器内部用 FeatureEnum.contains 守卫,确保只在对应模块授权变化时才动作。carbon 分支的 BillAutoBillTaskManager(modules/bill/business/task/BillAutoBillTaskManager.java)完整实现如下,可作为「授权驱动定时任务生命周期」的范本:
@Component @Slf4j public class BillAutoBillTaskManager implements InitTask { @Resource private BillAutoBillJob autoBillJob; @Resource private IScheduleHelper scheduleHelper; private final RedisService redisService = RedisKit.bill(); // 平台启动时:无账单授权则不启动任何账单任务 @Override public void execute(ApplicationArguments args) { if (!DogEncryptionStorage.hasAuth(FeatureEnum.MODULE_ENERGY_BILL_SERVICE)) return; startAutoTask(); } // 授权激活事件调用:重启全部账单自动任务 public void reStartAutoTask() { startAutoBillTask(true); // checkExist=true:已存在则跳过 startAutoArchiveTask(true); startAutoCloseOrderTask(true); startArrearsAlertTask(true); } // 授权失效事件调用:移除全部账单自动任务 public void removeAutoTask() { removeAutoBillTask(); removeArrearsAlertTask(); removeAutoArchiveTask(); removeAutoCloseOrderTask(); } // ... 每个 start*Task 内部用 scheduleHelper 注册 Quartz 定时任务,id 存 Redis }
ModuleAuthActivatedEvent)ModuleAuthEventListener.handleModuleAuthActivated/Expired —— 判断 activeFeatures / expiredFeatures 是否含 MODULE_ENERGY_BILL_SERVICEBillAutoBillTaskManager.reStartAutoTask() / removeAutoTask() —— 通过 IScheduleHelper 注册 / 移除 Quartz 定时任务(任务 ID 存 Redis 防重复)- 判断依据是
List<FeatureEnum>.contains(目标模块),激活事件拿到的是全集(判断「是否包含」即可),失效事件拿到的是差集(列表里的就是要处理的)。 - 平台启动时(
InitTask.execute)也用DogEncryptionStorage.hasAuth()做一次同样的守卫,保证重启后状态与授权一致。 - 业务管理器自身要做好幂等(carbon 用 Redis 存任务 ID,
checkExist=true时已存在则跳过),因为授权事件可能多次触发。
除在 ModuleAuthEventListener.handle* 里直接联动外,也可在任意 @Component 中用独立的 @EventListener 订阅(与 handle* 并行执行,互不影响)。项目内同款事件监听范式:AreaEventListener(区域删除解绑设备)、ProjectAsyncEventListener(项目状态变化刷新缓存)。
onModuleAuthActivated 是 synchronized 的,事件在发布线程内同步执行。若联动逻辑较重(如批量注册定时任务),可在监听方法上加 @Async 异步处理(参照 EnergyOverviewBizImpl.afterCalculation 的 @EventListener @Async 写法)。
DogEncryptionStorage.authPoint()(行 105):
- 分别从 hard/soft Map 取点位授权,按 featureId 降序取最大档位
- 换算:允许点数 =
(featureId - 100) * 500(每档 500 点) - 任一档位 featureId ≥ 201 → 返回
-1(无限制)
- 新增点位上限校验:
drivers/access/PlatformApiImpl.java、modules/iot/biz/impl/DriverBizImpl.java - 80% 容量预警(同上)
- 前端展示:
SysCertificationBizImpl.getPointCapacity()返回{curPointNum, pointCapacity, percentage}
写入点位授权 key 时,硬狗与软授权的边界判断条件不同,存在一处潜在不一致,排查点位计数异常时需留意。
| 来源 | 边界条件 | featureId=100 落点 | 写入条数 |
|---|---|---|---|
硬狗 CallbackImpl.java:78 | featureId >= 100 && <= 201 | 走点位 key(superDogPoint&&100) | 同时写两条(superDog100 + superDogPoint&&100) |
软授权 SysCertificationBizImpl.java:166 | featureId > 100 && <= 201 | 走模块 key(superDog100) | 只写一条(superDog100) |
即 featureId=100(0 点位档位)在硬狗下会进 superDogPoint&& 检索范围、在软授权下不会。实践中 100 档位(0 点)极少单独出现,影响有限,但排查点位计数异常时需留意此差异。
font_end/src/store/modules/authStore/authStore.ts:
| 成员 | 作用 |
|---|---|
state.authInfo | getCert() 返回的授权详情(CertVo) |
state.featureList | getActiveFeatures() 返回的激活功能列表 |
getter hasFeature(name) | 按枚举常量名判断(item.name === featureName) |
action getAuthInfo() | 拉 /system/certification/getCert |
action getFeatureList() | 拉 /system/feature/active,失败不置空(避免误隐藏已授权功能) |
font_end/src/router/index.ts:168,已有 router.beforeEach 守卫登录态校验通过后:
if (Cookies.get(multipleTabsKey) && userInfo) { useAuthStore().getFeatureList(); // 每次切页面刷新激活功能授权 ... }
不阻塞导航,保证后端缓存变化后前端下一次切页面即可同步。
font_end/src/layout/components/lay-navbar/index.vue:
- 智问按钮
v-show="showZhiwen",APP 按钮v-show="showApp" showZhiwen/showApp为 computed,基于authStore.hasFeature(...)- 用
v-show不用v-if:navbar 是常驻布局组件,v-show保持 DOM 骨架稳定,避免导航时虚拟 DOM 锚点失效(曾因v-if导致parentNode null报错,已修复)
font_end/src/layout/components/lay-navbar/components/AuthInfo.vue:
- 「大模型配置」Tab:
v-if="showZhiwen"(无智问授权则不显示该 Tab) - 软件 / 硬件授权卡片列表:无智问授权时过滤掉 name 含"智问"的卡片
- 授权成功回调(
submit()):除getAuthInfo()外立即调用getFeatureList(),使按钮 / Tab 即时按新权限显隐,无需等下次切页面
AuthInfo.vue 输入授权码 → POST /system/certification/addCertificationrefreshSoftCertification()ModuleAuthActivatedEvent("soft") → ModuleAuthEventListenerActiveFeatureHolder.refresh() [缓存更新]getAuthInfo() + getFeatureList()GET /system/feature/active 读到新缓存 → authStore.featureList 更新 → navbar 按钮 / 大模型 Tab 即时显隐硬件狗插拔:CallbackImpl 回调 → 发 ModuleAuthActivatedEvent("hard") → 缓存刷新 → 前端下次切页面 getFeatureList() 同步。
| 方式 | 适用场景 | 触发条件 | 无权限时 | 详见 |
|---|---|---|---|---|
① @ModuleAuth 注解 | 整个 Controller / 方法需要某模块授权才能访问 | 请求进入方法前 | 直接抛 AuthException(467) | §7.1 |
② hasAuth() 编程式 | 方法内部部分逻辑按权限分支(定时任务、降级逻辑) | 代码显式调用 | 由代码自行决定(通常静默跳过) | §7.2 |
| ③ Spring 事件 | 模块被激活 / 失效时启停业务(定时任务、缓存) | 授权状态变化时 | 不校验,只通知 | §7.3 / §7.4 |
「能不能访问这个接口?」→ 注解;「这段逻辑要不要执行?」→ 编程式;「授权变了我要做什么?」→ 事件。
在 Controller 方法 / 类上加注解 @ModuleAuth(FeatureEnum.XXX),由切面 CheckModuleAuth 拦截:进入方法前先查 DogEncryptionStorage.hasAuth(),无授权直接抛异常,业务方法不会执行。
@ModuleAuth(FeatureEnum.MODULE_ENERGY_BILL_SERVICE) // 进入前校验计费授权 @PostMapping("/save") public Result save(@RequestBody BillDTO dto) { // 能走到这里,说明已拥有计费模块授权(51) return Result.success(billBiz.save(dto)); }
- 有授权:正常进入方法执行。
- 无授权:抛
AuthException(NOT_HAS_THIS_FUNCTION_ACCESS),经AuthExceptionHandler统一转成Result.error(467, "用户无此功能权限")返回前端。
| 模块 | FeatureEnum | 位置 |
|---|---|---|
| 能耗计费 | MODULE_ENERGY_BILL_SERVICE (51) | modules/bill/** 下所有 Controller |
| 监控系统(应用组态) | MODULE_VIEW_SERVICE (26) | modules/optiview/controller/AppsController.java:76 |
| 大屏设计 | MODULE_BIG_SCREEN_SERVICE (52) | modules/view/controller/YcviewScreenController.java:345 |
注解只能用在模块级授权(featureId < 100)。点位授权(featureId 100~201)没有注解支持,需用 §7.2 编程式判断容量。
需要在方法内部按权限决定「要不要执行某段逻辑」时,直接调静态方法 DogEncryptionStorage.hasAuth(FeatureEnum.XXX)。不会抛异常,由调用方自行决定处理方式(通常用于定时任务、降级逻辑、点位容量校验)。
// 场景一:定时任务——无 EMS 授权则跳过执行 if (!DogEncryptionStorage.hasAuth(FeatureEnum.MODULE_ENERGY_SERVICE)) { return; // 静默跳过,不抛异常 } energyCalculateBiz.calculate(); // 场景二:启动时——无授权则不初始化该模块任务 if (!DogEncryptionStorage.hasAuth(FeatureEnum.MODULE_ENERGY_BILL_SERVICE)) return; startAutoTask();
| 方法 | 返回 | 用途 |
|---|---|---|
hasAuth(FeatureEnum) | boolean | 是否拥有某模块授权(最常用) |
hasAuth(featureId) | boolean | 同上,传 int |
hasAuthFeatureIdList() | Set<Integer> | 当前全部有效 featureId(已时间过滤) |
authPoint() | int | 允许点位数(-1 表示无限制,见 §5) |
注解是「拦截式」(无权限直接拒绝、抛 467),编程式是「询问式」(无权限时你自己决定怎么办)。接口入口用注解;方法内部分逻辑、定时任务、点位校验用编程式。
当某模块从无授权变为有授权(或软授权重新刷新)时发布。典型用途:模块被激活 → 自动启动该模块的后台任务 / 缓存。
| 属性 | 说明 |
|---|---|
| 触发时机 | 软授权 refreshSoftCertification() finally;硬狗 CallbackImpl.invoke() 末尾 |
| 携带数据 | source(soft/hard) + activeFeatures(当前激活全集 List<FeatureEnum>) |
| 判断方式 | activeFeatures.contains(目标模块) —— 因为是全集,判断「是否包含」即可 |
订阅写法(在 ModuleAuthEventListener.handleModuleAuthActivated 内联动,或在任意 @Component 用 @EventListener 独立订阅):
// 方式一:在 ModuleAuthEventListener.handleModuleAuthActivated 里联动(与缓存刷新同处) private void handleModuleAuthActivated(List<FeatureEnum> activeFeatures) { activeFeatureHolder.refresh(); if (activeFeatures.contains(FeatureEnum.MODULE_ENERGY_BILL_SERVICE)) { billAutoBillTaskManager.reStartAutoTask(); // 启动账单定时任务 } } // 方式二:独立 @EventListener(与 handle* 并行执行,互不影响) @EventListener public void onAuthActivated(ModuleAuthEventListener.ModuleAuthActivatedEvent event) { if (event.getActiveFeatures().contains(FeatureEnum.MODULE_ZHIWEN_SERVICE)) { // 智问授权被激活 → 初始化 AI 配置缓存 } }
用户在前端导入含某模块的授权码后,该模块对应的定时任务 / 业务资源会被自动拉起,无需重启平台。完整闭环范本见 §4.6.2(carbon 分支账单任务启停)。
当某模块从有授权变为无授权时发布(如拔狗、导入新授权码后旧模块消失、过期)。典型用途:模块失效 → 自动停止该模块的后台任务 / 释放资源。
| 属性 | 说明 |
|---|---|
| 触发时机 | 由 ModuleAuthEventListener 对比 lastActiveFeatureIds 差集后派发(差集非空才发) |
| 携带数据 | expiredFeatures(本次失效差集 List<FeatureEnum> —— 列表里的就是要处理的) |
| 判断方式 | expiredFeatures.contains(目标模块) —— 差集即「本次失效项」,命中即需处理 |
| 首次激活 | 不产生失效事件(基准 lastActiveFeatureIds 初始为空,差集恒空) |
订阅写法:
// 在 ModuleAuthEventListener.handleModuleAuthExpired 里联动 private void handleModuleAuthExpired(List<FeatureEnum> expiredFeatures) { activeFeatureHolder.refresh(); if (expiredFeatures.contains(FeatureEnum.MODULE_ENERGY_BILL_SERVICE)) { billAutoBillTaskManager.removeAutoTask(); // 移除账单定时任务 } }
失效事件触发后,业务侧应及时停止 / 清理该模块的运行态资源(定时任务、线程、缓存),避免无授权模块继续跑。业务管理器需做幂等(事件可能多次触发);若联动逻辑较重,监听方法可加 @Async 异步处理。
@ModuleAuth 注解无权限抛 467
hasAuth()无权限自行处理
订阅激活/失效事件
启停业务
选模块常量 → 见 §1.1 FeatureEnum;查错误码 → 见 §9;查完整代码位置 → 见 §8 文件索引。
| 职责 | 文件 |
|---|---|
| 启动 / 硬狗初始化 | src/main/java/com/sctech/opti/cloud/common/auth/dog/SuperDog.java |
| 硬狗回调 | .../common/auth/dog/CallbackImpl.java |
| DLL 接口 / 状态码 | .../common/auth/dog/SuperDogInterface.java、DogStatus.java |
| 全局状态仓库 | .../common/auth/domain/DogEncryptionStorage.java |
| 证书对象 | .../common/auth/domain/Certification.java |
| 功能枚举 | .../common/auth/enums/FeatureEnum.java、BitFeatureEnum.java |
| 软授权业务 | .../modules/system/biz/impl/SysCertificationBizImpl.java |
| RSA 解密 | .../modules/system/utils/RsaUtil.java |
| 机器码 | .../common/auth/utils/MachineUtil.java → .../modules/system/utils/MachineCodeWithOSHI.java |
| 事件监听 | src/main/java/com/sctech/opti/cloud/event/auth/ModuleAuthEventListener.java |
| 激活功能缓存 | .../common/auth/holder/ActiveFeatureHolder.java |
| 激活功能接口 | .../modules/system/controller/FeatureController.java |
| 时间防篡改 | .../common/auth/time/TimeTamperDetector.java |
| 请求拦截器 | .../common/auth/interceptor/WatchDogInterceptor.java、EnergyRouteInterceptor.java |
| 方法切面 | .../common/core/annotation/ModuleAuth.java、.../common/auth/aop/CheckModuleAuth.java |
| 菜单过滤 | .../modules/system/biz/impl/SysMenuBizImpl.java |
| 授权 Controller | .../modules/system/controller/CertificationController.java |
| 职责 | 文件 |
|---|---|
| 区域事件监听 | .../event/area/AreaEventListener.java |
| 项目事件监听(异步 + 事件链) | .../common/project/listener/ProjectAsyncEventListener.java |
| 异步事件监听 | .../modules/energy/biz/impl/EnergyOverviewBizImpl.java(@EventListener @Async) |
| 授权驱动定时任务(carbon 分支范本,可参照) | .../modules/bill/business/task/BillAutoBillTaskManager.java(carbon 分支) |
| 职责 | 文件 |
|---|---|
| 授权 store | src/store/modules/authStore/authStore.ts(+ authStore.d.ts) |
| 功能授权 API | src/api/system/feature.ts |
| 授权信息 API | src/api/system/authorization.ts |
| 路由守卫(切页面刷新) | src/router/index.ts |
| navbar 按钮 | src/layout/components/lay-navbar/index.vue |
| 授权信息弹窗 | src/layout/components/lay-navbar/components/AuthInfo.vue |
| code | 枚举 | 含义 | 触发处 |
|---|---|---|---|
| 451 | WATCH_DOG_INVALID | 加密狗失效,请重新授权 | 三个拦截器,主授权校验失败 |
| 452 | TIME_VALIDATE_FAILED | 服务器时间异常 | WatchDogInterceptor 时间篡改检测 |
| 467 | NOT_HAS_THIS_FUNCTION_ACCESS | 用户无此功能权限 | @ModuleAuth 切面 |
异常经 AuthExceptionHandler(@RestControllerAdvice)统一转成 Result.error。
v1.4(新增 §7「开发使用速查」——注解 / 编程式 / 双事件用法、效果与决策;§4.6.2 改为 carbon 分支实战案例)
适用版本:OptiCloud 2.1.1 · 相关文档:《软硬授权使用指南》(API 使用与 FAQ) · 最后更新:2026-07-29