面向读者

后端开发、前端开发、架构评审。本文聚焦授权机制的运行时全链路架构(当前已实现):授权如何加载、如何作用于业务(请求 / 方法 / 菜单 / 按钮)、授权变化事件如何传播(进程内事件 + 缓存)、前端如何感知联动。

与《软硬授权使用指南》互补——后者是面向运维 / 使用者的 API 使用与 FAQ;本文是面向开发的机制与代码链路。

0
一张图看懂授权机制
授权来源(两种载体)
硬件加密狗 SuperDog
Windows DLL · 20s 轮询 · CallbackImpl.invoke() 写 hardSortCertMap
软授权码(RSA 密文)
refreshSoftCertification() 写 softSortCertMap
DogEncryptionStorage全局静态 · JVM 内存
hardSortCertMap硬件狗授权状态
softSortCertMap软授权状态
key 规则:模块/主授权 → superDog+featureId | 点位授权 → superDogPoint&&+featureId
①
请求拦截器
WatchDog /** · EnergyRoute /energy /view
⑤
点位上限校验
新增点位 ≤ 授权容量 · 80% 容量预警
GET
授权信息查询接口
GET /system/certification/getCert
②方法切面 @ModuleAuth + CheckModuleAuth —— 计费(51)/大屏(52)/组态(26) 方法级 → 无权限抛 467
③编程式 hasAuth() —— 定时任务无 EMS 权限则跳过
④菜单/按钮 SysMenuBizImpl —— authorizeFeatureId 显式匹配 + menuSource 间接匹配 → 过滤菜单树 + 按钮权限串
事件驱动缓存刷新链路
ModuleAuthActivatedEvent (soft/hard) 发布 → ModuleAuthEventListener 差集 → ModuleAuthExpiredEvent(失效时) → ActiveFeatureHolder.refresh() → GET /system/feature/active → 前端 authStore

授权从「载体 → 内存 → 业务校验 / 缓存 / 前端」是一条完整的单向链路。

1
核心数据结构
1.1 FeatureEnum —— 功能授权枚举

common/auth/enums/FeatureEnum.java。每个枚举值 = 一个可独立授权的功能模块。

枚举常量featureIdfeatureNameshow说明
EASY_PRO_SERVICE99基础平台授权true主授权,必须拥有,否则全平台不可用
MODULE_ENERGY_SERVICE21能耗模块授权trueEMS 能耗管理
MODULE_ENERGY_BILL_SERVICE51能耗计费模块授权true能耗计费
MODULE_VIEW_SERVICE26监控系统授权true应用组态
MODULE_BASE_VIEW_SERVICE61可视化基础授权false可视化基础(不展示)
MODULE_BIG_SCREEN_SERVICE52大屏设计模块授权true大屏设计器
MODULE_DEVICE_SCREEN_SERVICE53设备组态模块授权false设备组态(不展示)
MODULE_APP_SERVICE54APP客户端模块授权true移动端 APP
MODULE_ZHIWEN_SERVICE55AI智问模块授权trueAI 智能问答
POINTS_LIMIT_BEGIN100点位授权true点位授权区间起点
POINTS_LIMIT_END201点位授权true终点;= 201 表示无点位限制
要点
  • featureId < 100 为模块/主授权;100 ≤ featureId ≤ 201 为点位授权档位。
  • show=false 的模块(61/53)不出现在授权信息列表(FeatureEnum.isShow() 过滤),但仍参与权限判断。
  • 前端判断权限统一用枚举常量名 name(如 "MODULE_ZHIWEN_SERVICE"),而非中文 featureName(文案可能变动)。FeatureEnum.getName() 返回该常量名。
1.2 BitFeatureEnum —— 二进制位编码

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() 按每个组合调用 DLL Init)。
1.3 Certification —— 证书对象(内存模型)

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)
1.4 DogEncryptionStorage —— 全局授权状态仓库

common/auth/domain/DogEncryptionStorage.java。静态类,两个 ConcurrentHashMap:

java
public static final Map<String, Certification> hardSortCertMap = new ConcurrentHashMap<>(); // 硬件狗
public static final Map<String, Certification> softSortCertMap = new ConcurrentHashMap<>(); // 软授权
key 规则(硬狗与软狗分属不同 Map,前缀相同不冲突)
授权类别key 格式示例
主/模块授权superDog + featureIdsuperDog99、superDog21
点位授权superDogPoint&& + featureIdsuperDogPoint&&105
核心判定方法
  • hasAuth(featureId) / hasAuth(FeatureEnum):是否拥有某模块授权
  • hasAuthFeatureIdList():合并 hard+soft 两个 Map,经 auth() 时间过滤,返回当前有效 featureId 集合
  • auth(certification, now):点位授权(featureId≥100)直接放行不参与时间判定;模块授权校验 begin/endTime 有效期
  • authPoint():计算允许点位数(详见 §5)
2
授权加载(来源 → 内存)

系统启动入口:SuperDog.execute()(common/auth/dog/SuperDog.java:141),由 InitializationManager 在 Spring 启动阶段调用,顺序执行三件事:

java
public void execute(ApplicationArguments args) {
    sysCertificationBiz.refreshSoftCertification();  // 1. 软授权
    init();                                          // 2. 硬狗(try-catch 不阻塞启动)
    TimeTamperDetector.init();                       // 3. 时间防篡改
}
启动加载流程
Spring 启动 · InitializationManager 调用 SuperDog.execute()
① 软授权 refreshSoftCertification()
清空 softSortCertMap → 读 DB 主授权密文 → RSA 公钥解密 → 解析明文 → 逐条写入 Map → finally 发 ModuleAuthActivatedEvent("soft")
② 硬狗 SuperDog.init()(try-catch 不阻塞启动)
仅 Windows 加载 SuperDog.dll;枚举所有授权组合调 DLL Init → SetInterval(dogID, 20*1000) + SetCallBack
③ 时间防篡改 TimeTamperDetector.init()
启动虚拟线程每 5 秒校验系统时间,发现时间回退 > 1 小时则判定篡改 → 关闭校验开关 → 拦截所有请求(452)
2.1 硬件加密狗(SuperDog)
  • 平台限制:仅 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)
    1. BitFeatureEnum.getFeatureIdsByAuthNum(featureNum) 按位拆出 featureId 集合
    2. 按 code 分支:code=0/45(成功)→ 按 type(1=永久,2/3=限期)构造 Certification 写入 hardSortCertMap;异常码 → remove 对应 key(拔狗即移除授权)
    3. 末尾发布 ModuleAuthActivatedEvent("hard")
  • 要点:硬件狗不经 RSA 解密,狗本身就是授权载体,DLL 直接返回 (code, type, datetime)。
2.2 软授权码(RSA)

SysCertificationBizImpl.refreshSoftCertification()(modules/system/biz/impl/SysCertificationBizImpl.java:117):

  1. softSortCertMap.clear() 清空
  2. 从 DB 读主授权记录(featureId=99 且 type=software_cert)的 cipherText
  3. RsaUtil.rsaDecToBean(cipherText) —— RSA 公钥解密(授权工具用私钥加密,后台用公钥解密)
  4. 解密明文格式:机器码#cloud#开始时间#结束时间#授权组合数字
  5. 解析出主授权 + 各模块/点位,逐条写入 softSortCertMap
  6. finally 块发布 ModuleAuthActivatedEvent("soft")
用户导入授权码(addCertification(),行 64)完整链路:

前端提交 cipherText → RSA 解密 → 校验含主授权(99) → 校验机器码匹配 → 校验时间合法 → 覆盖写 DB → refreshSoftCertification() 重建内存 → 发事件

2.3 时间防篡改(TimeTamperDetector)

common/auth/time/TimeTamperDetector.java:

  • 启动一个名为 "time-check" 的虚拟线程,每 5 秒校验一次系统时间
  • 用 AES 加密的时间戳日志文件 log.bin 记录上次时间;若发现时间回退超过 1 小时 → 判定篡改 → timeCheckEnabled.set(false)
  • 每个请求由 WatchDogInterceptor 先查 isTimeCheckEnabled(),被判定篡改后所有请求被拦截(抛 code 452)
安全设计

时间防篡改是授权有效期校验的前置保障,防止用户通过回拨系统时间绕过授权过期。一旦触发,整个平台请求被阻断,需排查 log.bin 后人工恢复。

3
授权作用于业务(四层校验)

授权状态写入 DogEncryptionStorage 后,通过以下四层作用于业务运行时:

DogEncryptionStorage 授权状态
hardSortCertMap + softSortCertMap 已就绪,向下分发至四层校验
四层业务校验
①
请求拦截器
主授权 99 全平台/能源路径
②
方法切面
@ModuleAuth 模块级
③
编程式
hasAuth() 定时任务
④
菜单/按钮过滤
SysMenuBizImpl
3.1 第一层:请求拦截器(主授权)
拦截器拦截路径校验对象失败处理
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 生效。

授权拦截器在整个鉴权链中的位置(AuthInterceptorConfig 注册顺序)
授权拦截器在整个鉴权链中的位置(AuthInterceptorConfig 注册顺序)
ContextInterceptor
清线程缓存(前置)
WatchDogInterceptor
★ 主授权校验(本节)
EnergyRouteInterceptor
★ /energy、/view 主授权校验(本节)
UserClientTokenInterceptor ×2
B端/C端 token 校验(区分 /wx/** 与平台端)
UserAccountInterceptor
token 校验
UserRoleInterceptor
接口/菜单角色权限
DataScopeInterceptor
数据权限
ProjectScopeInterceptor
项目权限(project_id)
前置门槛

授权校验(前 3 个)是整条链的前置门槛:主授权失效时后续拦截器根本不会执行(已 logout + 抛异常)。

3.2 第二层:方法切面 @ModuleAuth(模块级)

注解 @ModuleAuth(FeatureEnum.XXX) + 切面 CheckModuleAuth:

java
@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
3.3 第三层:编程式判断 hasAuth()
  • modules/energy/task/EnergyCalculateTask.java:103 —— 无 EMS 权限则不执行能耗计算
  • modules/energy/task/ScheduleTask.java:60 —— 无 EMS 权限则跳过阈值告警
3.4 第四层:菜单/按钮过滤(SysMenuBizImpl)

SysMenu 实体有 authorizeFeatureId 字段(菜单/按钮绑定的 featureId)和 menuSource(菜单类型)。

getMenuListForCurUser()(SysMenuBizImpl.java:86)两个过滤维度:

  • 显式匹配:菜单 authorizeFeatureId 必须在当前激活集合内
  • 间接匹配(isHasPermission,按 menuSource):APP 类型需 26、SCREEN 需 52、SYSTEM 需 53

getCurUserPermissions()(行 1480)同理过滤按钮,返回 perms 权限标识串供前端做按钮级控制。

前后端协同

后端已按授权过滤菜单树 / 路由 / 按钮权限,前端拿到的即是过滤后的结果。

4
授权变化事件链路(事件 + 缓存)

这是「授权变化 → 业务 / 前端感知」的核心机制。

4.1 两个 Spring 事件

定义在 event/auth/ModuleAuthEventListener.java 内部:

事件携带数据发布时机
ModuleAuthActivatedEventsource(soft/hard) + activeFeatures(构造时快照)软授权刷新后(SysCertificationBizImpl:176);硬狗回调后(CallbackImpl:137)
ModuleAuthExpiredEventexpiredFeatures(失效差集)仅由 Listener 内部计算差集后派发
4.2 发布点
外部发布激活事件 ModuleAuthActivatedEvent(2 处,业务方触发)
  • A. 软授权:SysCertificationBizImpl.refreshSoftCertification() 的 finally(行 176)—— 无论成功 / 清空 / 失败都发一次
  • B. 硬狗:CallbackImpl.invoke() 末尾(行 137)—— 每次硬狗状态回调解析完都发
内部派发失效事件 ModuleAuthExpiredEvent(1 处,Listener 自身触发)
  • C. 失效差集:ModuleAuthEventListener.onModuleAuthActivated() 内部(行 119)—— 对比 lastActiveFeatureIds 求差集,差集非空才发。失效事件不会被业务方直接发布,统一由 Listener 计算后转发。
4.3 ModuleAuthEventListener 处理逻辑
java
@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 保证并发安全。
4.4 ActiveFeatureHolder 缓存

common/auth/holder/ActiveFeatureHolder.java:

  • private volatile List<FeatureEnum> activeFeatures —— volatile 引用 + 不可变 List,读无锁、写 synchronized
  • @PostConstruct init() —— 启动预热一次(幂等)
  • refresh() —— 从 hasAuthFeatureIdList() 重算并整体替换引用
  • 触发方:ModuleAuthEventListener.handleModuleAuthActivated/Expired() 两处
4.5 查询接口

FeatureController(GET /system/feature/active)直接读 ActiveFeatureHolder 缓存返回 List<FeatureEnum>(序列化为对象数组,含 name/url/featureId/featureName/show)。不实时计算,返回事件刷新后的快照。

4.6 事件传播路径(当前已实现)

本节说明授权激活 / 失效事件在当前实现中如何传播,以及前端如何感知。

4.6.1 当前传播链路

授权事件是纯进程内 Spring 事件,完整传播路径如下:

授权变化 · 发布 ModuleAuthActivatedEvent("soft"/"hard")
触发源:软授权 refreshSoftCertification finally | 硬狗 CallbackImpl.invoke() 末尾
ModuleAuthEventListener.onModuleAuthActivated()synchronized
1
handleModuleAuthActivated() → ActiveFeatureHolder.refresh()刷新本地缓存
2
计算 lastActiveFeatureIds 差集
3
差集非空 → 发布 ModuleAuthExpiredEvent
└─ onModuleAuthExpired() → ActiveFeatureHolder.refresh()
4
更新 lastActiveFeatureIds
进程内传播到此为止,无 WebSocket / MQ / 告警等主动推送出口
前端被动感知
下次切页面时路由守卫触发 getFeatureList() → GET /system/feature/active 读 ActiveFeatureHolder 缓存 → 更新按钮/菜单显隐
要点
  • 事件的唯一副作用是刷新 ActiveFeatureHolder 本地缓存(ModuleAuthEventListener.java:141-154)。
  • 前端不会被实时通知,需等到下一次路由切换(路由守卫调 getFeatureList())或用户主动操作(如授权成功回调里立即调 getFeatureList(),见 §6.4)才会拉取最新状态。
  • 事件不跨进程 / 实例传播(授权状态本身存于 JVM 静态 DogEncryptionStorage,是单进程模型)。
4.6.2 如何使用授权事件(订阅 + 业务联动)

授权事件基于 Spring 的 ApplicationEvent 机制。两个事件类型定义在 ModuleAuthEventListener 内部:

事件类型触发时机携带数据
ModuleAuthActivatedEvent软授权刷新后 / 硬狗状态回调后source(soft/hard) + activeFeatures(当前激活全集)
ModuleAuthExpiredEvent激活事件触发后,检测到有失效项时expiredFeatures(本次失效差集)
当前 2.1.1 的处理位置

ModuleAuthEventListener 自身在 handleModuleAuthActivated/Expired 中调用 activeFeatureHolder.refresh() 刷新缓存(ModuleAuthEventListener.java:141-154)。这是事件的默认消费者。

业务联动写法(参考 carbon 分支真实案例 —— 授权驱动定时任务启停)

授权事件最常见的实战用途是:某个模块被激活 / 失效时,自动启停该模块的后台任务。以下示例取自 carbon 分支的账单模块(MODULE_ENERGY_BILL_SERVICE 被激活时启动自动账单 / 归档 / 关单 / 欠费告警定时任务,失效时移除)。

在 ModuleAuthEventListener 的两个 handle* 方法里,按 FeatureEnum 判断并联动业务:

java · ModuleAuthEventListener.java(参考 carbon 分支的真实联动写法)
@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)完整实现如下,可作为「授权驱动定时任务生命周期」的范本:

java · BillAutoBillTaskManager.java(carbon 分支范本)
@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_SERVICE
BillAutoBillTaskManager.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 写法)。

5
点位授权(authPoint)

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 边界不一致)

写入点位授权 key 时,硬狗与软授权的边界判断条件不同,存在一处潜在不一致,排查点位计数异常时需留意。

来源边界条件featureId=100 落点写入条数
硬狗 CallbackImpl.java:78featureId >= 100 && <= 201走点位 key(superDogPoint&&100)同时写两条(superDog100 + superDogPoint&&100)
软授权 SysCertificationBizImpl.java:166featureId > 100 && <= 201走模块 key(superDog100)只写一条(superDog100)

即 featureId=100(0 点位档位)在硬狗下会进 superDogPoint&& 检索范围、在软授权下不会。实践中 100 档位(0 点)极少单独出现,影响有限,但排查点位计数异常时需留意此差异。

6
前端联动机制
6.1 authStore(Pinia)

font_end/src/store/modules/authStore/authStore.ts:

成员作用
state.authInfogetCert() 返回的授权详情(CertVo)
state.featureListgetActiveFeatures() 返回的激活功能列表
getter hasFeature(name)按枚举常量名判断(item.name === featureName)
action getAuthInfo()拉 /system/certification/getCert
action getFeatureList()拉 /system/feature/active,失败不置空(避免误隐藏已授权功能)
6.2 每次切页面刷新

font_end/src/router/index.ts:168,已有 router.beforeEach 守卫登录态校验通过后:

typescript
if (Cookies.get(multipleTabsKey) && userInfo) {
  useAuthStore().getFeatureList();  // 每次切页面刷新激活功能授权
  ...
}

不阻塞导航,保证后端缓存变化后前端下一次切页面即可同步。

6.3 navbar 按钮 v-show 控制

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 报错,已修复)
6.4 授权信息弹窗(AuthInfo.vue)

font_end/src/layout/components/lay-navbar/components/AuthInfo.vue:

  • 「大模型配置」Tab:v-if="showZhiwen"(无智问授权则不显示该 Tab)
  • 软件 / 硬件授权卡片列表:无智问授权时过滤掉 name 含"智问"的卡片
  • 授权成功回调(submit()):除 getAuthInfo() 外立即调用 getFeatureList(),使按钮 / Tab 即时按新权限显隐,无需等下次切页面
6.5 端到端联动(软授权导入)
前端
用户在 AuthInfo.vue 输入授权码 → POST /system/certification/addCertification
后端
RSA 解密校验 → 写 DB → refreshSoftCertification()
发 ModuleAuthActivatedEvent("soft") → ModuleAuthEventListener
ActiveFeatureHolder.refresh() [缓存更新]
前端
submit 成功回调:getAuthInfo() + getFeatureList()
GET /system/feature/active 读到新缓存 → authStore.featureList 更新 → navbar 按钮 / 大模型 Tab 即时显隐

硬件狗插拔:CallbackImpl 回调 → 发 ModuleAuthActivatedEvent("hard") → 缓存刷新 → 前端下次切页面 getFeatureList() 同步。

7
开发使用速查
面向开发者

本章是「拿到授权后,业务代码该怎么用」的速查。授权机制对外暴露三种使用方式:方法注解、编程式判断、Spring 事件。前两种用于「校验当前请求 / 调用是否被允许」,第三种用于「授权状态变化时联动业务」。详细机制见 §3(业务校验)与 §4(事件链路),本章只讲怎么写、什么效果。

三种方式怎么选
方式适用场景触发条件无权限时详见
① @ModuleAuth 注解整个 Controller / 方法需要某模块授权才能访问请求进入方法前直接抛 AuthException(467)§7.1
② hasAuth() 编程式方法内部部分逻辑按权限分支(定时任务、降级逻辑)代码显式调用由代码自行决定(通常静默跳过)§7.2
③ Spring 事件模块被激活 / 失效时启停业务(定时任务、缓存)授权状态变化时不校验,只通知§7.3 / §7.4
一句话决策

「能不能访问这个接口?」→ 注解;「这段逻辑要不要执行?」→ 编程式;「授权变了我要做什么?」→ 事件。

7.1 @ModuleAuth 注解(接口级授权校验)

在 Controller 方法 / 类上加注解 @ModuleAuth(FeatureEnum.XXX),由切面 CheckModuleAuth 拦截:进入方法前先查 DogEncryptionStorage.hasAuth(),无授权直接抛异常,业务方法不会执行。

java · 用法
@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 编程式判断容量。

7.2 编程式 hasAuth()(方法内按权限分支)

需要在方法内部按权限决定「要不要执行某段逻辑」时,直接调静态方法 DogEncryptionStorage.hasAuth(FeatureEnum.XXX)。不会抛异常,由调用方自行决定处理方式(通常用于定时任务、降级逻辑、点位容量校验)。

java · 用法
// 场景一:定时任务——无 EMS 授权则跳过执行
if (!DogEncryptionStorage.hasAuth(FeatureEnum.MODULE_ENERGY_SERVICE)) {
    return;   // 静默跳过,不抛异常
}
energyCalculateBiz.calculate();

// 场景二:启动时——无授权则不初始化该模块任务
if (!DogEncryptionStorage.hasAuth(FeatureEnum.MODULE_ENERGY_BILL_SERVICE)) return;
startAutoTask();
主要 API
方法返回用途
hasAuth(FeatureEnum)boolean是否拥有某模块授权(最常用)
hasAuth(featureId)boolean同上,传 int
hasAuthFeatureIdList()Set<Integer>当前全部有效 featureId(已时间过滤)
authPoint()int允许点位数(-1 表示无限制,见 §5)
注解 vs 编程式

注解是「拦截式」(无权限直接拒绝、抛 467),编程式是「询问式」(无权限时你自己决定怎么办)。接口入口用注解;方法内部分逻辑、定时任务、点位校验用编程式。

7.3 ModuleAuthActivatedEvent(激活事件)

当某模块从无授权变为有授权(或软授权重新刷新)时发布。典型用途:模块被激活 → 自动启动该模块的后台任务 / 缓存。

属性说明
触发时机软授权 refreshSoftCertification() finally;硬狗 CallbackImpl.invoke() 末尾
携带数据source(soft/hard) + activeFeatures(当前激活全集 List<FeatureEnum>)
判断方式activeFeatures.contains(目标模块) —— 因为是全集,判断「是否包含」即可

订阅写法(在 ModuleAuthEventListener.handleModuleAuthActivated 内联动,或在任意 @Component 用 @EventListener 独立订阅):

java · 订阅激活事件
// 方式一:在 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 分支账单任务启停)。

7.4 ModuleAuthExpiredEvent(失效事件)

当某模块从有授权变为无授权时发布(如拔狗、导入新授权码后旧模块消失、过期)。典型用途:模块失效 → 自动停止该模块的后台任务 / 释放资源。

属性说明
触发时机由 ModuleAuthEventListener 对比 lastActiveFeatureIds 差集后派发(差集非空才发)
携带数据expiredFeatures(本次失效差集 List<FeatureEnum> —— 列表里的就是要处理的)
判断方式expiredFeatures.contains(目标模块) —— 差集即「本次失效项」,命中即需处理
首次激活不产生失效事件(基准 lastActiveFeatureIds 初始为空,差集恒空)

订阅写法:

java · 订阅失效事件
// 在 ModuleAuthEventListener.handleModuleAuthExpired 里联动
private void handleModuleAuthExpired(List<FeatureEnum> expiredFeatures) {
    activeFeatureHolder.refresh();
    if (expiredFeatures.contains(FeatureEnum.MODULE_ENERGY_BILL_SERVICE)) {
        billAutoBillTaskManager.removeAutoTask();   // 移除账单定时任务
    }
}
效果与注意

失效事件触发后,业务侧应及时停止 / 清理该模块的运行态资源(定时任务、线程、缓存),避免无授权模块继续跑。业务管理器需做幂等(事件可能多次触发);若联动逻辑较重,监听方法可加 @Async 异步处理。

7.5 速查决策
业务代码要怎么用授权?
先问自己:是要「拦截请求」、还是要「按权限分支」、还是要「响应授权变化」
三种决策路径
整个接口要授权
@ModuleAuth 注解
无权限抛 467
方法内分支判断
hasAuth()
无权限自行处理
响应授权变化
订阅激活/失效事件
启停业务
配套速查

选模块常量 → 见 §1.1 FeatureEnum;查错误码 → 见 §9;查完整代码位置 → 见 §8 文件索引。

8
关键文件索引
8.1 后端(D:\project\OptiCloud_2.0\opticloud)
职责文件
启动 / 硬狗初始化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 分支)
8.2 前端(D:\project\2.1.1_front\font_end)
职责文件
授权 storesrc/store/modules/authStore/authStore.ts(+ authStore.d.ts)
功能授权 APIsrc/api/system/feature.ts
授权信息 APIsrc/api/system/authorization.ts
路由守卫(切页面刷新)src/router/index.ts
navbar 按钮src/layout/components/lay-navbar/index.vue
授权信息弹窗src/layout/components/lay-navbar/components/AuthInfo.vue
9
关键错误码
code枚举含义触发处
451WATCH_DOG_INVALID加密狗失效,请重新授权三个拦截器,主授权校验失败
452TIME_VALIDATE_FAILED服务器时间异常WatchDogInterceptor 时间篡改检测
467NOT_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