这是在开发api开放平台中用到的一些新知识,这些概念对于初学者来说确实比较陌生,因此这里写一篇详细的开发过程和涉及到的知识。

API签名认证

在开发API开放平台的时候,需要管理API的调用,这里会涉及到一个问题,就是必须对api的发送作出限制,限制用户的调用,限制调用的次数,判定调用者的身份是否确实为该用户本身(中间人拦截再重复调用)。因此使用到了叫API签名认证的技术,本文对这个技术的学习过程作出一些总结。

具体实现方法

签名认证本质上就是后端去校验前端传来的签名,具体实现的方法第一种是通过请求头 request header来实现,首先我们必须在用户的表上添加属性 assessKey和secretKey,这个是两个标识,至于为什么要两个属性可以参考一下登陆系统要用的用户名和密码。

当然这两个key肯定不能写死,这里为了先跑通整个流程,先写死了,最后应该是在注册的时候生成?这个还需要再思考一下。

接下来我们在调用接口的时候传入这两个key,打个断点调试一下,可以得到结果:

进入到这个判断,就说明校验key的逻辑完成了✅。

思考

这种方法是一种方法,但是咱们来想一想,这样子做的话,前端在调用api发请求的时候也将assessKey和secretKey发送到了后端,这样子校验是非常危险的,因为同时将两个Key暴露在了网络传输当中,如果前端发送的请求被拦截,或者攻击者重复发送请求,都可能成功调用起后端接口,所以这种方法还需要进一步提升,提升的方法可以参考登录注册的方法,进行加密算法。

进一步的改进

因为直接将key放在请求头在服务器之间传递是非常危险的,我们必须给密钥做加密。这里我的做法是根据密钥生成一个sign,再去跟后端比对。

签名sign的生成

签名的生成过程:用户参数 + 密钥 => 签名生成算法(MD5,HMac,Sha1) => 不可解密的值

举个例子,abc+user1=>afasfasnwg,这个就是个乱码了,即使请求被拦截,抓出来也看不出这是个什么。接下来还有一个问题,就是怎么防止请求重放,这个可以用到时间戳和加入随机数来实现,后端存储随机数,加入时间戳限制来刷新随机数,检验有效期。

开始coding😭

首先我们理一理请求头里都有些什么元素:

  1. assessKey
  2. serectKey 这个一定不能传出去,所以不在请求头里🌟🌟🌟‼️
  3. nonce 随机数
  4. body 用户参数
  5. timestamp 时间戳
  6. sign 签名

确定之后就可以写生成签名的函数和生成请求头函数了

    /**
* 生成签名,拼接请求头的字符串
* @return sign
*/
private String getSign(Map<String,String> headers,String secretKey){
Digester md5=new Digester(DigestAlgorithm.SHA256);
String nonce = headers.get("nonce");
String body = headers.get("body");
String timestamp = headers.get("timestamp");
String content=nonce+'.'+body+'.'+timestamp+'.'+secretKey;
return md5.digestHex(content);
}

/**
* 设置请求头,要注意一定不能将secretKey发送给后端
* @return headers
*/
private Map<String,String> setKeys(String body){
Map<String,String> headers=new HashMap<>();
headers.put("assessKey",assessKey);
//secretKey一定不能传出去
//headers.put("secretKey",secretKey);
headers.put("nonce", RandomUtil.randomNumbers(4));
headers.put("body",body);
headers.put("timestamp",String.valueOf(System.currentTimeMillis()/1000));
headers.put("sign",getSign(headers,secretKey));
return headers;
}

这里建议把签名生成单独拿出来做工具类,这样校验的时候也方便,调用接口的签名生成之后,就要去服务端校验接口,校验接口的思路就很简单了,取出请求头中的nonce, timestamp 和 body,数据库查表找到secretKey,调用签名生成工具生成服务端签名,和前端传来的签名比对,比对不匹配就抛出异常。写完之后打好断点调试一下:

这里看到成功进入判断逻辑,两边的签名是一致的,到这里签名认证环节就结束了。

starter开发

在开发API开放平台的时候,由于我们需要让用户调用接口,而用户不应该自己去使用 调用第三方接口的库,这对于开发者来说太麻烦,平台展现给开发者的情况应该是:开发者应该只需要关心自己要用什么接口,传什么参数,密钥是什么等等,所以我们需要开发一个便于开发者使用的sdk。

首先我们来开发starter,starter是个什么东西呢,我的理解呢就是一个工具包,跟jar包有那么一点像,但是它可以直接在application.yml中配置,方便自动创建出这么一个客户端之类的东西

开发starter

首先呢我们新建springboot项目,引入依赖lombok和spring configuration processor,进入pom.xml文件中删除build标签下所有内容,因为我们不需要将它构建成一个可运行的springboot项目,随后删除主类,因为我们不需要启动这个项目,我们只是需要做一个工具包出来。这里有一个小小的坑,就是依赖要引入一下spring web的依赖,如果不引入的话呢,生成不了resource目录和application.yml文件,但是我看别人好像没有这个问题,当然目的就是为了生成resource目录。

前置工作做完之后就是写你自己的配置

package com.si1v3r.si1v3rapiclientsdk;


import com.si1v3r.si1v3rapiclientsdk.client.Si1v3rApiClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties("si1v3r.client")
@ComponentScan

public class si1v3rApiClientConfig {
private String assessKey;

private String secretKey;

@Bean
public Si1v3rApiClient si1v3rApiClient(){
return new Si1v3rApiClient(assessKey,secretKey);
}

}

这里注意要引入你相对应的客户端和对象,比如我这里就是Si1v3rApiClient,如果有必要的依赖和工具类也要一并加入进来,做好之后在resource目录下新建一个META-INF文件夹,新建spring.factories文件,加入配置路径

#spring boot starter
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.si1v3r.si1v3rapiclientsdk.si1v3rApiClientConfig

这里的路径是指定你的配置类的路径,这些做完以后就可以去生命周期里install了,这样会生成一个包,同时我们要记住这个sdk项目的groupId,artifactId, version这三个信息,到虚拟调用api去导入

这里能这么导入的原因其实是因为生成的maven包存在了本地的mvn仓库里,所以本地能直接导入,如果要想让其他人也能用,要上传到远程的mvnrepository。导入成功后我们进入application.yml,发现可以直接配置了(nb。

这里配置完两个密钥,就可以去写单元测试了

恶心的bug

但是第一次测的时候会出现一个hutools连接拒绝的问题,然后我们能发现项目的启动端口变成sdk的端口了(。这个问题相当逆天,报错图用一下别人的:
image-20221111235729445

这里我还发现项目启动端口变成sdk里默认的端口8080了,这就很蛋疼,说明我们一开始引入spring web的依赖的思路是不行的,一旦引入spring web会导致对端口进行配置,即使我后来删了application.yml还是更改了,很恶心。最后解决方法还是得一开始不引入这个spring web,自己去新建resources目录和META- INF目录,然后流程不变再生成导入一次,最后打开项目端口,运行单元测试,能发现测试终于过了T_T。