换票的事,平台替你做了几次:SagooIoT HTTP API 数据源的鉴权与 Token 缓存
常见是这样一幕一个已经跑了半个月的 API 数据源某天开始稳定报 401。密钥没换地址没改第三方那边也没发公告。最后定位到的是「请求参数」里那条手写的Authorization——它和鉴权配置注入的同名 Header 撞在了一起被后者盖掉于是真正发出去的凭证成了一串早就作废的旧值。问题不在于谁覆盖谁而在于很多人把 API 数据源的鉴权理解成填一个 Header。在 SagooIoT 里它不是五种鉴权类型、两套完全不同的凭证生命周期、一层按数据源维度缓存的换票结果——任何一层理解偏了对外表现都是同一个 401。这篇按运行时的真实顺序把这条路径拆开从数据源怎么建到票在哪取、缓存按什么维度走、哪些操作会把缓存清掉最后是四个高频报错各自对应到哪里。一、先划定范围这次只讲「api导入」数据中心的数据源目前有三类类型说明api导入定时拉取第三方 HTTP/HTTPS 接口设备绑定产品或设备节点映射物模型属性时序入库数据库直连 MySQL / MSSQL表或自定义 SQL支持增量与定时同步入口统一在数据中心 → 数据源 → 新增数据来源选「api导入」。建一个 API 源的标准动作是七步填写数据源标识、名称、描述数据来源选择api导入配置请求方法get/post/put、业务 URL、更新时间cron配置鉴权——下文全部落在这条线上可选配置请求参数组Header / Body / Query保存后配置数据节点JSON 路径映射预览查询确认有数据再发布数据源平台跑这条源的顺序固定是一句话可选取 Token → 注入 Header/Query → 调用业务 API → 按数据节点映射入库其中取 Token是可选的有没有这一步只取决于第 4 步选了哪种类型。而注入发生在参数组装之后——这个先后关系后面单独说它是绝大多数配了两处、只有一处生效问题的根因。「更新时间」用 cron 表达式六段式比如五分钟一轮就是0 */5 * * * *。写法与生成工具在定时任务那篇里已经讲过这里不重复。二、五种类型按「凭证会不会过期」分成两类界面上的选项有五个文档把它们平铺成一张表类型界面选项是否调用鉴权接口是否缓存 Token适用场景none无否—无需鉴权或自行在请求参数写死 HeaderbearerBearer Token否—长期有效的 Bearer TokenapikeyAPI Key否—固定 API KeyHeader 或 Queryoauth2OAuth2是按缓存是标准 OAuth2client_credentials / passwordcustom_token自定义 Token是按缓存是非标准登录/换票接口返回 Token平铺的五种不好记但按凭证会不会过期一切两半后面所有差异就都能推出来了静态凭证none/bearer/apikey。凭证是配置里的一段固定文本平台只负责把它塞进请求不会去打任何额外接口。动态凭证oauth2/custom_token。凭证要先从对方的鉴权接口换回来换回来的票有生命周期会过期。正因为会过期才需要在平台侧缓存。这条分界线解释了一件事为什么缓存规则只对oauth2/custom_token有意义。静态凭证没有票可缓存——它就在配置里躺着。三、静态的三种不换票但各有各的错法无none不是没有鉴权能力而是不启用平台鉴权。此时行为与静态请求一致你仍然可以在「请求参数」里手动补一个 Header参数类型参数标题参数名参数值header授权AuthorizationBearer eyJhbGciOi…调试期这么写很自然但它只适合调试。凭证是死的第三方一换票你就得回来改配置而且这段文本在配置里没有任何脱敏处理。Bearer Tokenbearer的坑很窄但命中率极高Token 字段里只填 token 本身不要带Bearer前缀。前缀是平台注入时加的你写了Bearer发出去的就成了Authorization: Bearer Bearer eyJ...。{method:get,url:https://api.example.com/v1/devices,cronExpression:0 */5 * * * *,auth:{type:bearer,token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...}}API Keyapikey有三个字段其中 Key 名称有默认值字段默认必填说明Key 名称X-API-Key否参数名API Key—是密钥值注入位置header否header或query{auth:{type:apikey,apiKey:sk_live_abc123,apiKeyName:X-API-Key,apiKeyIn:header}}选query时等价于GET /sensors?api_keysk_live_abc123apiKeyName换成api_key即可。注意这会把你以为很私密的 Key 写进 URL会进对方的访问日志也会进链路上任何一台代理的日志。能走 Header 就走 Header。四、OAuth2标准换票但注入模板被锁死了oauth2面向的是符合标准的 Token 接口响应长这样{access_token:xxxx,token_type:Bearer,expires_in:7200}平台拿到后的动作是三步POSTToken 地址Content-Type: application/x-www-form-urlencoded读响应里的access_token/expires_in缓存然后对业务请求注入Authorization: Bearer access_token支持两种授权类型字段差异如下字段默认必填说明授权类型client_credentials否或passwordToken 地址—是获取 token 的 URLClient ID—视类型客户端 IDClient Secret—视类型客户端密钥Scope空否权限范围用户名 / 密码—password 时仅 password 模式{method:get,url:https://api.example.com/v1/orders,cronExpression:0 */10 * * * *,auth:{type:oauth2,grantType:client_credentials,tokenURL:https://auth.example.com/oauth/token,clientId:my-client,clientSecret:my-secret,scope:read}}这里有一个容易忽略的约束文档专门用提示块标了出来管理端 OAuth2 表单不单独暴露注入模板字段固定按 Bearer Header 注入。把这句话翻译成选型规则就是一条硬分界线只要对方要求的不是Authorization 头 Bearer 前缀oauth2就直接出局。比如对方要X-Access-Token: xxx或者前缀是Token而不是Bearer标准类型帮不了你必须换custom_token。这一点在选择类型的时候就要判断等到 401 才发现成本高得多。五、自定义 Token把三层锁死的自由度还给你custom_token面向的是响应不是标准格式的登录/换票接口比如 token 藏在data.access_token或result.token里。字段默认必填说明鉴权请求方法POST否GET / POST鉴权请求地址—是登录或换票 URLToken 字段路径access_token建议gjson 点路径如data.token有效秒数—建议响应无expires_in时的兜底界面常填 7200注入参数名Authorization否写入业务请求的参数名注入模板Bearer {{token}}否用{{token}}占位注入位置header否header/query鉴权请求 Body{}视接口JSON 对象保留数字/布尔原始类型相比oauth2它多出的正是三个自由度取哪tokenPath、注到哪injectNameinjectType、长什么样injectTpl。一个先登录、再带着 token 访问业务接口的完整配置项值鉴权类型自定义 Token鉴权请求方法POST鉴权请求地址https://iot-vendor.com/api/loginToken 字段路径data.access_token有效秒数3600注入参数名Authorization注入模板Bearer {{token}}注入位置Header鉴权请求 Body{username:admin,password:Secret123}业务 URLhttps://iot-vendor.com/api/devices{method:get,url:https://iot-vendor.com/api/devices,cronExpression:0 */5 * * * *,auth:{type:custom_token,tokenMethod:POST,tokenReqURL:https://iot-vendor.com/api/login,tokenBody:{username:admin,password:Secret123},tokenPath:data.access_token,expiresIn:3600,injectType:header,injectName:Authorization,injectTpl:Bearer {{token}}}}把最后三行换掉就能适配别的形态。对方要X-Access-Token: xxx那就injectNameX-Access-Token、injectTpl{{token}}对方把票放在 Query 里就injectTypequery、injectNameaccess_token、injectTpl{{token}}。同一套取票逻辑只是注入形状不同。有两个细节值得单独拎出来。第一tokenBody保留数字/布尔原始类型。文档特意强调这一点是因为这类参数很容易被当成字符串处理——{port: 8080}被序列化成{port: 8080}之后一部分接口会直接拒掉报错信息通常只说参数错误不会告诉你类型错了。填 Body 的时候按 JSON 的原始类型写。第二鉴权请求的额外 Header 目前没有界面入口。有些换票接口要求带X-App-Id之类的标识文档给的路径是通过接口保存时写入tokenHeaders{auth:{type:custom_token,tokenMethod:POST,tokenReqURL:https://vendor.com/login,tokenHeaders:{Content-Type:application/json,X-App-Id:app001},tokenBody:{user:admin,pwd:123456},tokenPath:data.token,expiresIn:7200}}这条值得记下来它是一个文档承认的界面缺口。遇到换票接口要额外 Header的需求时别在界面上反复找。六、注入顺序为什么两处都写一定只有一处生效文档对参数与鉴权的关系给了一句很关键的描述鉴权注入发生在参数组装之后同名 Header 以鉴权写入为准。拆开看这句能推出三件事。一、同名必被覆盖而且是静默覆盖。你在「请求参数」里写Authorization同时鉴权类型选了custom_token最终发出去的一定是鉴权注入的那一份。整个过程不报错、不提示、界面上两处都还在——所以排查的时候容易盯着配置看很久却想不到发出去的请求和配置长得不一样。二、不同名可以共存。鉴权注入的是Authorization而你在请求参数里加X-Trace-Id、Accept-Language这类两者互不干扰——覆盖只发生在同名的情况下。三、同一轮同步内多组参数共用同一张票。请求参数组是一组对应一次业务请求多组就是多次拉取分页、按站点轮询这类需求都靠它。文档明确说不会因多组参数重复打鉴权接口因为缓存维度是数据源级的一轮同步里的所有请求打的是同一张票。文档 FAQ 里对这一条的结论是可以但不建议动态鉴权就交给鉴权配置别在两个地方同时维护凭证。理由很实际——两处凭证不可能同时保持新鲜其中一处迟早会变成那个 401 的来源。七、Token 缓存五个参数决定它怎么工作缓存规则的完整定义只有五行但每一行都有工程含义项说明维度按数据源sourceId位置进程内存单机有效多实例各自缓存有效期优先级① 响应expires_in→ ② 配置「有效秒数」→ ③ 默认 3600 秒提前失效约提前 60 秒视为过期避免边界失效清缓存编辑并保存数据源或进程重启维度按sourceId。缓存挂在数据源上不是挂在第三方系统上。两个数据源连的是同一家第三方、用同一套 client 凭证平台也会各取一次票互相看不见对方的缓存。想省票只能合并数据源但代价是把两个本来独立的业务口径塞进同一个参数组里——多数情况下不值得一次多余的换票请求远比分不清口径便宜。位置在进程内存。文档括注里那句多实例各自缓存是这一行最容易被扫过去的六个字它意味着一件很具体的事实例数就是换票倍率。三副本部署第三方看到的换票请求量就是单机的三倍。如果对方的换票接口带频率限制或按次计费这里会撞墙——而排查的人往往先从数据源配置上找很难想到要去看部署拓扑。这是集群部署时要单独确认的一条。有效期三档优先级。响应里带expires_in最准直接沿用响应没带才轮到配置里的「有效秒数」两者都没有就落到默认 3600 秒。这里有个可以推出来的差异custom_token的表单里有「有效秒数」这一格oauth2的表单里没有。所以对oauth2来说只有两档——响应给expires_in就按响应走不给就直接落到 3600 秒。第三方如果既不给expires_in、票又活不到一小时就一定会在某一轮同步上撞到过期的票。这种接口用custom_token反而更可控因为你能自己填兜底值。这一条是依字段表推的文档没有直说。提前 60 秒失效。这是为了避开取的时候还活着、用的时候刚死的边界。但它有一个副作用提前量是固定的 60 秒意味着第三方给的有效期如果本来就短于 60 秒这张票等于没有缓存——每次同步都会重新换票。这就是 FAQ 第一条为什么每次都在打鉴权接口里有效期过短那半句的由来。清缓存的两种路径。编辑并保存数据源或者进程重启。注意编辑并保存不区分改了什么你只改了个 cron 表达式、只改了个描述保存下去票一样会清。这不影响正确性——下一次同步重新取一张就是——但如果第三方对换票有频率限制改配置这个动作就值得攒着做别一小时里改十遍存十遍。八、和平台其他缓存对照它是留在进程里的那一个如果只看数据源这一节的文档容易以为Token 存在内存里是平台的通行做法。不是。在开源主仓库里平台级的缓存有一整套明确的键常量集中在internal/consts/cache.go// CacheSysDict 字典缓存菜单KEYCacheSysDictSystemCache:sysDict// CacheSysRole 角色缓存keyCacheSysRoleSystemCache:sysRole//CacheSysMenu 系统菜单CacheSysMenuSystemCache:sysMenu//CacheUserAuthorize 用户权限CacheUserAuthorizeSystemCache:userAuthorize//CacheDeviceOnline 下面的是网络部分用到的CacheDeviceOnlinenetworkDeviceOnline// 告警规则CacheAlarmRuleAlarmRule:rule字典、角色、菜单、用户权限、设备在线状态、告警规则——都在这套带前缀的键体系里而且这套体系是可以切走的。manifest/config/config.example.yaml里给了缓存适配器#缓存cache:prefix:SagooIot_Sys:#缓存前缀adapter:redis# 缓存驱动方式支持memory|redis|file不填默认memoryfileDir:./storage/cache# 文件缓存路径adapterfile时必填memory|redis|file三选一。也就是说平台里绝大多数缓存可以从进程内存切到 Redis多实例部署时把这一项改成redis就解决了共享问题。而 API 数据源的 Token 缓存文档明确写的是进程内存单机有效不在上面那套键体系里。这不太像遗漏更像一个有意的取舍Token 是短命的、廉价的、丢了重新换一张就行的凭证为它引入一次跨进程的缓存读写甚至一层 Redis 依赖不划算——尤其在cache.adaptermemory的单机部署下Redis 可能压根没装。但它把多实例 多份票这件事固定了下来代价在运维侧只能靠部署时知道这件事来消化。九、SSRFToken 地址也在检查范围内数据源配置里最容易让人误判成网络不通的一类失败来自 SSRF 策略。官方文档对这一块的描述是业务 URL 与 Token URL都可能受system.ssrf策略限制。这句话里最值得注意的正是Token URL——很多人以为安全检查只针对业务接口实际上换票地址走的是同一道闸。system.ssrf的字段在配置文档里是这样的system:ssrf:enabled:falseallowPrivateIP:falseallowLocalhost:falsewhitelist:api.example.com,*.internal.corpblacklist:evil.com按字面就能推出最常见的撞墙场景数据源指向内网的 MES 或 ERP而allowPrivateIP是 false请求在出门之前就被拦掉了。这种情况下报的是URL 安全检查失败之类的字样跟超时、DNS 失败长得不一样但功能表现上都是这个源取不到数。具体怎么开关、跟哪些节互相依赖部署那篇已经讲过一遍这里只留一句结论配 API 数据源时业务地址和换票地址要一起放进白名单别只放一个。需要交代边界上面这段配置是按官方文档写的。开源 main 分支的manifest/config/config.example.yaml只有 147 行system节里依次是name、version、description、enablePProf、pprofPort、ipMethod、isDemo、isCluster、deviceCacheData、pluginsPath、upload——没有ssrf也没有文档里提到的其他几个新节。也就是说这份样例对应的是较早版本凡是哪些配置节存在的问题都以官方文档为准。十、这个模块在开源仓库里的位置写这篇之前把主仓库过了一遍有一件事需要交代清楚免得读者按图索骥去找实现最后扑空。manifest/sql/init.sql里/api/v1/source/*相关的sys_api记录一共 42 条挂在三个父节点下父节点名称子接口数303数据源22342数据建模19409动态数据展示142 条记录全部status0停用、全部is_deleted1。数据中心的一级菜单sys_menuid 27路径/config/datahub同样是is_deleted1。再看代码侧internal/logic目录下没有datahubapi/v1目录下也没有。前端的接口声明倒是完整的——src/api/datahub/index.ts里有/source/api/add、/source/api/edit、/source/search、/source/detail、/source/deploy、/source/undeploy、/source/node/*、/source/template/*一整套但src/views下没有对应的页面目录。结论跟数据中心其他几层一样菜单和接口记录都在实现不在开源仓库。所以本文里凡是平台会怎么做的描述依据都是官方文档能从开源代码里核对的只有两处——上一节的缓存键体系与配置样例以及这一节的接口记录清单。做技术选型评估时这个边界建议先摸清楚别把文档描述的能力直接当成仓库里能读到的东西。十一、排障四个报错分别对应哪里把文档 FAQ 的四条按现象 → 最可能的原因 → 先看哪里整理成一张表排查时按这个顺序走现象最可能的原因先看哪里每次同步都在打鉴权接口有效期过短expiresIn填得过小多实例各自缓存或刚保存过数据源把缓存清了第三方响应里的expires_in、配置的「有效秒数」、部署实例数预览报「鉴权接口未返回 token」tokenPath与真实响应不匹配拿 Postman 打一次换票接口对着响应结构改路径——{data:{token:xxx}}就该填data.token业务接口 401票过期注入模板前缀不对有些接口要Token {{token}}而不是Bearer {{token}}注入位置选错鉴权配置的三行injectTpl、injectType、injectName「URL 安全检查失败」SSRF 策略拦了业务 URL 或 Token URLsystem.ssrf的enabled、allowPrivateIP、whitelist第一条和第三条容易互相掩盖频繁换票会让 401 消失于是每次都在打鉴权接口被当成正常现象接受下来直到对方的换票接口限流才暴露出来。十二、落地顺序最后把文档给的动作清单按依赖关系重排一下每一步都对应一个可验证的状态先用 Postman 或 curl 把鉴权接口和业务接口各自打通——它们都能单独通才说明问题不在第三方侧在平台新建 API 数据源选鉴权类型。判断依据只有一条对方要的凭证会不会过期、注入形状是不是标准 Bearer Header保存后点查询预览返回的 JSON——这一步能看到原始的响应结构tokenPath该填什么基本一眼就定了配置数据节点把 JSON 路径映射成字段发布数据源观察它是否按 cron 正常入库发布与停用是两个独立接口/source/deploy与/source/undeploy之后每次改密钥、改密码、改鉴权方式保存即清缓存下一轮同步会重新取票——这一条不用额外操作十三、项目地址开源仓库https://github.com/sagoo-cloud官方文档https://iotdoc.sagoo.cn数据源是数据中心链条的第一环也是唯一一环要跟外部世界打交道的地方。它要处理的事情里最难的不是拉数据是拉之前那一张会不会过期的票。把鉴权类型、缓存维度、注入顺序这三件事分开想清楚401 这类问题基本就只剩第三方自己的锅了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →