授权单元与水印.md 25 KB

授权单元与水印

本文引用的文件

  • App.vue
  • license.ts
  • login.ts
  • useWatermark.ts
  • AuthorizationInfoModal.vue
  • authorization/index.vue

目录

  1. 引言
  2. 项目结构定位
  3. 核心组件总览
  4. 架构总览
  5. 详细组件分析
  6. 依赖关系分析
  7. 新增授权字段、扩展水印内容与过期处理示例
  8. 性能与行为特性
  9. 常见问题排查
  10. 结论

引言

本文聚焦桌面客户端前端中“授权单元”和“页面水印”机制。重点说明:

  • App.vue 如何在应用启动后通过 authorizationInfoApi 获取服务端授权信息;
  • license.ts 中的 extractAuthorizationCache 如何解析 authorizationLabel、overDateTime、authorizationUnit,并写入用户页缓存;
  • useWatermark.ts 如何根据 authorizationUnit 动态设置或清除页面水印;
  • 授权缓存提取、过期判断、过期消息生成逻辑;
  • 与登录模块中 checkAuthApi、authorizationInfoApi 的关系;
  • 新增授权字段、扩展水印内容、处理授权过期的实践方式;
  • 常见故障场景及排查方法。

项目结构定位

授权与水印相关代码集中在 Electron 桌面端的前端工程内:

  • 应用入口组件:ai-electron/frontend/src/App.vue
  • 授权工具函数:ai-electron/frontend/src/core/utils/license.ts
  • 登录与系统信息接口:ai-electron/frontend/src/core/api/sys/login.ts
  • 水印 Hook:ai-electron/frontend/src/core/hooks/web/useWatermark.ts
  • 授权信息弹窗:ai-electron/frontend/src/core/layouts/default/setting/components/AuthorizationInfoModal.vue
  • 授权管理页面:ai-electron/frontend/src/plat/views/authorization/index.vue

    graph TB
    App["App.vue<br/>应用根组件"] --> LoginApi["login.ts<br/>authorizationInfoApi / checkAuthApi"]
    App --> License["license.ts<br/>extractAuthorizationCache"]
    App --> Watermark["useWatermark.ts<br/>setWatermark / clear"]
    AuthorizationPage["authorization/index.vue<br/>授权管理页"] --> LoginApi
    AuthorizationPage --> License
    AuthorizationModal["AuthorizationInfoModal.vue<br/>授权信息弹窗"] --> LoginApi
    AuthorizationModal --> License
    

图表来源

  • App.vue:1-85
  • login.ts:1-152
  • license.ts:1-124
  • useWatermark.ts:1-205
  • AuthorizationInfoModal.vue:158-390
  • authorization/index.vue:296-592

章节来源

  • App.vue:1-85
  • login.ts:1-152
  • license.ts:1-124
  • useWatermark.ts:1-205

核心组件总览

组件 职责 关键输出/副作用
App.vue 应用初始化时拉取授权信息,监听授权单元变化,驱动水印显示 调用 authorizationInfoApi、写入 userStore 的 authorizationUnit、调用 setWatermark 或 clear
license.ts 从任意结构的授权响应中提取授权标签、过期时间、授权单元,并提供过期判断和消息生成 extractAuthorizationCache、isAuthorizationExpired、getAuthorizationExpiredMessage、getAuthorizationLabel、isFormalAuthorization
login.ts 定义与后端 /sys/info、/sys/checkAuth 等接口的请求封装 authorizationInfoApi、checkAuthApi、offlineAuthorizationApi 等
useWatermark.ts 在页面 DOM 上绘制可防篡改、自适应尺寸的水印层 setWatermark(str)、clear()、clearAll()
AuthorizationInfoModal.vue 展示当前授权信息,计算是否正式授权、是否过期 使用 extractAuthorizationCache、isAuthorizationExpired
authorization/index.vue 授权管理主流程:加载授权信息、校验授权、更新授权、提示过期 同时使用 checkAuthApi、authorizationInfoApi、isAuthorizationExpired、getAuthorizationExpiredMessage

章节来源

  • App.vue:1-85
  • license.ts:1-124
  • login.ts:1-152
  • useWatermark.ts:1-205
  • AuthorizationInfoModal.vue:158-390
  • authorization/index.vue:296-592

架构总览

整体数据流分为两条主线:

  1. 授权信息加载线:App.vue → authorizationInfoApi → 后端 /sys/info → extractAuthorizationCache → userStore 页面缓存。
  2. 水印渲染线:authorizationUnit 变化 → useWatermark.setWatermark 或 clear → Canvas 生成背景图 → 注入全局水印节点。

    sequenceDiagram
    participant App as "App.vue"
    participant Api as "login.ts authorizationInfoApi"
    participant Server as "后端 /sys/info"
    participant License as "license.ts extractAuthorizationCache"
    participant Store as "userStore 页面缓存"
    participant WM as "useWatermark.ts"
    
    App->>Api: "GET /sys/info"
    Api-->>App: "授权原始响应"
    App->>License: "extractAuthorizationCache(响应)"
    License-->>App: "{ authorizationLabel, overDateTime, authorizationUnit }"
    App->>Store: "写入 authorizationUnit"
    Store-->>App: "authorizationUnit 变化"
    App->>WM: "setWatermark(authorizationUnit) 或 clear()"
    WM-->>WM: "Canvas 生成水印背景并插入 DOM"
    

图表来源

  • App.vue:40-75
  • login.ts:110-120
  • license.ts:100-124
  • useWatermark.ts:120-205

详细组件分析

App.vue:授权信息加载与水印联动

App.vue 是授权单元与水标的协调者:

  • 从 userStore 读取 authorizationUnit 作为计算属性;
  • 如果授权单元为空,且服务器地址可用,则调用 authorizationInfoApi;
  • 将 extractAuthorizationCache(res).authorizationUnit 写回 userStore.setPageCache('authorizationUnit', unit);
  • 监听 authorizationUnit 变化:非空时调用 setWatermark(unit),为空时调用 clear();
  • 请求失败时直接清理水印,避免残留旧水印。

    flowchart TD
    Start["onMounted"] --> CheckUnit["检查 authorizationUnit 是否为空"]
    CheckUnit -->|不为空| WatchUnit["监听授权单元变化"]
    CheckUnit -->|为空| CheckServer["检查服务器地址是否可用"]
    CheckServer -->|不可用| Wait["等待网络就绪"]
    CheckServer -->|可用| CallApi["调用 authorizationInfoApi"]
    CallApi --> Parse["extractAuthorizationCache 解析"]
    Parse --> WriteStore["写入 userStore.authorizationUnit"]
    WriteStore --> WatchUnit
    CallApi --> Error["请求失败"]
    Error --> ClearWm["clear() 清除水印"]
    WatchUnit --> UnitEmpty{"authorizationUnit 是否为空"}
    UnitEmpty -->|是| ClearWm
    UnitEmpty -->|否| SetWm["setWatermark(authorizationUnit)"]
    

图表来源

  • App.vue:40-75

章节来源

  • App.vue:1-85

license.ts:授权缓存提取、过期判断与消息生成

license.ts 的核心目标是把后端返回的结构化或非结构化授权响应,统一成前端可用的三个字段:

  • authorizationLabel:授权标签,如“试用”或正式授权时的空字符串;
  • overDateTime:授权过期时间;
  • authorizationUnit:授权单元,用于水印展示。

解析策略

extractAuthorizationCache 的处理顺序为:

  1. 从 source.data 优先取值;
  2. 若 data 中没有目标字段,则回退到顶层 rawData;
  3. overDateTime 从 overDateTime 或 overdatetime 两个键中匹配;
  4. authorizationUnit 从 unit 键取值;
  5. authorizationLabel 优先从以下位置取值:
    • rawData 本身是字符串时,直接作为标签;
    • data.authorizationLabel、data.authorizationStatus、data.authStatus、data.message;
    • 最后回退到根据 overDateTime 计算的 getAuthorizationLabel。

过期判断

  • getAuthorizationExpirationTime 支持带时区的时间字符串、日期格式、以及包含时分秒的完整时间;
  • isAuthorizationExpired 比较过期时间与当前时间;
  • isFormalAuthorization 通过固定正式授权日期常量判断正式授权;
  • getAuthorizationExpiredMessage 根据是否存在规范化后的 overDateTime 生成中文提示。

    flowchart TD
    Input["输入 source"] --> Resolve["resolveAuthorizationSource<br/>分离 raw/rawData/data"]
    Resolve --> OverDateTime["读取 overDateTime<br/>overDateTime / overdatetime"]
    Resolve --> Unit["读取 authorizationUnit<br/>unit"]
    Resolve --> LabelFallback["读取 authorizationLabel<br/>authorizationLabel / authorizationStatus / authStatus / message"]
    LabelFallback --> StringRaw{"rawData 是否为字符串"}
    StringRaw -->|是| UseString["直接使用 rawData 作为 label"]
    StringRaw -->|否| ComputeLabel["按 overDateTime 计算 label"]
    OverDateTime --> Expire["isAuthorizationExpired / getAuthorizationExpirationTime"]
    Unit --> Output["输出 { authorizationLabel, overDateTime, authorizationUnit }"]
    ComputeLabel --> Output
    UseString --> Output
    

图表来源

  • license.ts:1-124

章节来源

  • license.ts:1-124

login.ts:授权相关接口

login.ts 提供两类授权相关接口:

  • authorizationInfoApi:获取 /sys/info,用于展示授权信息和驱动水印;
  • checkAuthApi:获取 /sys/checkAuth,用于授权拦截判断。

两者都配置了 errorMessageMode: 'none',表示错误不自动弹出默认错误提示,由调用方自行处理。authorizationInfoApi 还关闭响应转换,以便 App.vue 和授权模块能拿到原始结构,再由 extractAuthorizationCache 解析。

graph LR
  Client["前端业务组件"] --> AuthorizationInfo["authorizationInfoApi<br/>/sys/info"]
  Client --> CheckAuth["checkAuthApi<br/>/sys/checkAuth"]
  AuthorizationInfo --> RawResponse["原始 Result 响应"]
  CheckAuth --> AuthCheckResult["授权校验结果"]

图表来源

  • login.ts:100-140

章节来源

  • login.ts:1-152

useWatermark.ts:水印绘制与防篡改

useWatermark 是一个 Vue Hook,负责:

  • 使用 Canvas 将水印文本转为 Base64 背景图;
  • 在指定目标元素(默认 document.body)上插入一个绝对定位的水印层;
  • 使用 MutationObserver 监听 DOM 变更,防止控制台删除或修改水印;
  • 监听窗口尺寸变化,重新计算水印尺寸;
  • 提供 setWatermark(str)、clear()、clearAll() 三个对外方法。

关键行为:

  • setWatermark 会创建水印节点并注册 ResizeListener;
  • clear 会移除水印节点、断开观察者、移除监听;
  • clearAll 会清理所有已注册水印实例;
  • 当父容器被删除时,MutationObserver 会尝试恢复水印节点;
  • 当水印层样式被修改时,会重新执行 setWatermark,实现防篡改。

    classDiagram
    class UseWatermark {
    +setWatermark(str)
    +clear()
    +clearAll()
    -createBase64(str, options)
    -resetWatermarkStyle(element, text, options)
    -updateWatermark(options)
    -createWatermark(str)
    }
    
    class MutationObserver {
    +observe(node, config)
    +disconnect()
    }
    
    class Canvas {
    +toDataURL(type)
    +fillText(text, x, y)
    }
    
    UseWatermark --> MutationObserver : "监听 DOM 变更"
    UseWatermark --> Canvas : "生成水印图片"
    

图表来源

  • useWatermark.ts:1-205

章节来源

  • useWatermark.ts:1-205

AuthorizationInfoModal.vue 与 authorization/index.vue:授权界面联动

这两个组件展示了授权能力在真实页面中的用法:

  • AuthorizationInfoModal.vue 调用 authorizationInfoApi,再用 extractAuthorizationCache 解析,并通过 isAuthorizationExpired 判断是否过期;
  • authorization/index.vue 同时使用 checkAuthApi 做授权拦截判断,并使用 getAuthorizationExpiredMessage 生成过期提示。

    sequenceDiagram
    participant Modal as "AuthorizationInfoModal.vue"
    participant Page as "authorization/index.vue"
    participant Api as "login.ts"
    participant License as "license.ts"
    
    Modal->>Api: "authorizationInfoApi()"
    Api-->>Modal: "授权响应"
    Modal->>License: "extractAuthorizationCache / isAuthorizationExpired"
    Page->>Api: "checkAuthApi()"
    Page->>License: "isAuthorizationExpired / getAuthorizationExpiredMessage"
    

图表来源

  • AuthorizationInfoModal.vue:158-390
  • authorization/index.vue:296-592

章节来源

  • AuthorizationInfoModal.vue:158-390
  • authorization/index.vue:296-592

依赖关系分析

授权与水印的依赖关系可以概括为:

  • App.vue 依赖 login.ts 的 authorizationInfoApi;
  • App.vue 依赖 license.ts 的 extractAuthorizationCache;
  • App.vue 依赖 useWatermark.ts 的 setWatermark 和 clear;
  • authorization/index.vue 和 AuthorizationInfoModal.vue 同时依赖 login.ts 和 license.ts;
  • license.ts 是纯工具模块,不依赖 UI 组件。

    graph TB
    App["App.vue"] --> LoginApi["login.ts"]
    App --> License["license.ts"]
    App --> Watermark["useWatermark.ts"]
    AuthPage["authorization/index.vue"] --> LoginApi
    AuthPage --> License
    AuthModal["AuthorizationInfoModal.vue"] --> LoginApi
    AuthModal --> License
    

图表来源

  • App.vue:1-85
  • login.ts:1-152
  • license.ts:1-124
  • useWatermark.ts:1-205
  • authorization/index.vue:296-592
  • AuthorizationInfoModal.vue:158-390

章节来源

  • App.vue:1-85
  • login.ts:1-152
  • license.ts:1-124
  • useWatermark.ts:1-205

新增授权字段、扩展水印内容与过期处理示例

新增授权字段

假设后端在 /sys/info 返回的新字段名为 enterpriseCode,新增步骤如下:

  1. 在 license.ts 的 extractAuthorizationCache 中增加对 enterpriseCode 的读取;
  2. 如果字段可能出现在 data 或 rawData,复用现有 getAuthorizationValue 的容错逻辑;
  3. 返回对象中增加该字段;
  4. 在需要展示的企业编码位置(例如水印或授权弹窗)使用该字段;
  5. 如果该字段影响授权状态,可在 isAuthorizationExpired 或新的状态函数中扩展判断。

参考路径:

  • license.ts:100-124

扩展水印内容

当前水印文字来自 authorizationUnit。如需同时显示企业编码或授权类型:

  1. 在 App.vue 中组合新的水印文本,例如拼接 authorizationUnit 与新增字段;
  2. 将新文本传给 setWatermark(newText);
  3. 如果水印字体、颜色、旋转角度需要调整,可基于 useWatermark 的选项类型扩展配置;
  4. 注意保持水印文本简洁,避免 Canvas 绘制过大导致性能下降。

参考路径:

  • App.vue:40-75
  • useWatermark.ts:1-80

处理授权过期

当前过期逻辑主要由 license.ts 提供:

  • isAuthorizationExpired 用于判断是否过期;
  • getAuthorizationExpiredMessage 用于生成中文提示;
  • authorization/index.vue 在授权管理页中使用这两个函数;
  • AuthorizationInfoModal.vue 也使用 isAuthorizationExpired 展示过期状态。

建议扩展点:

  • 若需要更细粒度的过期控制,可在 getAuthorizationExpirationTime 中增加自定义规则;
  • 若需要在应用级拦截过期用户,可在路由守卫或全局初始化流程中调用 checkAuthApi 并结合 isAuthorizationExpired 做跳转或提示;
  • 不要仅依赖水印判断授权,因为水印只是展示层,真正权限判断应结合后端接口。

参考路径:

  • license.ts:60-124
  • authorization/index.vue:584-592
  • AuthorizationInfoModal.vue:258-390

性能与行为特性

  • useWatermark 使用 Canvas 生成 Base64 背景图,适合短文本水印;长文本会增加绘图和内存开销;
  • 水印层使用绝对定位和较高 z-index,覆盖整个目标容器;
  • 通过 MutationObserver 监听 DOM 变更,存在一定监听成本,但用于防篡改是必要代价;
  • App.vue 仅在 authorizationUnit 为空时才请求授权信息,减少重复请求;
  • authorizationInfoApi 超时时间为 10 秒,避免长时间阻塞应用启动;
  • 授权信息接口禁用自动错误提示,便于上层统一处理;
  • 水印在组件卸载时会尝试清理,避免多实例残留。

[本节为通用行为说明,未直接分析具体代码片段]

常见问题排查

授权信息未加载

现象:页面启动后没有水印,userStore 中也没有 authorizationUnit。
可能原因:

  • 服务器地址缺失或未配置,isServerSetupMissing() 或 getApiOrigin() 返回无效值;
  • /sys/info 接口返回空对象或结构不符合预期;
  • 网络请求失败,catch 分支调用了 clear() 清理水印;
  • 后端返回的 unit、overDateTime、authorizationLabel 字段名不一致。

排查步骤:

  1. 检查网络请求是否成功访问 /sys/info;
  2. 打印 authorizationInfoApi 返回的原始响应;
  3. 确认后端返回结构中是否存在 data.unit、data.overDateTime、data.authorizationLabel 或其兼容键;
  4. 检查 App.vue 中 ensureAuthorizationUnit 是否进入 try 分支;
  5. 检查 license.ts 的 extractAuthorizationCache 是否能正确解析返回值。

参考路径:

  • App.vue:40-75
  • login.ts:110-120
  • license.ts:100-124

水印不显示

现象:授权单元已有值,但页面没有水印。
可能原因:

  • authorizationUnit 为空字符串或空白字符;
  • setWatermark 没有被调用;
  • 目标容器不存在或已被销毁;
  • 水印层被其他样式覆盖或被手动删除;
  • useWatermark 的 MutationObserver 被异常中断。

排查步骤

  1. 检查 userStore.getPageCacheByKey('authorizationUnit') 的值;
  2. 在 watch(authorizationUnit) 中添加日志,确认是否触发 setWatermark 或 clear;
  3. 打开浏览器开发者工具,查看是否存在带水印类名的 DOM 节点;
  4. 检查是否有第三方样式覆盖了 z-index 或 visibility;
  5. 检查控制台是否出现 MutationObserver 相关错误。

参考路径:

  • App.vue:60-75
  • useWatermark.ts:1-205

授权过期仍可使用

现象:overDateTime 已过期,但用户仍可继续使用。
重要区分:

  • 水印只是展示层,不能代替权限校验;
  • 真正的授权拦截应使用 checkAuthApi 与 isAuthorizationExpired;
  • authorization/index.vue 明确说明其判定方式与合并前一致,即 checkAuthApi() + isAuthorizationExpired()。

排查步骤

  1. 确认过期判断使用的是 isAuthorizationExpired;
  2. 检查是否只依赖水印或 authorizationLabel 做拦截;
  3. 检查 checkAuthApi 是否返回允许继续使用的结果;
  4. 检查授权管理页面是否正确调用 getAuthorizationExpiredMessage 并提示用户;
  5. 检查是否在后端网关或路由守卫中也实现了相同过期策略。

参考路径:

  • license.ts:60-124
  • authorization/index.vue:584-592
  • login.ts:130-140

localStorage 缓存不一致

现象:本地缓存中的 authorizationUnit 与实际后端返回不一致。
可能原因:

  • 用户手动修改了 localStorage;
  • 多次导入授权后,前端未刷新缓存;
  • 不同页面或模块各自维护缓存,但没有统一更新;
  • userStore.setPageCache 写入后,其他组件未重新读取。

处理建议

  1. 以 App.vue 启动时拉取的授权信息为准;
  2. 导入授权成功后,显式更新 userStore.setPageCache('authorizationUnit', unit);
  3. 如果需要持久化,确保只在一个入口处写入;
  4. 调试时可清空 localStorage 中相关键,观察应用是否重新拉取;
  5. 不要在多个地方随意覆盖同一缓存键。

参考路径:

  • App.vue:40-60

授权信息与网关 token 不一致

现象:前端授权信息显示正常,但网关拒绝请求或提示无权限。
可能原因:

  • 前端使用 /sys/info 展示授权信息,而网关可能基于 token 或会话状态做权限校验;
  • 用户切换了账号、租户、企业或角色;
  • 后端授权状态变更后,前端缓存未刷新;
  • 网关与服务端授权数据库不同步。

处理建议

  1. 不要将水印或前端授权标签视为网关权限依据;
  2. 检查登录态、token、会话是否与当前授权主体一致;
  3. 在授权变更后,考虑退出登录或刷新会话;
  4. 检查 authorization/index.vue 中授权更新流程是否同步了必要的认证上下文;
  5. 如果问题持续,结合后端日志检查 /sys/checkAuth、网关鉴权和授权库一致性。

参考路径:

  • login.ts:100-140
  • authorization/index.vue:296-592

结论

授权单元与水印机制在前端工程中形成了清晰的职责分工:

  • login.ts 负责与后端交互;
  • license.ts 负责解析授权数据、判断过期和生成提示;
  • App.vue 负责启动时拉取授权信息、写入缓存并驱动水印;
  • useWatermark.ts 负责在 DOM 层绘制稳定、可恢复的水印;
  • 授权管理页面和授权弹窗负责向用户展示授权状态和过期信息。

实际权限控制不应依赖水印。水印是可视化手段,真正的授权判断应结合 checkAuthApi、后端接口和用户会话。新增授权字段时,应优先扩展 license.ts 的解析逻辑,再在需要的 UI 层消费新字段,并确保授权过期路径在所有相关页面保持一致。