干了十几年企业级系统,从传统单体应用到现在的云端SaaS,感触最深的一点就是:客户关系管理系统这类东西,表面上比的是功能菜单谁家全,实际上到了真正落地部署的时候,比的全是安全、集成和边界处理。
DeskcommCRM我前后跟了好几个项目,从最初的客户管理、销售漏斗、工单流转,到后面陆续接呼叫中心、AI外呼、短信平台,再跟钉钉、企业微信做深度集成,功能链越来越长,跨域调用的场景也越来越多。尤其是第三方页面要在自己的域名下展示CRM客户数据,外部系统要回调工单状态、外呼结果,移动端、PC端、Web端多端联动,这些需求一旦堆上来,传统那套基于Session和Cookie的权限模型立刻就不够用了。
这篇文章不聊功能清单,就聊这次做跨域安全加固的完整思考过程:从登录态模型怎么改,到token怎么替换uid,再到请求签名防篡改、服务端校验链路怎么做,以及云环境下无状态会话的坑。基本是按项目复盘的方式写,里面所有踩过的坑都是真实发生的,希望能给正在做同类系统的朋友省点时间。
1. 加固背景:为什么传统会话模型在跨域场景下撑不住
先交代一下DeskcommCRM本身。它的核心模块包括客户信息管理、销售漏斗、大客户管理、合同订单、工单管理、产品目录、财务数据和经营报表,同时还内嵌了呼叫中心、AI外呼机器人、短信平台这些偏通信的能力。跟钉钉整合后能同步通讯录、待办、消息和审批流程,跟企业微信整合后能管理外部联系人、群发消息和聊天侧边栏。
这套系统一旦铺到真实企业环境里,立刻会面临一个很现实的问题:企业不一定只在CRM系统内部操作数据。我遇到过的典型场景有这么几类:
- 企业有自己的官网或者H5活动页,需要把在页面里收集到的线索直接写入CRM的获客渠道。
- 管理层希望在企业微信或钉钉的工作台里直接看到CRM的销售漏斗和合同审批。
- 呼叫中心外包坐席在第三方平台登录,外呼结束后要把通话记录、录音地址、意向标签回传到CRM系统。
- 售后服务商需要在异域环境下查询工单状态、提交处理结果。
这些场景说白了都是跨域。跨域之后,浏览器的同源策略不会帮你管理身份,Cookie能不能带过去看对方配置,Session在分布式环境下本身就不稳定,再叠加云环境里公网访问地址可能随时变化的问题,整个信任链条就断了。
我做安全加固的第一件事,不是急着写加密代码,而是先说服项目组接受一个前提:跨域环境下,不能再依赖"浏览器帮我们记住登录状态"这种隐式信任,必须改成显式的、每次请求都校验的信任模型。也就是把身份认证从环境层面下沉到请求层面。
2. 登录态模型演变:从uid裸奔到token双向校验
2.1 原来的问题:uid直接跨域传递
早期系统里,跨域调用的身份标识用的就是uid。前端登录后拿到用户uid,后续所有跨域请求都在参数或者header里带这个uid,后端拿着uid去判断用户是谁、有没有权限。
这套方案在内部系统里跑没问题,但一旦暴露到跨域环境,问题非常明显:
- uid是长期有效且不变的,泄露一次就等于账号永久泄露。
- uid是明文传输的,抓包就能看到,根本不用解密。
- 第三方调用方不需要知道密码,只要偷到一个有效uid,就能模拟任意用户操作。
我记得有一次排查线上问题,发现某个合作方的前端代码里直接硬编码了一个uid,用来拉取CRM客户列表。当时后背就有点发凉,这个uid是什么时候泄露的、被多少人拷贝过,完全不可控。
2.2 加固后的模型:uid替换为带时效token
这次加固的核心思路,是把跨域请求中的身份标识从"永久uid"替换为"带时效的token"。具体逻辑是这样的:
系统在运营后台创建用户映射时,不再直接暴露uid,而是由服务端根据用户身份信息生成一个token,这个token包含有效期、用户标识、来源域名等关键信息,并且使用AES对称加密后再参与签名计算。
前端或者第三方调用方在发起跨域请求时,请求头里带的是token而不是uid。服务端收到请求后,先从请求头取回明文token,解密后得到带时效的内嵌令牌,再从令牌中解析出可信用户信息。整个过程中,uid只在服务端内部流转,外部拿不到。
为什么要保留uid到token的映射关系?因为DeskcommCRM内部大量的业务表结构里存的就是uid,比如客户归属、工单处理人、合同创建人这些字段。改造的时候不可能把所有表都重构一遍,所以采取了"外部用token,内部用uid"的策略,在边界层完成转换。
2.3 双向校验机制
我这里说的"双向校验",不只是服务端校验客户端token,还包括客户端对服务端身份的确认。具体实现分两层:
第一层,服务端通过RSA非对称加密生成token签名,客户端拿到token后,用公钥验证token确实是目标服务端签发的,防止第三方伪造服务端接口诱导客户端提交数据。
第二层,服务端回调第三方接口时,同样会对请求体做签名,第三方用事先约定的公钥验证回调请求的真实性。这样数据在两端之间流转时,任何一端都不需要盲目信任对方。
整个链路用的是AES对称加密加密业务数据、RSA非对称加密做身份签名、TLS保证传输过程中不被窃听。AES快,适合大量业务数据;RSA慢但安全,适合做签名和密钥交换;TLS是底座,避免前面两层做了半天结果传输层裸奔。
3. 请求签名与防篡改:不只是防篡改,还要防重放
3.1 签名计算的核心逻辑
跨域请求里面临的最大威胁不是数据被加密后看不到,而是请求被篡改、被重放。比如第三方调用方把一个"查询自己客户"的请求,改成"查询全部客户",或者把一个"提交工单"的请求重复提交十次。
针对这个问题,我设计了一套请求签名机制:
- 客户端对请求体(body)做标准化处理,取关键参数按固定顺序拼接成待签名字符串。
- 使用AES密钥参与摘要计算,生成签名值,放进请求头header的sign字段。
- 服务端收到请求后,按照同样的规则重新计算签名,对比本地生成的sign值。如果两个值不一致,直接拒绝请求。
这里有一个特别容易踩坑的点:签名计算的字段顺序必须完全一致。我和团队有一次排查了半天,发现客户端和服务端算出来的签名始终对不上,最后定位到是某个字段在拼接时,客户端用的是JSON序列化后的顺序,服务端用的是对象属性的顺序,两边不一样。
后来我们做了个约定:所有参与签名的字段,一律按照字典序排序后再拼接,并且统一用"key=value&key=value"的格式,这样不管哪端用什么语言实现,只要按规则来,结果就是一致的。
3.2 时间戳防重放
单纯防篡改还不够,因为攻击者可以把一个合法请求原封不动地重复发送。比如一个"确认收款"的请求,被重放两次就可能导致重复入账。
所以签名机制里必须加入时间戳。客户端在请求头里带一个timestamp参数,服务端校验时检查这个时间戳与服务器时间的差值,超过一定范围(我们设的是5分钟)就直接拒绝。这样即使请求被完整截获,攻击者也没法在超过时间窗口后继续重放。
再进一步,对于一些敏感操作,比如合同审批、退款确认,服务端还会用Redis记录请求的唯一标识(requestId),如果同一个requestId在短时间内出现两次,直接判定为重放请求,予以拒绝。
3.3 自定义注解实现签名校验
签名校验如果每个接口都写一遍,那代码就废了。我用自定义注解加拦截器的方式来解决这个问题。
定义了一个@SignValidate注解,在需要做签名校验的Controller方法上标注即可。拦截器通过HandlerMethod拿到当前方法是否有这个注解,如果有,就执行签名校验逻辑。
这样做的好处是让业务代码保持干净,安全逻辑统一收口在拦截器里。新增一个跨域接口时,开发者只需要在方法上加一个注解,不用关心签名怎么算、怎么验。项目里有几十个跨域接口,靠这个机制统一管住了。
注意:拦截器在处理请求时,如果直接在拦截器里读取了HttpServletRequest的body流,controller里就拿不到参数了。这是个经典坑。解决方案是用ContentCachingRequestWrapper包装请求,让body流可以被重复读取。
4. 从uid到token:用户映射的服务端实现
4.1 用户映射的创建流程
在组织协同版本里,用户映射的逻辑是从adminToken开始的。具体流程我梳理一下:
- 运营后台创建数据时,携带adminToken发起请求。
- 服务端校验adminToken的合法性,确认调用方是受信来源。
- 从请求中解析account、name、uid等用户信息。
- 服务端生成带时效的token,将token与uid的映射关系存入Redis。
- 验证通过后返回一个回包,包含token、过期时间、刷新凭据等信息。
- 后续跨域请求携带这个token,服务端解密token即可还原出用户身份。
这套流程看起来不复杂,但里面有个关键设计:token是有时效的,过期后需要续期。如果不做续期,用户的体验会很差,每隔几小时就要重新登录一次;如果续期做得不好,又会出现并发续期导致token频繁刷新的问题。
4.2 token续期与并发控制
续期机制我采用的是"双token"思路:一个短期token用于实际请求校验,有效期2小时;一个长期refreshToken用于续期,有效期7天。当短期token过期时,客户端携带refreshToken请求新的token。
这里面有个坑叫"续期风暴"。当token快要过期时,如果客户端同时发起了多个请求,每个请求都会发现token过期,然后各自去续期,结果就是同一时间产生大量续期请求,服务端压力瞬间飙升。
我的处理方式是用Redis做续期锁。客户端续期时先尝试获取锁,获取成功的才真正执行token刷新,获取失败的等待一小段时间后重试。这样即使有并发续期请求,最终也只有一次真正刷新token,其他请求拿到新token后继续执行。
4.3 自定义注解与Redis缓存
用户映射关系我存储在Redis里,key是token,value是用户信息JSON字符串,过期时间跟token有效期一致。查询用户信息时先查Redis,查不到再查数据库,查到了回填Redis。
自定义注解在这块也有用途。我定义了一个@RequireToken注解,用于标识需要token校验的接口。在拦截器里,如果检测到目标方法有这个注解,就会从请求头Authorization中取出token,解密,校验有效性,然后把用户信息放入ThreadLocal,供后续代码直接使用。
ThreadLocal用完一定要清理,不然在高并发下会出现用户数据串线的问题。我在拦截器的afterCompletion方法里做ThreadLocal的remove操作,这个细节很重要。
5. 无状态云环境下的会话与快照方案
5.1 云环境带来的新挑战
传统的会话管理假设:客户端和服务端的网络环境相对固定,Session存在单台服务器内存里就好。但到了云环境,一切都不一样了:
- 公网访问地址随时可能变化,无法通过IP白名单来限制访问来源。
- 多台服务器批量创建,服务实例动态伸缩,Session存在单机内存里等于自杀。
- 服务无状态化是上云的基本要求,任何状态都要外置到Redis等分布式存储上。
这意味着,不仅用户登录态要无状态化,连"用户当前在哪个页面、正在做什么操作"这种会话上下文信息,也要想办法外置。
5.2 会话快照机制
我实现的方案是做会话快照:每隔一段时间,把用户当前的访问信息(包括所在页面、操作对象、关键参数、token状态)存一份到Redis。当用户切换设备或者token过期重新登录后,可以从快照中恢复上下文。
比如DeskcommCRM的工单模块里,客服人员正在处理一个客户投诉工单,填到一半去接了个电话,回来发现token过期被踢下线。有了会话快照机制,重新登录后系统能自动恢复之前的填写内容,不用从头再来。
这个功能对于呼叫中心坐席特别有用。坐席在CRM里记录通话内容时,经常需要在多个页面之间来回切换,如果每次切换都丢失上下文,工作效率会大受影响。
5.3 请求令牌的过期与续期
请求令牌的过期时间我设置为30分钟,比登录token的2小时短很多。因为请求令牌是每次请求都要校验的,过期时间短一些,令牌被泄露后的危害窗口就小一些。
续期策略分成两级:请求令牌过期后,用登录token换新的请求令牌;登录token过期后,用refreshToken换取新的登录token和请求令牌。两级续期都失败,才要求用户重新登录。
这里要特别注意续期的粒度。如果每过30分钟客户端就静默续期一次,服务端的压力不会太大,因为续期本身是个轻量级操作。但如果不做任何缓存策略,每个续期请求都去数据库查一遍用户信息,高并发下很有可能会把数据库打崩。
我的做法是:续期请求在Redis里更新用户信息时,用hash结构存储,只更新有变化的字段,尽量避免大对象的序列化和反序列化。
6. 跨域页面访问栈信息梳理与可信来源校验
6.1 为什么需要梳理访问栈
跨域场景下,一个页面可能嵌入了多个来源的内容,比如DeskcommCRM的第三方合作页面里,既展示CRM客户数据,又调用了企业微信的聊天侧边栏,还挂着一个独立的呼叫中心软电话面板。这种情况下,如果不对访问栈做梳理,根本不知道一个请求到底是从哪个页面发起的。
我做过一次访问栈梳理,拿实际的项目举例:某个客户的官网首页嵌入了CRM的线索收集组件,线索收集组件里又有AI外呼的回调入口,AI外呼完成后回调企业微信的群机器人,群机器人又把消息推送到钉钉工作台。这个链路上任何一环被伪造,都会导致线索数据被污染。
所以,我们要求用户映射列表里的创建者以及回调API的调用来源,必须指向官方应用信任的域名或服务器。先建立白名单,再处理业务逻辑。
6.2 Referer校验与Origin校验的局限性
有人可能会说,跨域安全直接用Referer和Origin校验不就行了?这两个头确实是浏览器自动带的,但有两个问题:
- Referer和Origin可以被修改,尤其在非浏览器客户端(比如后端服务间调用)中,这两个头完全由调用方自己控制。
- 很多网关或代理在转发请求时会丢弃或重写这两个头,导致服务端拿到的Referer不可信。
所以我的做法是把这些头部信息作为辅助手段,不是当作唯一依据。真正起决定性作用的还是token本身和签名校验。
6.3 可信来源校验的最终方案
最终的可信来源校验方案分三层:
第一层,校验调用方的域名是否在用户映射表的白名单里。白名单由运营后台维护,只有管理员能配置。
第二层,校验回调API的调用来源是否匹配。这个匹配是在服务端完成的,通过解析token里的来源域名信息和实际请求的来源地址做比对。
第三层,校验请求签名是否合法。这个前面已经详细说过了,不再重复。
三层校验都通过,这个请求才算真正可信。
7. Authorization头域携带与token传递规范
7.1 为什么放在Authorization头
登录令牌我放在请求头的Authorization域中,格式是固定的:Authorization: Bearer <token>。这个格式是业界标准,很多网关和框架都默认支持解析这个头。
为什么不用Cookie?核心原因是跨域环境下Cookie的携带策略非常不可控。域名不同,浏览器默认不会携带对方的Cookie;就算配置了CORS允许跨域,Cookie的SameSite属性设置不对,照样带不过去。与其在Cookie上跟各种浏览器特性做斗争,不如直接在请求头里显式传递,简单直接,所有端的行为一致。
7.2 网关截获Authorization头的坑
这里有一个很典型的坑必须分享:某些网关会把Authorization头截留下来用于自己的身份认证,不会转发给后端服务。
遇到这个问题的时候,我们的服务端已经调试通过了,但一上生产环境就发现token总是校验失败。排查了很久,最后发现是网关把所有请求的Authorization头都替换成了网关自己的token,后端拿到的根本不是客户端传过去的token。
解决方案有两种:一种是在网关层面配置白名单,对指定路径的请求放行Authorization头;另一种是改用自定义头,比如X-Token或X-Auth-Token,绕开网关的默认处理逻辑。我在一个项目里用的是自定义头方案,改动最小,也最省心。
7.3 多端消息联动的token携带
DeskcommCRM集成了钉钉和企业微信,多端消息联动是标配能力。比如钉钉审批通过后,同步更新CRM里的合同状态;企业微信聊天侧边栏里查看客户信息时,需要从CRM拉取客户数据和跟进记录。
这些场景里,token的携带方不是浏览器而是钉钉或企业微信的服务端,所以要采用"应用凭证换取用户token"的模式:
- 钉钉或者企业微信的服务端通过自己的API换取用户的unionId或userId。
- CRM系统根据unionId或userId,查找对应的映射记录,生成临时的请求token。
- 第三方应用携带这个请求token调用CRM接口,完成数据查询或更新。
这套链路处理好了之后,用户在企业微信里打开聊天侧边栏,点击一个客户头像,能直接看到CRM里这个客户的历史跟进记录、合同状态和最近通话,体验非常顺畅。
8. 踩坑实录:这些坑你可能也会遇到
8.1 AES密钥硬编码风险
早期实现里,AES密钥是直接写在项目配置文件里的。后来做安全审计时发现,代码仓库的权限控制并不严格,前外包开发人员、第三方驻场人员都可能接触到源码,密钥一旦泄露,整个加密体系就形同虚设。
现在的做法是:AES密钥通过环境变量注入,不在任何配置文件里明文出现。每次部署环境单独生成密钥,测试环境、预发环境、生产环境的密钥互相隔离。
更稳妥的做法是使用密钥管理服务(KMS),不过考虑到私有化部署的客户环境差异很大,不是每个客户都能提供一个KMS服务,所以环境变量注入算是一个性价比最高的折中方案。
8.2 签名计算字段顺序不一致
签名计算的字段顺序坑我在前面已经提到过,这里再展开说一下具体的表现。
客户端用Java的HashMap存储参数,签名时直接遍历HashMap拼接字符串;服务端用TreeMap存储参数,按自然顺序输出。结果两边计算出来的签名值完全不同。排查到这个问题时,整个项目组都很崩溃,因为代码本身没有bug,就是数据结构的输出顺序问题。
固定解法是:签名前把所有参数按照key的字典序重新排序,然后以key=value&key=value的格式拼接,最后再计算摘要。这个规则必须在接口文档里写清楚,并且提供示例代码,让调用方照着做。
8.3 拦截器里读body流后controller拿不到参数
这个坑非常隐蔽。在拦截器里做签名校验时,需要读取请求体的原始内容来计算签名。但Servlet的ServletRequest的InputStream是一次性的,读完之后,controller层再尝试读取请求体,得到的就是空。
报错现象很典型:接口签名校验已经通过了,但controller里接收到的是一个空对象或者全是null。因为参数解析是在框架层面完成的,它也要从InputStream里读数据,但流已经被拦截器消费完了。
解决方案就一个:使用ContentCachingRequestWrapper包装原始的HttpServletRequest,它的核心作用是缓存body内容,让多次读取成为可能。无论是签名校验还是controller参数解析,都能从缓存中拿到数据,不会再出现"流被消费"的问题。
8.4 token续期风暴
token续期风暴在预发环境就踩到了。客户端有多个并发请求共用同一个token,token过期时,所有请求并发发起续期,一瞬间打了几十个续期请求到服务端。虽然服务端抗下来了,但响应时间明显增加,用户体验受到很大影响。
解决方案就是前面提到的续期锁。但这里有个细节:不能直接用分布式锁的框架,因为续期请求本身要非常快,锁的获取和释放不能太重。我用Redis的SETNX命令实现了一个轻量级互斥锁,设置2秒的过期时间,续期逻辑执行完就释放锁,没有释放就等锁过期自动释放。
8.5 云环境公网访问地址变化
私有化部署在云环境时,公网访问地址可能会因为负载均衡器重新创建、弹性IP调度而发生变化。如果代码里硬编码了回调地址,一旦地址变化,回调就彻底断了。
我踩过这个坑后,定了一条规则:凡是涉及回调地址的配置,一律走配置中心或者数据库字典表,禁止硬编码。系统启动时从配置中心拉取回调地址,运行期间如果地址发生变化,通过管理后台手动更新,更新后立即生效,不需要重启服务。
8.6 跨域携带Cookie问题
这是个在开发环境永远测不出来、一上生产就出问题的场景。开发环境下,前端和后端在同一个域名下,Cookie携带没有任何问题。但生产环境的前端部署在静态资源服务器上,后端是独立的API服务器,域名不一样,Cookie就不带了。
即使后端配置了CORS头,也还受限于Cookie的SameSite属性。如果SameSite设置成Lax或者Strict,跨站请求就不会带Cookie。我在最后一个客户现场被这个问题卡了两天,最终是用请求头携带token的方式彻底解决,Cookie只用于登录验证码这种一次性场景。
9. 第三方调用方接入规范与实践落地
9.1 接入流程文档化
安全机制做得再完善,如果外部调用方不知道怎么对接,一样白搭。我总结了一套第三方调用方的接入流程,写进《跨域接口接入规范》文档里:
- 提交调用方信息:包括企业名称、应用名称、回调地址、需要访问的数据范围。
- 运营后台创建用户映射:分配appId和appSecret,配置可访问的接口白名单。
- 获取token:调用方用appId和appSecret换取访问token,token有效期2小时。
- 数据签名:请求体用AES密钥加密后,参与签名计算,签名值放在header的sign字段。
- 发送请求:请求头携带Authorization、sign、timestamp、requestId等字段。
- 接收回调:服务端调用第三方接口时,同样遵循签名规则,第三方需验证签名后处理数据。
9.2 与DeskcommCRM核心模块的配合场景
前面说的这些安全机制,最终要落到具体业务场景里。我挑几个典型的说说:
呼叫中心外呼回调:坐席在第三方软电话上发起外呼,通话结束后,软电话调用CRM接口回传通话记录、时长、录音地址、客户意向标签。这些数据属于敏感业务数据,必须走完整签名校验链路。我接入过的项目里,通过这个接口把外呼结果实时同步到CRM客户档案,销售跟进的时候能看到每一次通话的完整记录。
工单跨域推送:售后服务商在自己的系统里处理工单,处理完成后把结果回传到CRM。如果这个接口没有安全防护,攻击者可以伪造工单状态,破坏整个售后服务流程的完整性。加上签名和token校验后,售后工单数据在异域系统间流转时保持一致和可信。
AI外呼机器人的数据回流:AI外呼完成后,机器人平台需要把对话文本、意向分类、通话摘要回传到CRM。这类接口通常是高频调用,因为外呼量大的时候,一天可能回传几万条记录。签名校验不能成为性能瓶颈,所以在实现时对签名计算做了缓存优化,同一请求体内容在短时间内重复计算签名时,直接复用缓存结果。
9.3 非对称加密在密钥交换中的使用
很多人问,既然用了AES对称加密,为什么还要用RSA非对称加密?直接AES一把梭不就好了?
答案是:AES加密需要双方共享同一个密钥,但这个密钥怎么安全地分发给对方,本身就是一个问题。如果直接通过网络传输明文密钥,被截获的可能性很大。
所以实际的方案是:RSA密钥对由服务端生成,公钥下发给客户端,客户端用公钥加密AES密钥后回传,服务端用私钥解密得到AES密钥。之后的数据传输都走AES加密,RSA只在密钥交换阶段起作用,保证AES密钥不会在网络上明文传输。
10. 常用的技术选型与实现要点
10.1 Redis在安全机制中的角色
Redis在整套安全机制里承担了四个角色:
- token存储:存登录token、请求token、refreshToken以及对应的用户信息。
- 签名防重放:存requestId,防止同一请求被重复提交。
- 续期锁:用SETNX实现轻量级互斥锁,防止续期风暴。
- 会话快照:存用户当前访问状态,满足无状态云环境下的上下文恢复需求。
Redis的性能很关键。token校验是每个请求都会走的路径,Redis的响应时间直接影响接口的整体延迟。我在压测时发现,Redis的读延迟超过1毫秒时,接口整体的P99响应时间就会明显变差。所以生产环境尽量让Redis和业务服务部署在同一个内网网段,避免跨机房访问。
10.2 MyBatis-Plus在用户映射查询中的使用
用户映射表的查询在用户映射创建、token校验、用户信息变更等场景里大量使用。MyBatis-Plus在这个模块里主要负责用户映射的CRUD操作,但有一点需要注意:
MyBatis-Plus的LambdaQueryWrapper虽然方便,但如果使用不当,容易造成严重的全表扫描。比如用户映射表按照uid查询时,如果没有给uid字段建索引,数据量一大查询就会超时。我遇到过的一个客户场景是用户映射表里有几十万条数据,查询耗时从几十毫秒直接劣化到几秒钟。
解决方式也简单:给查询频繁的字段加索引,并且用MyBatis-Plus的分页插件防止一次拉取过多数据。
10.3 自定义注解的具体实现思路
自定义注解有两个,分别是@SignValidate和@RequireToken。
@SignValidate用于需要签名校验的接口,拦截器在handler执行前判断方法上有没有这个注解,有的话执行签名校验逻辑。@RequireToken用于需要登录态校验的接口,拦截器从Authorization头里取token,解密后把用户信息放入ThreadLocal。
两个注解可以组合使用,比如一个接口既需要登录态又需要签名校验,就同时标注两个注解。拦截器的执行顺序是先校验签名,再校验token,两者的顺序有讲究:先确认请求未被篡改,再确认请求者的身份,如果请求本身都被篡改了,校验身份就没有意义。
11. 写在最后的经验沉淀
这套跨域安全加固方案从设计到落地,前后迭代了两个多月。核心的加密、签名、token机制都是现成的技术方案,真正花时间的反而是在业务链路上把每个环节的细节理顺。
我个人最大的体会是:安全加固不能做成"一刀切"的全封闭体系,而是在保证安全底线的同时,给外部调用方留出合理的接入空间。DeskcommCRM这种多模块、多端的系统,如果安全方案做得过于严苛,外部协作方接入成本太高,业务推进就会受阻;如果做得太松,数据泄露的风险又让人睡不安稳。
现在这套机制上线后,我养成了一个习惯:每周固定看一次安全审计日志,关注token异常、签名失败、白名单外域名调用这几类事件。有一次就是通过签名失败日志,发现某个合作方的服务器时间不同步,导致签名里的时间戳校验全部失败,提前规避了一次线上事故。
最后再分享一个小技巧:不管你的安全方案做得多完善,接口文档里一定要提供"签名计算示例"和"常见错误码说明"。跨域对接的大多数问题,最后都出在调用方没有完全理解签名规则上。一份好文档,能帮你减少至少一半的联调沟通成本。