API 开放平台中的 SDK 开发与签名认证
在 API 开放平台中,SDK(软件开发工具包)的开发和签名认证机制是确保安全性和可靠性的重要环节。本文将深入探讨这两个方面的实现
模拟接口项目开发
我们开发了yunfei - api - interface
项目,用于提供模拟接口。例如,NameController
中包含了getNameByGet
、getNameByPost
和getUsernameByPost
等接口,如下:
@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 工具库中的HttpUtil
和HttpRequest
进 行请求的发送。
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 签名认证
- 签名认证的本质:包括签发签名和使用签名(校验签名)两个步骤。其目的是保证安全性,防止未经授权的调用。
- 实现方式:使用 accessKey 作为调用的标识,secretKey 作为密钥。为了避免密钥在服务器之间直接传递被拦截,我们采用加密方式生成签名。这里使用了对称加密、非对称加密和 md5 加密(不可解密)等方式。用户参数和密钥通过签名算法生成不可解密的值,服务端用相同的参数和算法生成签名,与用户传的签名进行比对,以验证其正确性。
- 防止请求重放:通过添加 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 的步骤如下:
- 新建一个项目,并添加相关依赖,包括
spring-boot-configuration-processor
,该插件可以自动生成配置代码提示。 - 配置客户端,创建
YunfeiApiClientConfig
类,用于存储客户端的配置信息,如 accessKey 和 secretKey。 - 在
resources
目录下创建META - INF
文件夹,并放入spring.factories
文件,写上配置类的信息。 - 将该项目打包安装到本地,注意不要启 动测试,因为没有主类。
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模块
在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);
}
}
运行结果:
将secretKey换为错误的secret-key: abcdefghdaw