跳到主要内容

API 开放平台中的 SDK 开发与签名认证

在 API 开放平台中,SDK(软件开发工具包)的开发和签名认证机制是确保安全性和可靠性的重要环节。本文将深入探讨这两个方面的实现

模拟接口项目开发

我们开发了yunfei - api - interface项目,用于提供模拟接口。例如,NameController中包含了getNameByGetgetNameByPostgetUsernameByPost等接口,如下:

@RestController
@RequestMapping("/name")
public class NameController {

@GetMapping("/get")
public String getNameByGet(String name, HttpServletRequest request) {
System.out.println(request.getHeader("yunfei"));
return "GET 你的名字是" + name;
}

@PostMapping("/post")
public String getNameByPost(@RequestParam String name) {
return "POST 你的名字是" + name;
}

@PostMapping("/user")
public String getUsernameByPost(@RequestBody User user, HttpServletRequest request) {
String result = "POST 用户名字是" + user.getUsername();
return result;
}
}

再来开发一个YunfeiApiClient来调用这些接口,并使用 Hutool 工具库中的HttpUtilHttpRequest进行请求的发送。

hutool工具库:https://doc.hutool.cn/pages/index/

public class YunfeiApiClient {
public static void main(String[] args) {
YunfeiApiClient client = new YunfeiApiClient();
client.getNameByGet("yunfei");
client.getNameByPost("yunfei");
User user = new User();
user.setUsername("yunfei");
client.getUsernameByPost(user);
}

public String getNameByGet(String name) {
HashMap<String, Object> map = new HashMap<>();
map.put("name", name);
String res = HttpUtil.get("http://localhost:10002/api/name/get", map);
System.out.println(res);
return res;

}

public String getNameByPost(String name) {
HashMap<String, Object> map = new HashMap<>();
map.put("name", name);
String res = HttpUtil.post("http://localhost:10002/api/name/post", map);
System.out.println(res);
return res;
}

public String getUsernameByPost(User user) {
String json = JSONUtil.toJsonStr(user);
String res = HttpRequest.post("http://localhost:10002/api/name/user").body(json).execute().body();
System.out.println(res);
return res;
}
}

API 签名认证

  1. 签名认证的本质:包括签发签名和使用签名(校验签名)两个步骤。其目的是保证安全性,防止未经授权的调用。
  2. 实现方式:使用 accessKey 作为调用的标识,secretKey 作为密钥。为了避免密钥在服务器之间直接传递被拦截,我们采用加密方式生成签名。这里使用了对称加密、非对称加密和 md5 加密(不可解密)等方式。用户参数和密钥通过签名算法生成不可解密的值,服务端用相同的参数和算法生成签名,与用户传的签名进行比对,以验证其正确性。
  3. 防止请求重放:通过添加 nonce 随机数和 timestamp 时间戳来防止请求重放。nonce 随机数每个请求只能用一次,服务端需要保存用过的随机数;timestamp 时间戳用于校验时间戳是否过期。

签名工具类:

public class SignUtils {
public static String genSign(String body, String secretKey) {
Digester digester = new Digester(DigestAlgorithm.SHA256);
String content = body + "." + secretKey;
return digester.digestHex(content);
}
}

在发起请求的代码中 ApiClient

    String accessKey;  
String secretKey;

public YunfeiApiClient(String accessKey, String secretKey) {
this.accessKey = accessKey;
this.secretKey = secretKey;
}

private Map<String, String> getHeaderMap(String body) {
Map<String, String> headerMap = new HashMap<>();
headerMap.put("accessKey", accessKey);
//一定不能直接传递 secretKey// headerMap.put("secretKey", secretKey);
headerMap.put("nonce", RandomUtil.randomNumbers(4));
headerMap.put("timestamp", String.valueOf(System.currentTimeMillis()));
headerMap.put("sign", SignUtils.genSign(body, secretKey));
headerMap.put("body", body);
return headerMap;
}

public String getUsernameByPost(User user) {
String json = JSONUtil.toJsonStr(user);
String res = HttpRequest.post("http://localhost:10002/api/name/user")
.addHeaders(getHeaderMap(json))
.body(json).execute()
.body();
System.out.println(res);
return res;
}

接口校验密钥,这里应该从数据库查:

@PostMapping("/user")  
public String getUsernameByPost(@RequestBody User user, HttpServletRequest request) {
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");
String serverSign = SignUtils.genSign(body, "abcdefgh");
if (!sign.equals(serverSign)) {
throw new RuntimeException("无权限");
}
xxx
}

SDK 开发

如果客户每次都要写这么多代码,会变得很麻烦,为了方便用户调用接口,我们需要开发一个客户端 SDK。用户只需输入 accessKey 和 secretKey,就可以像调用自己的代码一样简单地使用接口,而无需关心具体的实现细节。为此,我们去实现一个starter

开发 SDK 的步骤如下:

  1. 新建一个项目,并添加相关依赖,包括spring-boot-configuration-processor,该插件可以自动生成配置代码提示。
  2. 配置客户端,创建YunfeiApiClientConfig类,用于存储客户端的配置信息,如 accessKey 和 secretKey。
  3. resources目录下创建META - INF文件夹,并放入spring.factories文件,写上配置类的信息。
  4. 将该项目打包安装到本地,注意不要启动测试,因为没有主类。

Starter实现

新建一个项目,添加依赖

		<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

这个插件可以自动生成配置代码提示,一定要删除maven 的xml文件中的build内容

客户端配置

/**
* 客户端配置
*/
@Configuration
@ConfigurationProperties("yunfeiapi.client")
@Data
@ComponentScan
public class YunfeiApiClientConfig {

private String accessKey;

private String secretKey;

@Bean
public YunfeiApiClient yunfeiapiClient() {
return new YunfeiApiClient(accessKey, secretKey);
}
}

将之前的内容复制到新的sdk模块

image.png

在resourcs目录下面创建一个 META-INF文件夹,里面放spring.factories文件,写上配置类

# spring boot starter
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.yunfei.yunfeiapiclientsdk.YunfeiApiClientConfig

在将改项目进行打包install到本地 ,注意不要启动测试,因为没有主类了。

测试sdk是否可用

在interface模块中加入sdk依赖

        <dependency>
<groupId>com.yunfei</groupId>
<artifactId>yunfeiapi-client-sdk</artifactId>
<version>0.0.1</version>
</dependency>

在配置文件中添加配置:

yunfeiapi:
client:
access-key: yunfei
secret-key: abcdefgh

测试:

因为接口调用需要访问10002接口开放的接口,因此后台需要 运行这个服务 ,然后 再启动测试

@SpringBootTest
class yunfeiApiInterfaceApplicationTests {

@Resource
private YunfeiApiClient yunfeiapiClient;

@Test
void contextLoads() {
String result = yunfeiapiClient.getNameByGet("yunfei");
User user = new User();
user.setUsername("woshinibaba");
String usernameByPost = yunfeiapiClient.getUsernameByPost(user);
System.out.println(result);
System.out.println(usernameByPost);
}
}

运行结果:

image.png

将secretKey换为错误的secret-key: abcdefghdaw

image.png