前言
想象一下:在一个拥有数百个微服务的系统中,每个服务都像城市中的一个建筑,而服务间的通信就是连接这些建筑的街道。如果任何人都能随意假扮快递员进入任何建筑,整个城市将陷入混乱。这就是微服务安全要解决的核心问题------如何确保每个进入建筑的"快递员"都是可信的?
第一阶段:告别"万能钥匙"------引入服务端签发令牌机制
问题场景
运维团队接到警报,某个服务的API密钥已经泄露,攻击者已经用它访问了多个核心服务。更糟糕的是,这是个长期有效的密钥,传统的静态凭证就像一把"万能钥匙",一旦丢失,整个系统门户大开。
破局思路
我们引入了服务端签发令牌机制,将长期有效的"万能钥匙"换成短期"临时通行证"。
即使Token泄露,危害时间窗口被限制在很短的有效期内,安全性得到初步提升。
Authorization = Bearer {token}
实现方案
认证服务端
基于Bearer认证进行增强,构建BTS(Bearer Token Service)架构。
建立中央认证服务端,负责令牌颁发和有效性验证。

令牌颁发
请求方发起接口调用时,使用身份凭证,从认证服务端获取短期有效的令牌(Token)

json
POST /v1/tokens
{
"app_id": 0, // BTS应用ID
"timestamp": 0, // 当前时间戳
"sign": "string" // 密钥签名,对原始值【appId+":"+timestamp】使用mac算法加密
}
返回对象
{
"access_token": "701AD048849FD82BB364E0BDD1B69ED9338F7F3...",
"expires": 1757000000000
}
- 应用使用提供的密钥,将应用ID与时间戳组合后,通过HMac256算法计算签名,签名验证通过即登录成功(验证签名而非明文密钥,可避免密钥被窃取)
- 客户端提供的时间戳,与服务端的当前时间戳误差必须在5分钟内,避免登录参数被重复使用。
- Token签发的有效期默认为48小时,过期需要重新申请,避免长期有效导致泄漏后无法回收。
令牌验证
被调用方收到请求时,通过认证服务端验证Token的有效性。

- Token为特定格式,对Token进行初步自检,可以直接过滤掉无效Token。
- Token会同时存储在服务端的数据库和Redis中,通过优先检查Redis缓存可以有效提升校验效率,而数据库则作为兜底检查。
- 为了避免Redis缓存击穿影响数据库,对于Redis中不存的的令牌,若数据库存在则反写回Redis,数据库不存在则写入本地缓存,再次使用该无效令牌时直接快速失败。
第二阶段:性能攻坚------打造自包含信息载体的令牌编码
问题场景
网关日志服务需要记录请求方的来源信息,用于故障分析、请求量统计、请求来源拦截等特性,如果每个请求都需要到认证服务端进行验证后才能获取到请求来源,必然会增加大量的额外资源开销。
破局思路
让令牌"自包含"关键信息,避免了无效的资源浪费。
实现方案
采用结构化令牌编码,在Payload中嵌入应用ID、令牌过期时间等关键上下文信息。
使用对称加密算法,在合适的地方可以将信息直接还原。
Authorization = Bearer {base64(des({来源应用ID}_{令牌过期时间}))}
第三阶段:防御重放攻击------引入随机串机制
问题场景
安全审计发现,攻击者可以拦截合法请求后,在Token有效期内无需做任何的改动,就可以重复发送相同请求。虽然Token本身安全,但重复执行的请求仍然会造成业务损失。
破局思路
调整认证信息格式,为每个请求赋予唯一标识,确保请求的一次性。
Authorization = BTS id="{token}",nonce="{nonce}"
实现方案
- 在认证头中引入随机数(Nonce),格式为【当前时间戳:6位随机英文数字串】,时间戳加长随机串的目的是为了保证唯一性,对于高QPS的服务可以适当调整格式以确保唯一性。
- 被调用方在接收到请求时,会检查nonce是否已被使用过,若已存在则认证失败。
- 生成Nonce的时间戳,与认证服务端的当前时间戳误差必须在5分钟内(限制时效可以实现过期自动清理,避免产生过多的检查数据,5分钟则是服务之间时间戳的合理误差范围)
kotlin
return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', tonumber(ARGV[2]))
第四阶段:保障数据完整性------实现数据签名机制
问题场景
攻击者采用更高级的手段:截获合法请求后,使用postman等工具,修改参数后发起新的请求。虽然Token本身安全,但请求内容可能被篡改。
破局思路
为每个请求增加"数字指纹",建立完整的请求保护链条,防止数据被篡改。
Authorization = BTS id="{token}",nonce="{nonce}",mac="{mac}"
实现方案
在认证头中添加签名信息,对请求关键要素进行签名,服务端验证签名的一致性。
- 生成签名的Payload多个值之间,采用换行符[\n]进行分隔,可以避免特殊字符的影响。
- 签名保护的关键信息包括请求方法、域名、URI、防重放串、自定义请求头。
- 自定义请求头以bts-开头,增加特定前缀的目的,是因为请求头在不同框架中有可能会发生变化,导致生成的mac签名不一致,而我们只需要针对特定请求头进行保护,无需增加额外复杂度。请求头允许存在多个自定义字段,多个字段按字段名排序后拼接值。(需要特别注意的是:当请求头的值为null时,由于spring框架不支持空值,会自动转换成空字符串,因此签名需要忽略空值和空字符串)
- 签名采用mac算法进行哈希,哈希完成后使用Base64算法生成16进制字符串(令牌颁发接口在登录成功时会返回Token对应的mac_key,确保每个令牌都有各自的加密key)
第五阶段:精细化治理------实现最小授权原则
问题场景
普通用户在ERP系统通常只具有访问自己所在项目组的权限,但是攻击者却可以使用通用的令牌越权访问管理层的数据(如会议纪要),甚至执行修改和删除操作,这种权限过大的问题严重影响了信息的安全性。
破局思路
建立精细化的权限管理体系,让每个Token只能访问必要的资源。
实现方案
构建BTS管理平台,实现基于角色的访问控制(RBAC),支持API级别的授权。

- 提供bts管理平台,被访问方使用角色和策略的机制,对外暴露一系列的权限许可,请求方通过权限申请机制获取到部分接口的访问权限。
- 被访问方在调用BTS服务端校验token时,需要同时校验请求方是否有指定接口的访问权限。

- 针对非核心应用,为了减少用户学习成本及操作复杂度,允许开启自动授权功能:当请求方发起访问时,若没有被访问方的授权,且被访问方开启了自动授权,则被访问方由系统自动生成一个具有所有权限的角色,并自动创建访问申请通过的记录。
第六阶段:严防泄露------实施动态密钥与配置注入
问题场景
代码仓库泄露事件导致多个服务的硬编码密钥暴露,面临严重安全威胁。将密钥集中存储在配置中心,虽然提升了安全性,但明文的密钥依然存在泄漏风险,并且硬编码的密钥也不利于批量变更密钥的操作。
破局思路
建立动态密钥管理体系,实现密钥的生命周期管理,避免非活跃密钥长期存续。
建立密钥自动注入模式,用户无需手动维护密钥,降低密钥泄漏风险。
实现方案
多密钥平滑轮转:
- 每个应用都可以配置多个密钥,多密钥机制可以确保应用在需要更换密钥时平滑过渡。
- 密钥的有效期默认为30天,每当使用密钥登录成功时会异步执行续期操作,若长时间未登录就会自动过期,避免非活跃密钥长期存续。

流水线动态注入密钥:
- 为了尽可能避免在代码或配置中保存明文密钥,我们针对使用k8s容器的应用,采取动态注入配置的方式来提升安全性。在使用CI/CD流程进行发布时,流水线通过内部通道动态获取一个有效的密钥,注入到K8S容器的环境变量中,在请求方需要登录时就可以从环境变量中获取到密钥。
- 对于非k8s容器化部署时(如静态站),通常会将密钥信息存储在配置中心,此时需要应用的创建者通过管理界面查看密钥明文信息进行配置。
第七阶段:坚不可摧------构建可靠性保障体系
问题场景
BTS服务端机房迁移时操作不慎导致BTS服务短暂不可用,意外地引发了整个生产系统的级联故障,所有使用BTS认证的服务均无法使用,认证服务成了单点故障。
破局思路
构建高可用架构,确保认证服务"永不掉线"
实现方案

K8S集群化部署:
- 认证核心集群:内部服务间认证的服务端,采用支持弹性伸缩的多实例部署,支持一键切换网关流量到不同区域集群,增加定时拨测和监控告警,并对功能进行剪枝(仅提供基本的令牌申请和令牌验证功能)
- 公网接入集群:面向互联网开放的认证服务端,用于支持云上部署环境的认证,严格的IP白名单和应用白名单授权。
- 管理后台集群:仅限内部开发人员使用的业务后台,提供了完整的RBAC功能配置界面。
第八阶段:极致体验------完成客户端SDK与缓存优化
问题场景
开发团队反馈认证集成学习成本高,性能优化困难,重复造轮子。且不同技术栈的接入成本差异很大,影响整体推进进度。
破局思路
针对不同的场景提供相应的简化工具,降低用户使用成本。
同时,引入客户端缓存和降级策略,提升性能和稳定性。
实现方案
操作界面摸拟请求
对于开发人员需要手动在线模拟调用接口的场景,基于已有的swagger服务接口展示页面,内置支持使用BTS认证发起模拟请求。开发人员可以选择一个自己有权限的应用作为请求方,对目标接口发起请求。
调试工具前置脚本
针对内部成员以调试为目的需要使用BTS的场景,提供了简易的认证生成脚本。
通过在postman的前置操作中增加登录和生成签名的方法,将生成的认证信息作为参数自动设置到Authorization请求头中,以此实现请求方自动认证功能。

SDK组件简化开发
针对使用率最高的Java技术栈,我们提供了专用的sdk简化开发。
开发人员只需要在pom.xml文件中引入依赖,即获得了使用BTS认证的能力。
使用由SDK提供的RestTemplate对象或Feign注解发起请求,即可自动携带认证头。
xml
<dependencies>
<dependency>
<groupId>com.company.bts</groupId>
<artifactId>bts-sdk</artifactId>
</dependency>
</dependencies>
typescript
@Autowired
private BtsRestTemplate btsRestTemplate;
@Override
public void upgrade() {
btsRestTemplate.getForObject("/v1/upgrade", String.class);
}
关键配置
SDK需要用户提供3个关键配置:
- 服务端地址:默认为当前环境的BTS服务端地址(当需要多环境切换时用户可以在配置中手动指定)
- 请求应用名:默认为当前部署实例所对应的应用名(当有特殊需求需要混用时可以在配置中指定)
- 请求应用密钥:默认从环境变量中读取,密钥会在部署时由CICD流水线自动注入(当在本地IDE启动调试时,需要手动在配置中指定密钥)
请求端缓存优化
令牌本地缓存:使用本地缓存存储有效的Token,若已存在且离过期时间大于5分钟则直接使用,否则从服务端重新申请新的Token(若请求后返回令牌无效异常,则强制使该Token失效)
被访问方缓存优化
令牌本地缓存:使用本地缓存存储已验证有效的Token,被调用时若缓存未命中则使用远端校验,若缓存已命中则进行本地权限校验。
权限本地校验:启动异步定时器每隔5分钟从BTS服务端拉取最新的应用配置数据,当令牌缓存命中时直接使用本地签名校验和访问权限校验,可以避免过多的服务端交互以提升性能。

离线模式
为了避免在服务端故障时造成大面积停摆,SDK增加了离线模式,当与BTS服务端交互出现502/503状态码时,执行3次重试,全部失败则切换为离线模式。
离线模式下,请求方直接在本地生成Local类型的令牌,接收方在收到此类型令牌时,若处于离线模式则直接在本地验证令牌格式,否则拒绝接收。
SDK处于离线模式时,启动异步线程每隔10秒对服务端可用性进行探测,若服务端已恢复正常则切换到在线模式。

演进总结与未来展望
回顾这八个阶段的演进历程,我们构建的安全体系经历了从简单到复杂、从中心化到分布式、从基础认证到全面安全的蜕变。每个阶段都是针对实际生产环境中遇到的挑战而提出的解决方案,最终形成了一套完整、健壮、易用的微服务安全认证体系。
我们正在探索与云原生技术的深度集成,计划将认证逻辑进一步下沉到基础设施层。通过与服务网格和API网关的深度融合,未来业务开发者可能完全感知不到认证过程的存在,安全将成为真正透明的基础设施,实现"安全即基础设施"。
思考题
在您的架构演进过程中,哪个安全挑战让您印象最深刻?您是如何平衡安全要求与开发效率的?欢迎在评论区分享您的实战经验!