许可证与认证问题排查.md 29 KB

许可证与认证问题排查

本文引用的文件

  • LicenseUtil.java
  • PathConst.java
  • SystemService.java
  • SaTokenWebConfig.java
  • LoginActivityService.java
  • GlobalExceptionHandler.java
  • StickyRouteFilter.java
  • LoginRequestFilter.java
  • LoginResponseFilter.java
  • TokenResolver.java
  • NodeHealthRegistry.java
  • GatewayErrorHandler.java
  • application.yaml
  • env.sh
  • gateway.sql

目录

  1. 引言
  2. 项目结构与排查边界
  3. 快速诊断清单
  4. 架构总览:许可证、认证与网关转发链路
  5. 场景一:regInfo.exe 无法执行或返回异常
  6. 场景二:机器码不一致
  7. 场景三:许可证过期或授权单元为空
  8. 场景四:x-token 丢失或与其他节点不一致导致 401/403
  9. 场景五:Gateway 未绑定节点或节点健康检查失败
  10. 场景六:账号被顶下线后仍持有旧 token
  11. 场景七:服务端重启后会话丢失
  12. 数据库字段与配置项对照表
  13. 常见问题 FAQ
  14. 结论

引言

本指南面向运维和后端工程师,聚焦以下故障面:

  • 本地离线授权相关:regInfo.exe(Linux 为 regInfo)不可执行、命令返回异常、机器码变化。
  • 授权状态:许可证文件缺失、授权过期、授权单元为空。
  • 认证与网关:x-token 缺失、过期、被单端登录顶掉;Gateway 粘性路由、节点绑定、健康检查。
  • 会话持久化:Redis 作为 Sa-Token 存储时,服务端重启导致的“已登录状态消失”。

阅读顺序建议:先看「快速诊断清单」定位大类,再进入对应场景,最后用「数据库字段与配置项对照表」核对落地数据。

项目结构与排查边界

本系统由三层组成:

  • 前端 Electron 应用:发起登录、上传授权文件、携带 x-token 访问后端。
  • Gateway:负责用户粘性路由、x-token 解析、节点选择与健康检查。
  • ai-server:业务服务,负责 Sa-Token 登录态、许可证校验、业务接口。

    graph TB
    Client["客户端<br/>Electron 前端"] --> GW["AI 网关<br/>StickyRouteFilter / LoginResponseFilter"]
    GW --> NodeA["后端节点 A<br/>ai-server"]
    GW --> NodeB["后端节点 B<br/>ai-server"]
    GW -.-> Redis["共享 Redis<br/>Sa-Token / Token 缓存"]
    NodeA -.-> DB["PostgreSQL<br/>gateway_* 表 + 业务表"]
    NodeB -.-> DB
    NodeA -.-> License["本地授权文件<br/>QingJian/license/license.xlts"]
    NodeA -.-> RegInfo["regInfo.exe / regInfo"]
    

图表来源

  • StickyRouteFilter.java:1-180
  • LoginResponseFilter.java:1-176
  • TokenResolver.java:1-100
  • application.yaml:1-81
  • PathConst.java:1-110

章节来源

  • StickyRouteFilter.java:1-180
  • application.yaml:1-81
  • PathConst.java:1-110

快速诊断清单

步骤 检查点 正常期望 异常信号
1 能否访问 Gateway 暴露地址 HTTP 200,后端健康接口返回成功 404、连接超时、502
2 是否先走 /js/a/sys/checkAuth 返回授权状态 返回“未授权”“文件不存在”
3 是否能获取机器码 返回机器码字符串 返回“获取机器码失败”
4 授权文件是否存在 QingJian/license/license.xlts 存在 提示“授权文件不存在”
5 regInfo.exe 或 regInfo 是否可执行 子进程返回 0 子进程非 0、权限不足、找不到文件
6 登录请求是否带 username 请求体含 username 登录按匿名轮询,可能绑错节点
7 登录后是否返回 x-token 响应包含 token 无 token,后续请求全部 401
8 Redis 中是否有 Sa-Token x-token 能解析出 userId 解析失败、登录失效
9 Gateway 中是否有用户绑定 gateway_user_route 有记录 503 “用户未绑定节点”
10 绑定节点是否健康 node_health 不是 DOWN 503 “节点不可用”
11 多节点 token 是否一致 同一用户所有节点 token 相同 某节点 401,另一节点正常

架构总览:许可证、认证与网关转发链路

许可证校验发生在 ai-server 启动和业务调用前;认证使用 Sa-Token,并依赖 Redis;Gateway 只读 token,不签发 token,同时维护用户到节点的粘性绑定。

sequenceDiagram
    participant C as "客户端"
    participant G as "Gateway"
    participant S as "ai-server"
    participant R as "Redis"
    participant D as "PostgreSQL"

    C->>G: POST /js/a/auth/login {username, password}
    G->>S: 转发登录请求
    S->>R: Sa-Token 登录并写入 x-token
    S-->>G: 登录成功 Result{code=200, data.token}
    G->>D: 写 gateway_user_route(用户→节点)
    G->>R: 写 gateway:token:{serviceId}:{token} → userId
    G-->>C: 登录响应原样透传

    C->>G: 业务请求(携带 x-token)
    G->>R: 解析 token → userId
    G->>D: 查询用户绑定节点
    G->>S: 转发到绑定节点
    S-->>G: 业务结果
    G-->>C: 返回结果

图表来源

  • LoginRequestFilter.java:1-120
  • LoginResponseFilter.java:1-176
  • StickyRouteFilter.java:1-180
  • TokenResolver.java:1-100
  • gateway.sql:1-107

章节来源

  • SaTokenWebConfig.java:1-65
  • LoginActivityService.java:1-106
  • TokenResolver.java:1-100

场景一:regInfo.exe 无法执行或返回异常

复现步骤

  1. 部署 ai-server,工作目录指向仓库根。
  2. 确认 QingJian/license/regInfo.exe(Windows)或 QingJian/license/regInfo(Linux)存在。
  3. 调用 /sys/code 或 /sys/checkAuth,观察是否抛出激活异常。
  4. 在 Linux 上检查 QingJian/license 目录及 license.xlts 的读写权限。

关键日志位置

  • ai-server 中命令执行日志:regInfo.exe 或 regInfo 命令开始执行、输出、返回值。
  • 若子进程抛 IOException、InterruptedException、ExecutionException,会统一包装为服务器异常。

需要检查的配置项

  • 运行目录是否为仓库根:PathConst.RUN_QINGJIAN_PATH 基于 user.dir。
  • Windows:QingJian/license/regInfo.exe 是否存在且未被安全软件拦截。
  • Linux:QingJian/license/regInfo 是否具有可执行权限;license.xlts 是否可写。
  • Linux 下还会尝试对 QingJian/license 执行权限修改操作。

数据库表字段

此场景主要影响本地授权文件,不涉及 Gateway 表。若离线授权成功,会更新平台系统中的系统信息表中的授权信息字段。

网关转发结果

  • 如果授权校验失败,后端通常返回业务错误码;Gateway 仅做透传。
  • 若命令执行异常直接抛出,可能被全局异常处理器转为业务错误响应。

修复方案

  1. 确保启动工作目录是仓库根,使 QingJian/license 路径生效。
  2. Windows:重新解压安装包或从可信备份恢复 regInfo.exe。
  3. Linux:赋予 regInfo 可执行权限,并确保当前用户可读写 QingJian/license。
  4. 若命令返回非 0,查看命令标准输出和错误输出日志,确认是机器码不匹配、授权文件损坏还是其他错误码。
  5. 若频繁报系统级异常,检查 Java 进程权限、SELinux/AppArmor、杀毒软件策略。

章节来源

  • LicenseUtil.java:1-163
  • PathConst.java:1-110
  • SystemService.java:1-200

场景二:机器码不一致

复现步骤

  1. 在目标机器上调用获取机器码接口。
  2. 将机器码发给授权方生成 license.xlts。
  3. 上传授权文件并执行离线授权。
  4. 再次调用授权校验接口,观察是否返回“机器码不匹配”。

关键日志位置

  • 授权码解析日志:显示授权激活器返回的授权码。
  • 授权状态分支日志:包括“机器码不匹配”“激活超时”等消息。

需要检查的配置项

  • QingJian/license/license.xlts 是否确实是当前机器码对应的授权文件。
  • 是否把 A 机器的授权文件复制到 B 机器。
  • 机器硬件、磁盘序列号、主机名等用于生成机器码的环境是否发生变化。

数据库表字段

离线授权成功后,系统信息表中会写入 machinecode、unit、phone、activeDateTime、authdatetime、overdatetime 等字段。

网关转发结果

该场景发生在授权阶段,不影响 Gateway 的 token 路由逻辑;但授权未通过时,前端可能无法进入主界面或限制功能。

修复方案

  1. 在当前机器重新获取机器码。
  2. 删除旧的 license.xlts,重新申请并上传。
  3. 若机器码频繁变化,确认是否更换了操作系统镜像、虚拟机模板、容器基础镜像或磁盘卷。
  4. 若授权文件本身损坏,重新生成后再上传。

章节来源

  • LicenseUtil.java:1-163
  • SystemService.java:1-200
  • PathConst.java:1-110

场景三:许可证过期或授权单元为空

复现步骤

  1. 打开授权页面,观察是否提示“未授权”或“请先完成离线授权”。
  2. 查看系统授权信息接口返回的剩余天数。
  3. 检查数据库中系统信息表记录的过期时间。
  4. 检查授权文件中解析出的授权单元字段是否为空。

关键日志位置

  • 授权状态检查结果:包括“授权文件不存在”“激活超时”“机器码不匹配”等。
  • 离线授权保存后的授权信息写入日志。

需要检查的配置项

  • QingJian/license/license.xlts 是否存在且可读。
  • 授权文件是否过期。
  • 授权单元、手机号、客户端编号等是否在授权文件中填写完整。

数据库表字段

  • 系统信息表中的 overdatetime:用于计算剩余天数。
  • clientNo、machinecode、unit、phone、activeDateTime、authdatetime:离线授权后写入。

网关转发结果

授权页面本身属于系统信息接口,不一定依赖登录;但若系统整体因授权失败拒绝核心能力,用户可能无法继续使用分析功能。

修复方案

  1. 若提示“授权文件不存在”,重新上传有效的 .xlts 文件。
  2. 若授权已过期,联系授权方续期。
  3. 若授权单元为空,检查授权文件内容或重新申请授权。
  4. 注意:LicenseUtil.checkAuthStatus 对授权结果做了 60 秒缓存,修改授权文件后可能需要等待缓存过期或重启服务才能刷新。

章节来源

  • LicenseUtil.java:1-163
  • SystemService.java:1-200

场景四:x-token 丢失或与其他节点不一致导致 401/403

复现步骤

  1. 在浏览器开发者工具的网络面板中查看登录响应是否返回 token。
  2. 检查后续业务请求是否携带 x-token。
  3. 在多节点环境下,分别访问不同节点的业务接口,比较返回状态码。
  4. 检查 Redis 中是否存在对应 token 的键。

关键日志位置

  • Gateway:token 缓存读取失败、sa-token 解析 token 失败。
  • ai-server:未登录访问,包含 NotLogin 类型。
  • 全局异常处理器:将未登录异常转为 HTTP 200 + body.code=401。

需要检查的配置项

  • Gateway 的 Sa-Token 配置:sa-token.token-name=x-token。
  • Redis 是否可达,是否与 ai-server 共用同一个 Redis。
  • 各节点 Sa-Token 配置是否一致。

数据库表字段

Gateway 侧不会因 token 解析失败而修改业务表;但登录成功后会在 gateway_user_route 写入用户绑定。

网关转发结果

  • x-token 为空:Gateway 按匿名请求处理,后端返回 401,前端引导重新登录。
  • x-token 无效:Gateway 解析不到 userId,同样按匿名处理。
  • 若后端返回 403,需检查后端权限逻辑;Gateway 默认只负责 token 解析和粘性路由。

修复方案

  1. 确认登录接口返回了 token,前端已将其放入请求头 x-token。
  2. 检查 Redis 中是否存在 gateway:token:{serviceId}:{token} 和 Sa-Token 对应键。
  3. 若某个节点 401 而其他节点正常,说明该节点上的 token 缓存或 Sa-Token 不一致,应清除 Redis 中对应 token 后重新登录。
  4. 不要手动复制 token 到不同节点,应让 Gateway 通过登录流程重建绑定和缓存。

章节来源

  • TokenResolver.java:1-100
  • GlobalExceptionHandler.java:27-76
  • application.yaml:1-81

场景五:Gateway 未绑定节点或节点健康检查失败

复现步骤

  1. 登录后访问任意业务接口,观察是否返回 503。
  2. 检查 PostgreSQL 中 gateway_service、gateway_node、gateway_user_route 是否有记录。
  3. 检查 Gateway 健康检查日志,看节点是否被标记 DOWN。
  4. 直接访问后端节点的健康接口,确认节点本身是否存活。

关键日志位置

  • Gateway:服务不存在或已停用、用户未绑定节点、用户绑定的后端节点不可用。
  • 节点健康状态:节点 xxx 健康状态 -> UP/DOWN。
  • Gateway 错误处理器:粘性不可用统一返回 503 JSON。

需要检查的配置项

  • application.yaml 中的 gateway.services 首次播种是否覆盖到数据库。
  • health-check.interval、timeout、failure-threshold。
  • default-node-id 是否指向有效节点。

数据库表字段

表 关键字段 作用
gateway_service id、path_prefix、login_path、health_path、default_node_id 定义对外路径前缀、登录路径、健康检查路径和兜底节点
gateway_node id、service_id、base_url、enabled 登记 ai-server 节点地址
gateway_user_route service_id、user_id、username、node_id 用户粘性绑定到节点

网关转发结果

  • 无可用节点:返回 503,提示服务暂无可用节点。
  • 用户未绑定节点且无兜底节点:返回 503,提示用户未绑定节点。
  • 绑定节点存在但禁用或健康状态为 DOWN:返回 503,不自动切换。

修复方案

  1. 确认 gateway_service 已创建,且 path_prefix=/js/a。
  2. 确认至少一个 gateway_node 指向正确的 ai-server 地址。
  3. 若无用户绑定,检查登录后是否成功写入 gateway_user_route。
  4. 若节点健康检查失败,检查后端健康接口 /js/a/sys/health 是否正常。
  5. 若节点 DOWN,不要盲目切节点:案件数据、附件、Agent 记忆位于节点本地磁盘,切换可能导致数据不可见。

章节来源

  • StickyRouteFilter.java:1-180
  • NodeHealthRegistry.java:1-68
  • GatewayErrorHandler.java:1-98
  • gateway.sql:1-107
  • application.yaml:1-81

场景六:账号被顶下线后仍持有旧 token

复现步骤

  1. 在同一账号下先后打开两个客户端或浏览器标签页。
  2. 后登录的客户端成功登录。
  3. 前一个客户端继续发起请求,观察是否出现 401。
  4. 检查 Redis 中 Sa-Token 的 token 列表和活跃标记。

关键日志位置

  • ai-server 全局异常处理器:未登录访问,类型为 BE_REPLACED 或 KICK_OUT。
  • LoginActivityService:判断账号是否在使用时的 Redis 查询日志。

需要检查的配置项

  • Sa-Token 配置:is-concurrent=false 表示单端登录。
  • replaced-login-exit-mode=OLD_DEVICE、replaced-range=ALL_DEVICE_TYPE:新登录会替换旧设备。
  • ZSJZ_AUTH_IN_USE_WINDOW_MINUTES:默认 30 分钟,用于“账号正在使用”判定窗口。

数据库表字段

该场景主要影响 Redis 中的 Sa-Token 和活跃标记,不直接修改业务表。

网关转发结果

  • 旧 token 被顶掉后,Gateway 通过 Sa-Token 或 token 缓存解析失败,按匿名处理。
  • 后端返回 HTTP 200 + body.code=401,前端跳转到登录页。

修复方案

  1. 接受单端登录语义:后登录会踢掉前登录。
  2. 前端应在收到 401 后清理本地缓存并跳转登录页。
  3. 若希望避免误踢,可在同一设备上复用 token,或使用不同账号。
  4. 若出现“账号正在使用”弹窗,说明 LoginActivityService 检测到窗口内活跃;确认后才会允许强制登录。

    flowchart TD
    Start["新客户端登录"] --> CheckActive["检测账号窗口内是否活跃"]
    CheckActive --> InUse{"窗口内活跃?"}
    InUse -->|否| DoLogin["直接登录并顶掉旧会话"]
    InUse -->|是| Confirm["提示账号正在使用,需用户确认"]
    Confirm --> Confirmed{"用户确认继续?"}
    Confirmed -->|是| ForceLogin["强制登录并顶掉旧会话"]
    Confirmed -->|否| Cancel["取消登录"]
    DoLogin --> OldClient["旧客户端继续请求"]
    ForceLogin --> OldClient
    OldClient --> TokenInvalid{"token 仍有效?"}
    TokenInvalid -->|否| Redirect["返回 401,前端跳转登录"]
    TokenInvalid -->|是| Retry["重试一次"]
    

图表来源

  • LoginActivityService.java:1-106
  • GlobalExceptionHandler.java:44-76

章节来源

  • LoginActivityService.java:1-106
  • GlobalExceptionHandler.java:27-76
  • application.yaml:1-81
  • env.sh:1-172

场景七:服务端重启后会话丢失

复现步骤

  1. 登录后保持页面打开。
  2. 重启 ai-server。
  3. 刷新页面或发起业务请求,观察是否 401。
  4. 检查 Redis 中 Sa-Token 是否仍存在。

关键日志位置

  • Redis 连接失败或 Sa-Token 初始化异常日志。
  • Gateway 的 token 缓存读取失败、sa-token 解析 token 失败。

需要检查的配置项

  • Redis 地址、端口、密码是否正确。
  • SPRING_DATA_REDIS_HOST、SPRING_DATA_REDIS_PORT。
  • Sa-Token 的 timeout=2592000(约 30 天),前提是 Redis 持久化配置正确。

数据库表字段

会话本身保存在 Redis,不在 PostgreSQL 业务表中;但 Gateway 的用户绑定记录仍在数据库中,因此重新登录后可复用绑定。

网关转发结果

  • 若 Redis 不可用:Sa-Token 无法验证 token,后端返回 401。
  • 若 Gateway 本地 token 缓存存在:短期可能仍能解析 userId,但最终仍需 Sa-Token 验证。

修复方案

  1. 优先恢复 Redis 服务。
  2. 检查 Redis 是否启用持久化,避免重启后丢失 key。
  3. 若只是临时 Redis 故障,等待恢复后无需手动清理 token。
  4. 若确认 Redis 数据已丢失,让用户重新登录即可;Gateway 会通过登录响应重新建立 token 缓存和用户绑定。

章节来源

  • TokenResolver.java:1-100
  • SaTokenWebConfig.java:1-65
  • env.sh:1-172

数据库字段与配置项对照表

Gateway 表结构

表名 字段 说明
gateway_service id 服务唯一标识
gateway_service path_prefix 对外路径前缀,如 /js/a
gateway_service login_path 相对登录路径,如 /auth/login
gateway_service health_path 完整健康检查路径
gateway_service default_node_id 存量用户兜底节点
gateway_service enabled 服务是否启用
gateway_node id 节点唯一标识
gateway_node service_id 所属服务
gateway_node base_url 不含路径的后端地址
gateway_node enabled 节点是否启用
gateway_user_route service_id 所属服务
gateway_user_route user_id 绑定用户 ID
gateway_user_route username 绑定用户名
gateway_user_route node_id 目标节点

章节来源

  • gateway.sql:1-107

关键配置项

配置项 位置 含义
sa-token.token-name Gateway application.yaml 请求头名称,必须为 x-token
sa-token.timeout Gateway application.yaml token 超时时间
sa-token.is-concurrent Gateway application.yaml 是否允许多端同时在线
gateway.health-check.interval Gateway application.yaml 健康检查间隔
gateway.health-check.timeout Gateway application.yaml 健康检查超时
gateway.health-check.failure-threshold Gateway application.yaml 连续失败阈值
gateway.admin.ip-whitelist Gateway application.yaml 管理端 IP 白名单
SPRING_DATA_REDIS_HOST env.sh Redis 主机
SPRING_DATA_REDIS_PORT env.sh Redis 端口
ZSJZ_AUTH_IN_USE_WINDOW_MINUTES env.sh 账号正在使用判定窗口(分钟)

章节来源

  • application.yaml:1-81
  • env.sh:1-172

常见问题 FAQ

Q1:为什么登录后仍然 401?

可能原因:

  • 前端没有把响应中的 token 放到请求头 x-token。
  • Redis 中 Sa-Token 失效或被清空。
  • 多节点环境中 token 不一致。
  • 账号在其他客户端登录被顶掉。

排查要点:

  • 检查登录响应是否有 token。
  • 检查后续请求是否携带 x-token。
  • 检查 Redis 中 token 是否存在。
  • 检查是否出现“账号已在其他客户端登录”的错误。

章节来源

  • TokenResolver.java:1-100
  • GlobalExceptionHandler.java:54-76

Q2:为什么只有某个节点返回 503?

可能原因:

  • 用户绑定到了该节点,但该节点已被标记 DOWN。
  • 该节点被停用。
  • 健康检查失败次数达到阈值。

排查要点:

  • 检查 gateway_user_route 中用户绑定的 node_id。
  • 检查 Gateway 健康状态日志。
  • 直接访问后端节点健康接口。

章节来源

  • StickyRouteFilter.java:1-180
  • NodeHealthRegistry.java:1-68

Q3:为什么不能自动切换到另一个健康节点?

设计原因: 粘性路由明确不自动切换,因为案件数据、附件、Agent 记忆都在节点本地磁盘。切到其他节点后,请求虽然能到达健康节点,却看不到该用户的业务数据。

章节来源

  • StickyUnavailableException.java:6-24
  • StickyRouteFilter.java:1-180

Q4:为什么授权页面提示“未授权”但 Redis 正常?

可能原因:

  • QingJian/license/license.xlts 不存在。
  • regInfo.exe 或 regInfo 不可执行。
  • 授权文件过期。
  • 机器码与当前环境不一致。

章节来源

  • LicenseUtil.java:1-163
  • PathConst.java:1-110

Q5:重启服务后为什么 token 失效?

可能原因:

  • Redis 未持久化,重启后 Sa-Token 丢失。
  • Redis 地址配置错误。
  • 多节点之间 Redis 访问不一致。

章节来源

  • env.sh:1-172
  • TokenResolver.java:1-100

Q6:如何判断账号是否真的“正在使用”?

机制说明: LoginActivityService 会维护 Redis 键 zsjz:auth:last-active:{userId},TTL 等于判定窗口。判断条件包括:

  • Sa-Token 中存在该用户的 token。
  • Redis 中该用户在窗口内有活跃标记。

章节来源

  • LoginActivityService.java:1-106

结论

排查许可证与认证问题时,应按“本地授权 → 登录态 → Gateway 粘性路由 → 节点健康”的顺序逐步缩小范围:

  1. 先确认 regInfo.exe 可执行、授权文件存在且机器码一致。
  2. 再确认登录返回 x-token,Redis 中 Sa-Token 有效。
  3. 然后检查 Gateway 是否解析到 userId、是否找到用户绑定节点。
  4. 最后确认绑定节点健康,并接受“不自动切换节点”的设计约束。
  5. 遇到 401 时优先检查 token 是否存在、是否被顶掉;遇到 503 时优先检查节点绑定和健康状态。