个人练手项目(Java)-API开放平台(四)——签名认证与SDK(技术难点)
签名认证
为什么要做签名认证?
如果你直接把接口暴露出去,黑客可以通过抓包拿到请求地址,然后用脚本疯狂刷你的接口,导致你的服务器瘫痪,甚至把你的数据库拖垮
防守策略(AK/SK 机制):
- AK (AccessKey):是公开的,代表“你是谁”。每次请求都要带在请求头(Header)里。
- SK (SecretKey):是绝密的,代表“你的密码”。绝对不能放在请求头里在网络上传输!
- Sign (签名):客户端在发请求前,把请求参数和绝密的
SK拼接在一起,用 MD5 等算法算出一串“乱码”(这就是签名)。 - 验证:服务端收到请求后,拿到明文的
AK,去数据库查出对应的SK。然后服务端用同样的参数和SK再算一次签名。如果两次签名一致,说明请求确实是这个用户发出的,且参数没有被篡改。
实现签名生成算法
第一步:打造签名生成算法
我们先回到 api-platform-common 模块,把这个签名算法写成一个通用的工具类,这样以后不管是客户端发请求,还是服务端做校验,都可以复用。
1 | /** |
第二步:定义标准的请求头契约
为了让签名机制生效,客户端每次调用我们的接口,都必须在 HTTP 请求头(Header)里携带以下四个关键信息:
accessKey:标识调用者身份。nonce:随机数(防止重放攻击)。timestamp:时间戳(防止请求过期)。sign:利用我们刚才的工具类算出来的签名。
到这里签名算法工具就准备好了,接下来就是在服务端写一个拦截器,专门去扒取请求头里的签名,并去数据库里查信息对比
实现服务端拦截器
接下来,需要在 api-platform-interface 模块里建立这道“安检门”,标准步骤如下:
- 拦截请求:在 HTTP 请求到达
NameController之前,强制把它拦下。 - 扒取请求头:从 Header 中提取出调用方传来的
accessKey、nonce、timestamp和sign。 - 风控基础校验:
- 防过期:判断
timestamp是不是超过了 5 分钟?(防止黑客拿着几天前的请求一直刷)。 - 防重放:判断
nonce(随机数)是不是在短时间内已经被用过了?
- 防过期:判断
- 核心签名校验:根据
accessKey去数据库查出这个用户的secretKey。服务端用同样的参数和查到的私钥再算一遍签名,如果算出来的结果和传过来的sign严丝合缝,安检放行;否则,直接报“无权限”踢出。
在实现拦截器时,一般有两个注意点:
- 这个拦截动作通常会放在**统一网关(Gateway)**里做,而不是每个具体的微服务自己做。
- 校验签名时,需要去数据库查
SecretKey。但我们的api-platform-interface是一个模拟第三方接口的独立模块,它没有连接主数据库。
所以为了跑通核心的签名算法和防刷逻辑,这个阶段先在这个 interface 模块里写一个本地拦截器,并且在代码里Mock一个正确的 AK 和 SK。等到了项目第四阶段(引入 Gateway 和 RPC),我们会把这段逻辑无缝迁移到网关,并连上真实的数据库。
第一步:引入 Common 模块依赖
我们需要用到刚才在 common 模块写的 SignUtils 工具类,添加下面的依赖:
1 | <dependency> |
第二步:编写核心安全拦截器 (Interceptor)
在 api-platform-interface 的apiinterface 下新建包 interceptor,然后创建 ApiAuthInterceptor.java。
这段代码包含了防伪造、防重放、防过期的风控逻辑:
1 | /** |
这段代码采用了 Timestamp + Nonce 的双重防御。首先判断 Timestamp 是否过期(通常是 5 分钟),拦截掉旧请求。然后将 Nonce(随机数)存入 Redis 并设置 5 分钟过期时间。每次请求来时去 Redis 查,如果 Nonce 已经存在,说明是重放攻击,直接拒绝。这两者结合,保证了安全又不会撑爆 Redis 内存。
当然现阶段还没有引入Redis,所以到阶段四的时候会完善。
第三步:注册拦截器并挂载到接口上
拦截器写好了,但 Spring Boot 还不知道要把这扇门安在哪里。我们需要写一个配置类,告诉系统:“所有访问 /** 的请求,都必须走这个安检门。”
在 apiinterface 包下新建包 config,创建 MvcConfig.java:
1 | /** |
现在重新启动接口服务并访问之前测试过的简单的地址:
http://localhost:8102/name/get?name=accycx
会抛出500异常,控制台会打印出写的报错信息:“无权限:AccessKey错误或不存在”,这就说明拦截器生效了
SDK
模拟用户使用接口服务
由于手写底层的 HttpURLConnection太过繁琐,所以我们在这里引入Hutool工具包
第一步:引入 Hutool 工具包
在api-platform-interface 模块引入依赖:
1 | <dependency> |
第二步:编写客户端调用类 (ApiClient.java)
在 api-platform-interface 模块的apiinterface 下新建一个包 client,然后创建 ApiClient.java:
1 | /** |
第三步:测试安检门
在 api-platform-interface 的 src/test/java 目录下建一个 Main.java 测试类,编写测试代码:
1 | public class Main { |
启动接口服务,并运行测试类,得到结果:

故意把 String secretKey = "accycx_test_sk";改错后再测试,后台会返回错误信息:

开发SDK
为什么要开发 SDK?
刚才我们为了测试接口,在 Main 方法里手动组装了 Header,手动算了 Sign。 试想一下,如果你的平台有 1000 个开发者接入,难道你要让这 1000 个人每个人都去研究你的签名算法,然后各自写一遍 SignUtils 和 ApiClient 吗? 如果他们算错了哪怕一个字符,就会一直报 500 错误,然后疯狂找你对线。
解决方法:官方提供一个 SDK(比如一个定制的 Spring Boot Starter)。开发者只需要引入这个依赖,在 application.yml 里填上你发给他的 AK/SK,剩下的签名计算、请求头拼接,SDK 全部在底层自动搞定。开发者只需要写一行代码 apiClient.getUserNameByPost(user) 就能拿到数据。
第一步:创建独立的 SDK 模块
由于 SDK 是要打成 jar 包发给别人用的,它必须是一个干净、纯粹的模块,所以在根工程下,新建一个模块,命名为 api-platform-client-sdk。
- 配置
pom.xml
这是 SDK 的核心依赖,注意,我们要引入 spring-boot-configuration-processor,这是做自定义 Starter 的灵魂,它能让使用 SDK 的人在 application.yml 里敲代码时拥有自动提示功能。
1 | <dependencies> |
- 搬运核心资产 (代码大迁移)
为了让 SDK 独立运行,我们需要把刚才在其他模块写的几个类原封不动地复制到 api-platform-client-sdk 的 src/main/java/com/jingxuan/apiclientsdk 包下:
User.java(专门接收参数的小模型)SignUtils.java(签名工具)ApiClient.java(客户端)
然后把api-platform-interface包下的ApiClient.java删了
第二步:编写自动装配类 (AutoConfiguration)
我们要写一段配置代码,让 Spring Boot 在启动时,自动读取配置文件里的 AK/SK,然后自动帮我们创建一个 ApiClient 实例扔进 Spring 容器里(@Bean)。
1. 创建属性配置类
在 apiclientsdk 包下新建 client 包,创建 ApiClientConfig.java:
1 | /** |
2. 注册自动配置类
由于我们是要做一个给别人用的 jar 包,别人项目启动时,默认只会扫描他们自己包下的类,根本扫不到我们写的 ApiClientConfig。
在 src/main/resources 目录下,依次新建三个嵌套文件夹:META-INF -> spring。 最终路径为:src/main/resources/META-INF/spring。
在这个文件夹下,新建一个文本文件,名字必须叫:org.springframework.boot.autoconfigure.AutoConfiguration.imports *
在这个文件中,写入刚才写的配置类的全限定名:
1 | com.accycx.apiclientsdk.client.ApiClientConfig |
完成这两步之后,API开放平台客户端SDK就大功告成了,下面在interface模块里面引入它测试一下
模拟测试
第一步:将 SDK 打包并安装到本地仓库
现在写的SDK只是普通代码,我们需要把它变成一个 .jar 包,并放到电脑本地的 Maven 仓库(.m2 文件夹)里,这样其他模块才能引用它。
在maven面板里面给SDK模块clean再install一遍,SDK就发布好了。
为什么要点 install 而不是 package?
因为 package 只是把 jar 包打在当前模块的 target 目录下;而 install 会把打好的 jar 包安装到本地的 Maven 仓库中,让整台电脑里的其他项目都能通过 pom.xml 搜到并使用它。
第二步:在测试模块引入 SDK
现在回到之前测试用的 api-platform-interface 模块,我们要把刚刚自己写的 SDK 像引入 Spring Boot 官方组件一样引进来。
打开 api-platform-interface 模块的 pom.xml,在 <dependencies> 中加入:
1 | <dependency> |
第三步:在配置文件中填入凭证(极简配置)
打开 api-platform-interface 模块的 src/main/resources/application.yml,在最下面加上我们在 SDK 里定义好的配置前缀:
1 | api: |
第四步:使用 Spring Boot Test 测试
既然交给了 Spring Boot 自动装配,我们就不能用普通的 main 方法测试了,因为普通的 main 方法没有启动 Spring 容器。我们需要用 Spring Boot 的单元测试。
在 api-platform-interface 的 src/test/java/com/accycx/apiinterface 目录下,新建(或修改已有的)测试类 ApiInterfaceApplicationTests.java:
1 |
|
点击测试,可以看到控制台成功打印了结果,如图

到这里就说明我们的自定义SDK已经大功告成了!
但是,我们刚才只是在interface 模块里“本地模拟”了第三方开发者。这只证明了我们的SDK能发请求,interface 能拦请求,整个平台的闭环还没有打通。
- 缺失的拼图一:动态分配凭证。 现在的 AK/SK 都是我们为了跑通主流程在代码里写死的(
accycx_test_ak)。真实情况是,每个用户注册后,会在主平台的数据库里生成自己独一无二的 AK/SK。 - 缺失的拼图二:真实的验签逻辑。 我们的
interface服务现在是在本地拦截器里写死了AK、SK 来对比。真实的业务是:interface服务收到请求后,必须通过某种方式,拿着用户传过来的accessKey,去主数据库里查出对应的secretKey,才能完成验签。
所以,下一步要进行:主后台与SDK联动。
主后台与SDK联动
我们要把刚才写好的、纯净无暇的 SDK 接入到主业务模块 api-platform-backend 中,让主后台真正拥有调用 API 的能力。
未来的业务场景:
管理员可以在主平台的管理界面上,直接点击一个“在线测试调用”按钮,主后台底层就会使用这个 SDK,自动带着管理员的 AK/SK 去调用 interface 模块里的真实接口,并把结果展示在页面上!
第一步:在主业务后台引入 SDK
打开 api-platform-backend 模块的 pom.xml,把我们的轮子装上去:
1 | <dependency> |
第二步:给主后台配置“管理员”凭证
为了让主后台能代表平台方去“在线测试”接口,我们需要给主后台配置一套凭证(这也是我们 SDK 自动装配必须的)。
打开 api-platform-backend 模块的 src/main/resources/application.yml,在底部添加:
1 | api: |
第三步:编写“在线调用”业务逻辑
我们在之前的InterfaceInfoController,增加一个“在线测试调用”的接口。
1. 新建在线测试请求体 (DTO) 在 api-platform-model 模块下的 com.accycx.model.dto.interfaceinfo 包中,新建 InterfaceInfoInvokeRequest.java:
1 | /** |
2. 增加调用 Controller 接口 回到 api-platform-backend 模块的 InterfaceInfoController,在最下面添加这个接口:
1 |
|
写完这段代码后,/invoke 接口就完成了,现在我们可以把interface模块的接口地址信息用之前写过的增加接口功能存入数据库里,然后再用ApiFox测试这个新添的接口。
跨服务鉴权与 RPC 调用
之前我们为了跑通流程,在interface模块的拦截器里写了:private static final String MOCK_SK = "accycx_test_sk1";这样的模拟数据,并没有从数据库里查询真实数据。
目前的问题:
interface模块收到了调用者的accessKey。- 它需要查出对应的
secretKey来验签。 - 但用户信息存在主库
api_platform中,而interface作为一个模拟的第三方独立微服务,绝对不能直接连主库(如果每个微服务都能直连主库,一旦数据库崩溃,全盘皆输,这就违背了微服务架构的初衷)。
所以我们需要用到RPC(远程过程调用)
让 interface 模块在代码里打个“内线电话”,调用主业务 backend 模块里的方法去查数据库。这个“电话线”,我们要用到的技术选型就是 Apache Dubbo。
为了打通 Dubbo,我们分为三步走,现在直接开始第一步:建立通信契约。
第一步:在 Common 模块定义“内线服务接口”
在微服务中,两个服务要打电话,必须要有一个双方都认识的“电话簿”(公共接口)。这个电话簿理所当然要放在大家都能引用的 api-platform-common 模块里。
打开 api-platform-common 模块,在 src/main/java/com/accycx/common 下新建一个包 service,然后创建两个专门用于内部调用的 RPC 接口(注意:这和我们之前在 backend 里写的对外 Service 是不一样的):
1. 内部用户服务 (InnerUserService.java)
用于 interface 模块向 backend 查询用户的 AK/SK 是否合法。
1 | /** |
2. 内部接口信息服务 (InnerInterfaceInfoService.java)
用于 interface 模块查询用户正在调用的这个接口,在数据库里是否存在?是不是开启状态?
1 | /** |
这两个接口里用到了 User 和 InterfaceInfo 实体类。因为现在这两个类还在 model 模块里,而 common 并没有引入 model,引用一下就好了。
契约定好了,接下来就需要把**Dubbo 和 Nacos(注册中心)** 引入到项目中,让 backend 把电话接起来,让 interface 把电话拨出去。
第一步:环境整备(依赖与中间件)
在写代码前,我们得先在本地启动 **Nacos**。它就像是微服务世界的“中枢站”,没有它,服务之间找不到彼此。
1.启动 Nacos 服务,默认端口 8848
下载 Nacos 2.x 并在本地启动。启动成功后,访问 http://localhost:8848/nacos,看到控制台界面就说明中间件准备好了。
2.在父工程/Common 引入依赖,统一版本管理
在主 pom.xml 中引入 Spring Cloud Alibaba 依赖。然后在 backend 和 interface 的 pom.xml 中分别加入:
dubbo-spring-boot-starterspring-cloud-starter-alibaba-nacos-discovery
第二步:服务端(backend)接电话
我们要让 backend 模块把之前在 common 里定义的契约接口实现出来,并通过 Dubbo 广播出去。
- 实现内部服务类
在 api-platform-backend 的 service.impl 下新建 InnerUserServiceImpl.java:
1 | //核心:告诉Dubbo这是一个服务实现类,提供给其他微服务调用 |
- 配置 backend 的
application.yml
1 | dubbo: |
第三步:调用端(interface)拨电话
现在我们要改掉那个写死的 MOCK_SK,让拦截器去实时查数据库。
- **配置 interface 的
application.yml**
1 | dubbo: |
- 重构拦截器
ApiAuthInterceptor.java
在这里,我们用 @DubboReference把远在另一个进程的对象调过来
1 | public class ApiAuthInterceptor implements HandlerInterceptor { |
最后记得在主类加上@EnableDubbo注解
1 |
|
然后现在准备联调测试
1.启动 Nacos。
2.启动 Backend:查看 Nacos 控制台的“服务列表”,如果出现了 api-platform-backend,说明电话接通了。
3.启动 Interface。
4.运行之前的 SDK 测试用例:如果控制台依然返回成功,说明这一通“跨服务内线电话”打通了!
结果如图:

到这里本项目的阶段三就完成了,接下来将进入阶段四:网关与中间件(高并发攻坚),然后会先搭建统一API网关(Gateway)
目前架构的痛点: 现在我们的验签拦截器 (ApiAuthInterceptor) 是写在 api-platform-interface 这个具体的接口服务里的。 假设以后平台做大了,又开发了 weather-interface(天气服务)、sms-interface(短信服务)、ai-interface(AI 问答服务),难道要把这坨又臭又长的验签拦截器,在每一个项目里都复制粘贴一遍吗?如果哪天签名算法要升级,那得改多少个项目?
所以我们需要在所有微服务的最前面,建立一个统一的“海关大楼”(网关模块)。
- 所有的第三方开发者(SDK)不再直接请求具体的接口,而是把请求全部打到网关。
- 网关负责统一验签:把拦截器里的代码搬到网关里,验签通过后,网关再负责把请求**路由(转发)**到对应的真实接口服务。
- 保护真实服务:真实的接口服务将不再对外暴露端口,只允许网关访问。
另外还能解决接口调用次数统计的问题,因为我们做的是一个API开放平台,平台是要算计配额的。“调用次数”是这个项目的核心业务数据。网关不仅要验签,还要在转发请求成功后,让这个用户的接口剩余调用次数减 1。
这就需要我们用到之前建好的 user_interface_info(用户接口关系表),并且再次用到 Dubbo 的 RPC 调用。
接下来的路线:
1.新建 网关模块 (api-platform-gateway)。
2.迁移 鉴权逻辑:把 interface 里的拦截器废弃,在网关里写一个 全局过滤器 (GlobalFilter) 来接管验证。
3.实现 次数统计:利用 Dubbo 发起 RPC 调用,在数据库里对调用次数进行 count + 1,剩余配额 leftNum - 1。
4.路由 转发:网关验证无误后,把请求平滑地送到 interface。







