ARTICLE / 项目演练

个人练手项目(Java)-API开放平台(四)——签名认证与SDK(技术难点)

API 开放平台系列第四篇,记录 AK/SK 签名认证、客户端 SDK 与服务调用链路。

个人练手项目(Java)-API开放平台(四)——签名认证与SDK(技术难点)封面
FEATURE IMAGE · 项目演练

签名认证

为什么要做签名认证?

如果你直接把接口暴露出去,黑客可以通过抓包拿到请求地址,然后用脚本疯狂刷你的接口,导致你的服务器瘫痪,甚至把你的数据库拖垮

防守策略(AK/SK 机制):

  1. AK (AccessKey):是公开的,代表“你是谁”。每次请求都要带在请求头(Header)里。
  2. SK (SecretKey):是绝密的,代表“你的密码”。绝对不能放在请求头里在网络上传输!
  3. Sign (签名):客户端在发请求前,把请求参数和绝密的 SK 拼接在一起,用 MD5 等算法算出一串“乱码”(这就是签名)。
  4. 验证:服务端收到请求后,拿到明文的 AK,去数据库查出对应的 SK。然后服务端用同样的参数和 SK 再算一次签名。如果两次签名一致,说明请求确实是这个用户发出的,且参数没有被篡改。

实现签名生成算法

第一步:打造签名生成算法

我们先回到 api-platform-common 模块,把这个签名算法写成一个通用的工具类,这样以后不管是客户端发请求,还是服务端做校验,都可以复用。

/**
 * API签名工具类
 */
public class SignUtils {
    /**
     * 生成API调用签名
     *
     * @param body 请求体内容(或者请求参数)
     * @param secreKey 用户的私钥
     * @return 经过MD5加密的签名字符串
     */
    public static String genSign(String body,String secreKey){

//        防止明文拼接被破解,可以在body和secretKey之间加入一个固定的分隔符,增加破解难度
        String content = body+ "." + secreKey;

        return DigestUtils.md5DigestAsHex(content.getBytes(StandardCharsets.UTF_8));
//            TODO:一般还会把随机数(Nonce)和时间戳(Timestamp)加入签名拼接中,以此防范“重放攻击”
//             目前先用最精简的 body + SK 跑通主流程,后面做网关拦截时可以再加固
    }
}

第二步:定义标准的请求头契约

为了让签名机制生效,客户端每次调用我们的接口,都必须在 HTTP 请求头(Header)里携带以下四个关键信息:

  1. accessKey:标识调用者身份。
  2. nonce:随机数(防止重放攻击)。
  3. timestamp:时间戳(防止请求过期)。
  4. sign:利用我们刚才的工具类算出来的签名。

到这里签名算法工具就准备好了,接下来就是在服务端写一个拦截器,专门去扒取请求头里的签名,并去数据库里查信息对比

实现服务端拦截器

接下来,需要在 api-platform-interface 模块里建立这道“安检门”,标准步骤如下:

  1. 拦截请求:在 HTTP 请求到达 NameController 之前,强制把它拦下。
  2. 扒取请求头:从 Header 中提取出调用方传来的 accessKeynoncetimestampsign
  3. 风控基础校验
    • 防过期:判断 timestamp 是不是超过了 5 分钟?(防止黑客拿着几天前的请求一直刷)。
    • 防重放:判断 nonce(随机数)是不是在短时间内已经被用过了?
  4. 核心签名校验:根据 accessKey 去数据库查出这个用户的 secretKey。服务端用同样的参数和查到的私钥再算一遍签名,如果算出来的结果和传过来的 sign 严丝合缝,安检放行;否则,直接报“无权限”踢出。

在实现拦截器时,一般有两个注意点:

  1. 这个拦截动作通常会放在**统一网关(Gateway)**里做,而不是每个具体的微服务自己做。
  2. 校验签名时,需要去数据库查 SecretKey。但我们的 api-platform-interface 是一个模拟第三方接口的独立模块,它没有连接主数据库。

所以为了跑通核心的签名算法和防刷逻辑,这个阶段先在这个 interface 模块里写一个本地拦截器,并且在代码里Mock一个正确的 AK 和 SK。等到了项目第四阶段(引入 Gateway 和 RPC),我们会把这段逻辑无缝迁移到网关,并连上真实的数据库。

第一步:引入 Common 模块依赖

我们需要用到刚才在 common 模块写的 SignUtils 工具类,添加下面的依赖:

<dependency>
            <groupId>com.accycx</groupId>
            <artifactId>api-platform-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

第二步:编写核心安全拦截器 (Interceptor)

api-platform-interfaceapiinterface 下新建包 interceptor,然后创建 ApiAuthInterceptor.java

这段代码包含了防伪造、防重放、防过期的风控逻辑:

/**
 * API 调用全局权限拦截器
 */
@Componen
public class ApiAuthInterceptor implements HandlerInterceptor {

//    模拟数据库中查出来的分配给这个用户的真实AK和SK
    private static final String MOCK_AK = "accycx_test_ak";
    private static final String MOCK_SK = "accycx_test_sk";

    @Override
    public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception{
//        1.从请求头中扒取调用方带过来的凭证
        String accessKey = request.getHeader("accessKey");
        String nonce = request.getHeader("nonce");
        String timestamp = request.getHeader("timestamp");
        String sign = request.getHeader("sign");
        String body = request.getHeader("body");

//        2.校验AK是否存在及合法
        if(accessKey == null || !accessKey.equals(MOCK_AK)){
            throw new RuntimeException("无权限:AccessKey 错误或不存在");
        }

//        3.防重放:校验随机数(简单版)
//        一般做法:把nonce存进Redis,如果发现这个nonce已经存在,说明是黑客在重放攻击,直接拒绝
//        这里先简单校验长度,大于4位即可
        if(nonce == null || nonce.length() <4){
            throw new RuntimeException("无权限:非法请求(Nonce 不合法)");
        }

//        4.防过期:校验时间戳
        if(timestamp == null){
            throw new RuntimeException("无权限:缺少时间戳");
        }

//        计算当前时间与请求时间的差值(设定请求有效期为5分钟)
        long currentTime = System.currentTimeMillis() / 1000;
        final long FIVE_MINUTES = 5 * 60;
        if((currentTime - Long.parseLong(timestamp)) >= FIVE_MINUTES){
            throw new RuntimeException("无权限:请求过期");
        }

//        5.校验签名
//        服务端使用同样的body和查出来的真实SK再算一遍签名
        String serverSign = SignUtils.genSign(body,MOCK_SK);

//        比对调用方传来的签名和算出来的签名是否一致
        if(sign == null || !sign.equals(serverSign)){
            throw new RuntimeException("无权限:签名校验失败,数据可能被篡改!");
        }

//        所有安检通过,放行请求,进入对应的Controller
        return true;
    }
}

这段代码采用了 Timestamp + Nonce 的双重防御。首先判断 Timestamp 是否过期(通常是 5 分钟),拦截掉旧请求。然后将 Nonce(随机数)存入 Redis 并设置 5 分钟过期时间。每次请求来时去 Redis 查,如果 Nonce 已经存在,说明是重放攻击,直接拒绝。这两者结合,保证了安全又不会撑爆 Redis 内存。

当然现阶段还没有引入Redis,所以到阶段四的时候会完善。

第三步:注册拦截器并挂载到接口上

拦截器写好了,但 Spring Boot 还不知道要把这扇门安在哪里。我们需要写一个配置类,告诉系统:“所有访问 /** 的请求,都必须走这个安检门。”

apiinterface 包下新建包 config,创建 MvcConfig.java

/**
 * Web MVC 配置类
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Autowired
    private ApiAuthInterceptor apiAuthInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry){
//        将拦截器注册到Spring MVC中
        registry.addInterceptor(apiAuthInterceptor)
                .addPathPatterns("/**");//拦截所有进入此服务的请求
    }
}

现在重新启动接口服务并访问之前测试过的简单的地址:

http://localhost:8102/name/get?name=accycx

会抛出500异常,控制台会打印出写的报错信息:“无权限:AccessKey错误或不存在”,这就说明拦截器生效了

SDK

模拟用户使用接口服务

由于手写底层的 HttpURLConnection太过繁琐,所以我们在这里引入Hutool工具包

第一步:引入 Hutool 工具包

api-platform-interface 模块引入依赖:

<dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.16</version>
        </dependency>

第二步:编写客户端调用类 (ApiClient.java)

api-platform-interface 模块的apiinterface 下新建一个包 client,然后创建 ApiClient.java

/**
 * 调用第三方接口的客户端类
 */
public class ApiClient {

    private final String accessKey;
    private final String secretKey;

//    构造方法,强制要求调用者传入AK和SK
    public ApiClient(String accessKey,String secretKey){
        this.accessKey = accessKey;
        this.secretKey = secretKey;
    }

    /**
     * 核心逻辑:组装请求头
     * 把凭证全部放在Header里
     */
    private Map<String,String> getHeaderMap(String body){
        Map<String,String> hashMap = new HashMap<>();
        hashMap.put("accessKey",accessKey);

//       生成随机数(防重放)
        hashMap.put("nonce", RandomUtil.randomNumbers(4));

//        生成当前时间戳(防过期)
        hashMap.put("timestamp",String.valueOf(System.currentTimeMillis() / 1000));

//        将请求体参与签名计算
        hashMap.put("body",body);

//        生成签名
        hashMap.put("sign", SignUtils.genSign(body,secretKey));

        return hashMap;
    }

//    1.调用GET接口
    public String getNameByGet(String name){
//        Hutool的HttpUtil可以简化HTTP请求的发送
        HashMap<String,Object> paramMap = new HashMap<>();
        paramMap.put("name",name);
        String result = HttpUtil.get("http://localhost:8102/name/get",paramMap);
        System.out.println(result);
        return result;
    }

//    2.调用POST URL传参接口
    public String getNameByPost(String name){
        HashMap<String,Object> paramMap = new HashMap<>();
        paramMap.put("name",name);
        String result = HttpUtil.post("http://localhost:8102/name/post",paramMap);
        System.out.println(result);
        return result;
    }

//    3.调用POST JSON接口(携带签名)
    public String getUserNameByPost(User user){
//        将User对象转为JSON字符串
        String json = JSONUtil.toJsonStr(user);

//        发送带请求头的HTTP请求
        HttpResponse httpResponse = HttpRequest.post("http://localhost:8102/name/user")
                .addHeaders(getHeaderMap(json)) //关键:把算好的签名头放进去
                .body(json) //塞入请求体
                .execute();

        System.out.println(httpResponse.body());
        String result = httpResponse.body();
        System.out.println(result);
        return result;

    }
}

第三步:测试安检门

api-platform-interfacesrc/test/java 目录下建一个 Main.java 测试类,编写测试代码:

public class Main {
    public static void main(String[] args) {
        // 1. 填入在拦截器里 Mock 好的真实 AK 和 SK
        String accessKey = "accycx_test_ak";
        String secretKey = "accycx_test_sk";
        // 2. 实例化客户端
        ApiClient apiClient = new ApiClient(accessKey, secretKey);
        // 3. 准备参数
        User user = new User();
        user.setUsername("accycx");
        // 4. 发起带有签名验证的请求
        System.out.println("----- 测试开始 -----");
        String result = apiClient.getUserNameByPost(user);
        System.out.println("服务端返回结果"+ result);
    }
}

启动接口服务,并运行测试类,得到结果:

测试结果

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

测试结果

开发SDK

为什么要开发 SDK?

刚才我们为了测试接口,在 Main 方法里手动组装了 Header,手动算了 Sign。 试想一下,如果你的平台有 1000 个开发者接入,难道你要让这 1000 个人每个人都去研究你的签名算法,然后各自写一遍 SignUtilsApiClient 吗? 如果他们算错了哪怕一个字符,就会一直报 500 错误,然后疯狂找你对线。

解决方法:官方提供一个 SDK(比如一个定制的 Spring Boot Starter)。开发者只需要引入这个依赖,在 application.yml 里填上你发给他的 AK/SK,剩下的签名计算、请求头拼接,SDK 全部在底层自动搞定。开发者只需要写一行代码 apiClient.getUserNameByPost(user) 就能拿到数据。

第一步:创建独立的 SDK 模块

由于 SDK 是要打成 jar 包发给别人用的,它必须是一个干净、纯粹的模块,所以在根工程下,新建一个模块,命名为 api-platform-client-sdk

  1. 配置 pom.xml

这是 SDK 的核心依赖,注意,我们要引入 spring-boot-configuration-processor,这是做自定义 Starter 的灵魂,它能让使用 SDK 的人在 application.yml 里敲代码时拥有自动提示功能。

  <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.16</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>
  1. 搬运核心资产 (代码大迁移)

为了让 SDK 独立运行,我们需要把刚才在其他模块写的几个类原封不动地复制到 api-platform-client-sdksrc/main/java/com/jingxuan/apiclientsdk 包下:

  1. User.java (专门接收参数的小模型)
  2. SignUtils.java (签名工具)
  3. ApiClient.java (客户端)

然后把api-platform-interface包下的ApiClient.java删了,接着需要把ApiClient.java里写的私有组装请求头的方法单独分离出来,保证ApiClient.java里面都是干净的调用接口类:(这里在Common模块也进行同样操作,后面会用到组装请求头的方法)

Utils包下创建AuthUtils.java

public class AuthUtils {
    /**
     * 核心逻辑:组装请求头
     * 把凭证全部放在Header里
     */
    public static Map<String,String> getHeaderMap(String body, String accessKey, String secretKey){
        Map<String,String> hashMap = new HashMap<>();
        hashMap.put("accessKey",accessKey);

//       生成随机数(防重放)
        hashMap.put("nonce", cn.hutool.core.util.RandomUtil.randomNumbers(4));

//        生成当前时间戳(防过期)
        hashMap.put("timestamp",String.valueOf(System.currentTimeMillis() / 1000));

//        将请求体参与签名计算
        hashMap.put("body",body);

//        生成签名
        hashMap.put("sign", SignUtils.genSign(body,secretKey));

        return hashMap;
    }
}

第二步:编写自动装配类 (AutoConfiguration)

我们要写一段配置代码,让 Spring Boot 在启动时,自动读取配置文件里的 AK/SK,然后自动帮我们创建一个 ApiClient 实例扔进 Spring 容器里(@Bean)。

1. 创建属性配置类

apiclientsdk 包下新建 client 包,创建 ApiClientProperties.java

/**
 * API 客户端配置属性(纯数据类)
 */
@Data
//这个注解的意思是:去 application.yml 里读取前缀为 "api.client" 的配置,映射到这个类的属性上
@ConfigurationProperties("api.client")
public class ApiClientProperties {

    /**
     * 访问密钥(只要加上这行注释,别人敲 YAML 时就会有中文提示)
     */
    private String accessKey;

    /**
     * 秘密密钥
     */
    private String secretKey;
}

在同一个包的位置下创建:ApiClientConfig.java

/**
 * ApiClient 自动配置类
 */
@Configuration
@EnableConfigurationProperties(ApiClientProperties.class) // 激活属性类
public class ApiClientConfig {

    @Bean
    public ApiClient apiClient(ApiClientProperties properties) {
        // 通过入参拿到已经装载好 YAML 数据的 properties
        return new ApiClient(properties.getAccessKey(), properties.getSecretKey());
    }
}

2. 注册自动配置类

由于我们是要做一个给别人用的 jar 包,别人项目启动时,默认只会扫描他们自己包下的类,根本扫不到我们写的 ApiClientConfig

src/main/resources 目录下,依次新建三个嵌套文件夹:META-INF -> spring。 最终路径为:src/main/resources/META-INF/spring

在这个文件夹下,新建一个文本文件,名字必须叫:org.springframework.boot.autoconfigure.AutoConfiguration.imports *

在这个文件中,写入刚才写的配置类的全限定名:

com.accycx.apiclientsdk.client.ApiClientConfig

完成这两步之后,API开放平台客户端SDK就大功告成了,下面在interface模块里面引入它测试一下

模拟测试

第一步:将 SDK 打包并安装到本地仓库

现在写的SDK只是普通代码,我们需要把它变成一个 .jar 包,并放到电脑本地的 Maven 仓库(.m2 文件夹)里,这样其他模块才能引用它。

在maven面板里面给SDK模块cleaninstall一遍,SDK就发布好了。

为什么要点 install 而不是 package

因为 package 只是把 jar 包打在当前模块的 target 目录下;而 install 会把打好的 jar 包安装到本地的 Maven 仓库中,让整台电脑里的其他项目都能通过 pom.xml 搜到并使用它。

第二步:在测试模块引入 SDK

现在回到之前测试用的 api-platform-interface 模块,我们要把刚刚自己写的 SDK 像引入 Spring Boot 官方组件一样引进来。

打开 api-platform-interface 模块的 pom.xml,在 <dependencies> 中加入:

        <dependency>
            <groupId>com.accycx</groupId>
            <artifactId>api-platform-client-sdk</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

第三步:在配置文件中填入凭证(极简配置)

打开 api-platform-interface 模块的 src/main/resources/application.yml,在最下面加上我们在 SDK 里定义好的配置前缀:

api:
  client:
    access-key: jingxuan_test_ak
    secret-key: jingxuan_test_sk

第四步:使用 Spring Boot Test 测试

既然交给了 Spring Boot 自动装配,我们就不能用普通的 main 方法测试了,因为普通的 main 方法没有启动 Spring 容器。我们需要用 Spring Boot 的单元测试。

api-platform-interfacesrc/test/java/com/accycx/apiinterface 目录下,新建(或修改已有的)测试类 ApiInterfaceApplicationTests.java

@SpringBootTes
class ApiPlatformInterfaceApplicationTests {

    // 这个没有写任何 new ApiClient() 的代码,直接注入就能用
    // 因为我们写的 SDK 里的 AutoConfiguration 已经在后台帮我们把 application.yml 里的密钥塞进去并实例化了。
    @Resource
    private ApiClient apiClient;

    @Tes
    void contextLoads() {
        // 1. 准备参数
        User user = new User();
        user.setUsername("AccyCx");

        // 2. 一行代码,直接调用!SDK 在底层会自动算好签名、拼好请求头并发送。
        String result = apiClient.getUserNameByPost(user);

        System.out.println("测试结果:" + result);
    }
}

点击测试,可以看到控制台成功打印了结果,如图

测试结果

到这里就说明我们的自定义SDK已经大功告成了!

但是,我们刚才只是在interface 模块里“本地模拟”了第三方开发者。这只证明了我们的SDK能发请求,interface 能拦请求,整个平台的闭环还没有打通。

  • 缺失的拼图一:动态分配凭证。 现在的 AK/SK 都是我们为了跑通主流程在代码里写死的(accycx_test_ak)。真实情况是,每个用户注册后,会在主平台的数据库里生成自己独一无二的 AK/SK。
  • 缺失的拼图二:真实的验签逻辑。 我们的 interface 服务现在是在本地拦截器里写死了AK、SK 来对比。真实的业务是:interface 服务收到请求后,必须通过某种方式,拿着用户传过来的 accessKey,去主数据库里查出对应的 secretKey,才能完成验签。

跨服务鉴权与 RPC 调用

之前我们为了跑通流程,在interface模块的拦截器里写了:private static final String MOCK_SK = "accycx_test_sk1";这样的模拟数据,并没有从数据库里查询真实数据。

目前的问题:

  1. interface 模块收到了调用者的 accessKey
  2. 它需要查出对应的 secretKey 来验签。
  3. 但用户信息存在主库 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 是否合法。

/**
 * 内部用户服务(仅供微服务内部调用)
 */
public interface InnerUserService {

    /**
     * 数据库中查是否已分配给用户秘钥(根据accessKey 查找到对应的User,里面包含secretKey)
     *
     * @param accessKey 用户秘钥
     * @return 如果找不到返回null
     */
    User getInvokeUser(String accessKey);
}

2. 内部接口信息服务 (InnerInterfaceInfoService.java)

用于 interface 模块查询用户正在调用的这个接口,在数据库里是否存在?是不是开启状态?

/**
 * 内部接口信息服务
 */
public interface InnerInterfaceInfoService {

    /**
     * 从数据库中查询接口是否存在(请求路径、请求方法、状态为开启)
     *
     * @param path 请求路径
     * @param method 请求方法
     * @return 如果找不到返回null
     */
    InterfaceInfo getInterfaceinfo(String path, String method);
}

这两个接口里用到了 UserInterfaceInfo 实体类。因为现在这两个类还在 model 模块里,而 common 并没有引入 model,引用一下就好了。

契约定好了,接下来就需要把**DubboNacos(注册中心)** 引入到项目中,让 backend 把电话接起来,让 interface 把电话拨出去。

第一步:环境整备(依赖与中间件)

在写代码前,我们得先在本地启动 **Nacos**。它就像是微服务世界的“中枢站”,没有它,服务之间找不到彼此。

1.启动 Nacos 服务,默认端口 8848

下载 Nacos 2.x 并在本地启动。启动成功后,访问 http://localhost:8848/nacos,看到控制台界面就说明中间件准备好了。

2.在父工程/Common 引入依赖,统一版本管理

在主 pom.xml 中引入 Spring Cloud Alibaba 依赖。然后在 backendinterfacepom.xml 中分别加入:

  • dubbo-spring-boot-starter
  • spring-cloud-starter-alibaba-nacos-discovery

第二步:服务端(backend)接电话

我们要让 backend 模块把之前在 common 里定义的契约接口实现出来,并通过 Dubbo 广播出去。

  1. 实现内部服务类

api-platform-backendservice.impl 下新建 InnerUserServiceImpl.java

@DubboService //核心:告诉Dubbo这是一个服务实现类,提供给其他微服务调用
public class InnerUserServiceImpl implements InnerUserService {

    @Resource
    private UserMapper userMapper;

    @Override
    public User getInvokeUser(String accessKey){
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("access_key", accessKey);
        return userMapper.selectOne(queryWrapper);
    }
}
  1. 配置 backend 的 application.yml
dubbo:
  application:
    name: api-platform-backend # 服务名
  protocol:
    name: dubbo
    port: -1 # 随机可用端口
  registry:
    address: nacos://localhost:8848 # 注册到 Nacos

第三步:调用端(interface)拨电话

现在我们要改掉那个写死的 MOCK_SK,让拦截器去实时查数据库。

  1. **配置 interface 的 application.yml**
dubbo:
  application:
    name: api-platform-interface
  registry:
    address: nacos://localhost:8848
  1. 重构拦截器 ApiAuthInterceptor.java

在这里,我们用 @DubboReference把远在另一个进程的对象调过来

public class ApiAuthInterceptor implements HandlerInterceptor {

    @DubboReference // 核心:远程引用 backend 暴露的服务
    private InnerUserService innerUserService;

    @Override
    public boolean preHandle(HttpServletRequest request, ...) {
        String accessKey = request.getHeader("accessKey");

        // 1. 动态查库:不再是 MOCK,而是真的去 backend 查
        User invokeUser = innerUserService.getInvokeUser(accessKey);
        if (invokeUser == null) {
            throw new RuntimeException("无权限:AccessKey 错误");
        }

        // 2. 拿到真实的 secretKey 进行验签
        String secretKey = invokeUser.getSecretKey();
        String serverSign = SignUtils.genSign(body, secretKey);

        if (!sign.equals(serverSign)) {
            throw new RuntimeException("无权限:签名校验失败");
        }
        return true;
    }
}

最后记得在主类加上@EnableDubbo注解

@SpringBootApplication
@EnableDubbo
public class ApiPlatformBackendApplication {

	public static void main(String[] args) {
		SpringApplication.run(ApiPlatformBackendApplication.class, args);
	}

}

然后现在准备联调测试

1.启动 Nacos

2.启动 Backend:查看 Nacos 控制台的“服务列表”,如果出现了 api-platform-backend,说明电话接通了。 如图:

nacos列表显示

3.启动 Interface

4.运行之前的 SDK 测试用例:如果控制台依然返回成功,说明这一通“跨服务内线电话”打通了!

结果如图:

测试结果

到这里本项目的阶段三就完成了,接下来将进入阶段四:网关与中间件(高并发攻坚),然后会先搭建统一API网关(Gateway)

目前架构的痛点: 现在我们的验签拦截器 (ApiAuthInterceptor) 是写在 api-platform-interface 这个具体的接口服务里的。 假设以后平台做大了,又开发了 weather-interface(天气服务)、sms-interface(短信服务)、ai-interface(AI 问答服务),难道要把这坨又臭又长的验签拦截器,在每一个项目里都复制粘贴一遍吗?如果哪天签名算法要升级,那得改多少个项目?

所以我们需要在所有微服务的最前面,建立一个统一的“海关大楼”(网关模块)。

  1. 所有的第三方开发者(SDK)不再直接请求具体的接口,而是把请求全部打到网关
  2. 网关负责统一验签:把拦截器里的代码搬到网关里,验签通过后,网关再负责把请求**路由(转发)**到对应的真实接口服务。
  3. 保护真实服务:真实的接口服务将不再对外暴露端口,只允许网关访问。

另外还能解决接口调用次数统计的问题,因为我们做的是一个API开放平台,平台是要算计配额的。“调用次数”是这个项目的核心业务数据。网关不仅要验签,还要在转发请求成功后,让这个用户的接口剩余调用次数减 1

这就需要我们用到之前建好的 user_interface_info(用户接口关系表),并且再次用到 Dubbo 的 RPC 调用。

接下来的路线:

1.新建 网关模块 (api-platform-gateway)。

2.迁移 鉴权逻辑:把 interface 里的拦截器废弃,在网关里写一个 全局过滤器 (GlobalFilter) 来接管验证。

3.实现 次数统计:利用 Dubbo 发起 RPC 调用,在数据库里对调用次数进行 count + 1,剩余配额 leftNum - 1

4.路由 转发:网关验证无误后,把请求平滑地送到 interface

COMMENTS · TWIKOO

评论

正在翻开留言簿…