API签名认证算法
为什么需要用到API签名认证算法?
API签名认证算法主要用于确保API请求的合法性、参数的完整性以及防止重放攻击。
比如我们在调用图床服务的时候,其实图传服务端就是使用了API签名认证算法,我们每次调用API的时候,本地的SDK会第一次为我们请求签名,当签名发送到服务端的时候
什么是重放攻击?
假如你的请求在通过一个代理服务器的时候被拦截了,攻击者可以在浏览器重复发起你之前发过的请求
为什么不能使用Token替代API签名认证?
-
Token无法保证请求的完整性,Token内包含用户身份和访问权限信息,在传输过程中可能被截获并篡改,API签名认证后即使请求被篡改了也可以校验出来
-
Token无法防范重放攻击
在实际应用中,很多系统会同时使用Token和API签名认证算法,以综合利用它们的优势。Token用于身份验证和授权,而API签名认证算法用于确保请求的完整性和安全性。这样的组合可以提供更全面的安全保障,特别是在处理敏感数据或执行重要业务逻辑的情况下。
签名流程
- 在用户在本地客户端对Body和SK进行MD5加密,将AK、serverSign(签名)、请求参数发送到网关层
- 网关层根据AK获取到SK,根据在客户端一样的签名流程,将加密出的签名与客户端上传的签名作比较,如果不相同,说明签名遭到了篡改,拒绝服务的请求
签名实现
签名的实现需要以下参数
accessKey(用户标识)
作用:识别用户身份
secretKey(用户密钥)
作用:结合accessKey,判断用户是否具有请求权限,不再请求中携带
sign(签名)
sign一般会使用单向加密算法实现,例如MD5,具体内容为MD5(请求体 + secretKey + timestamp)
作用:对请求签名,防止请求被篡改
timestamp(时间戳)
要注意客户端与服务端的时间差问题
作用:发起请求的时间,如果请求超过发起请求的5分钟,就判定请求失败
nonce(随机数)
作用:请求唯一标识,防止重放攻击,一般使用UUID实现,存储在服务端,配合timestamp使用,可以存放在缓存Redis中,超过timestamp时间就删除nonce
实操
客户端(SDK)
/**
* 构造访问API的请求头
*/
public Map<String, String> getHeaderMap(String body) {
Map<String, String> headerMap = new HashMap<>();
headerMap.put("accessKey", accessKey);
headerMap.put("nonce", RandomUtil.randomNumbers(12));
headerMap.put("body", body);
headerMap.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
headerMap.put("sign", SignUtil.getSign(body, secretKey));
return headerMap;
}
/**
*加密工具类
*/
public class SignUtil {
public static String getSign(String body, String secretKey) {
Digester md5 = new Digester(DigestAlgorithm.MD5);
String content = body.toString() + "." + secretKey;
return md5.digestHex(content);
}
}
// 调用服务端API
public String getUserNameByPost(User user) {
String json = JSONUtil.toJsonStr(user);
String result2 = HttpRequest.post(GATE_WAY_HOST + "/api/name/user").addHeaders(getHeaderMap(json))
.body(json)
.execute().body();
System.out.println(result2);
return "Post用户的名字是:" + result2;
}
服务端(网关层)
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 请求日志
ServerHttpRequest request = exchange.getRequest();
String path = INTERFACE_HOST + request.getPath().value();
String method = request.getMethod().toString();
log.info("请求唯一标识:" + request.getId());
log.info("请求路径:" + path);
log.info("请求方法:" + method);
log.info("请求参数:" + request.getQueryParams());
String sourceAddress = request.getLocalAddress().getHostString();
log.info("请求来源地址:" + sourceAddress);
log.info("请求来源地址:" + request.getRemoteAddress());
ServerHttpResponse response = exchange.getResponse();
// 3. 用户鉴权(判断 ak、sk 是否合法)
HttpHeaders headers = request.getHeaders();
String accessKey = headers.getFirst("accessKey");
String nonce = headers.getFirst("nonce");
String timestamp = headers.getFirst("timestamp");
String sign = headers.getFirst("sign");
String body = headers.getFirst("body");
// 去数据库根据accessKey查询用户
User invokeUser = null;
try {
invokeUser = innerUserService.getInvokeUser(accessKey);
} catch (Exception e) {
log.error("getInvokeUser error", e);
}
if (invokeUser == null) {
return handleNoAuth(response);
}
// 时间和当前时间不能超过 5 分钟
long currentTime = System.currentTimeMillis() / 1000;
final long FIVE_MINUTES = 60 * 5L;
if ((currentTime - Long.parseLong(timestamp)) >= FIVE_MINUTES) {
return handleNoAuth(response);
}
// 校验nonce, 判断是否是重放攻击
boolean nonceExisted = Boolean.TRUE.equals(redisTemplate.hasKey(SIGN_KEY + nonce + timestamp));
if (nonceExisted) {
return handleNoAuth(response);
}
// 从数据库中查出 secretKey与加密后的字符进行比较
String secretKey = invokeUser.getSecretKey();
String serverSign = SignUtil.getSign(body, secretKey);
if (sign == null || !sign.equals(serverSign)) {
return handleNoAuth(response);
}
// 4. 请求的模拟接口是否存在,以及请求方法是否匹配
InterfaceInfo interfaceInfo = null;
try {
interfaceInfo = innerInterfaceInfoService.getInterfaceInfo(path, method);
} catch (Exception e) {
log.error("getInterfaceInfo error", e);
}
if (interfaceInfo == null) {
return handleNoAuth(response);
}
// todo 是否还有调用次数
// 5.判断用户是否还有调用次数
boolean hasInvokeNum = innerUserInterfaceInfoService.hasInvokeNum(interfaceInfo.getId(), invokeUser.getId());
if (!hasInvokeNum) { // 没有调用次数
return handleNoAuth(response);
}
// 将nonce存入redis, 设置超时时间为5min
redisTemplate.opsForValue().set(SIGN_KEY + nonce + timestamp, nonce, FIVE_MINUTES, TimeUnit.SECONDS);
// 5. 请求转发,调用模拟接口
return handleResponse(exchange, chain, interfaceInfo.getId(), invokeUser.getId());
}