API签名认证
- 为什么需要API签名认证?
为了保证后台接口的安全,不能随便一个人就能调用后台接口。
- 怎么设计API签名认证?
参数一:accessKey
参数二:secretKey
用于加密和解密,类似于公钥和私钥,都是无序,无规则。sk不能放到请求头中,防止被窃取
参数三:sign
参数四:请求参数
客户端通过aK+请求参数+签名算法进行加密(如MD5)得到一个不能被解密的值。
这个值就是sign。客户端会将sign和aK和请求参数传到服务端,服务端根据ak去数据库中查询sk,
用得到的sk+请求参数+同样的签名算法的到另一个sign2,然后比较客户端传过来的sign和sign2是否相同,
如果相同说明请求参数没有被篡改。
但是此时,如果有中间人捕获了一个正确的请求,就可以通过不断的重放来攻击服务器。
所以现在需要防止重放。
参数五:nonce
参数六:timestamp
每个请求都随机生成一个nonce,服务端可以考虑用redis记录该nonce,如果出现相同的nonce,说明该请求是重放过的。
但是如果有大量请求,则又会生成大量的数据存储在redis中,造成服务器压力。可以考虑对每个nonce设置一个过期时间。时间过后自动清理。
可是现在又会出现一个问题。中间人等上一个nonce过期后再进行重放,又可以攻击服务器。
此时需要引入timestamp时间戳
对每个请求设置一个时间戳,自定义时间(比如1分钟),服务器只接受当前时间与请求时间相差1分钟内的请求,视大于1分钟的请求为过期请求。现在就可以设置redis的缓存时间略大于1分钟,就可以解决重放攻击。
-
大概图解
-
怎么实现API签名认证算法
客户端实现
// 客户端调用部分
/**
* 将请求参数加密
* @param body 请求参数
* @return 请求头的参数
*/
private Map<String, String> getHeaderMap(String body) {
Map<String, String> hashMap = new HashMap<>();
hashMap.put("accessKey", accessKey);
// 一定不能直接发送
// hashMap.put("secretKey", secretKey);
hashMap.put("nonce", RandomUtil.randomNumbers(4));
hashMap.put("body", body);
hashMap.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
// 加密
hashMap.put("sign", SignUtils.genSign(body, secretKey));
return hashMap;
}
public String getUsernameByPost(User user) {
String json = JSONUtil.toJsonStr(user);
// 利用HuTool的工具库来向网关发起请求
HttpResponse httpResponse = HttpRequest.post(GATEWAY_HOST + "/api/name/user")
// 加密
.addHeaders(getHeaderMap(json))
.body(json)
.execute();
System.out.println(httpResponse.getStatus());
String result = httpResponse.body();
System.out.println(result);
return result;
}
// 通过sk和body加密后进行传输
public class SignUtils {
static final String SALT = "xcxc";
/**
* 生成签名
* @param body
* @param secretKey
* @return
*/
public static String genSign(String body, String secretKey) {
Digester md5 = new Digester(DigestAlgorithm.SHA256);
String content = body + SALT + secretKey;
return md5.digestHex(content);
}
}
服务端部分
// 我是放到网关中一起处理
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 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");
User invokeUser = null;
try {
invokeUser = innerUserService.getInvokeUser(accessKey);
} catch (Exception e) {
log.error("getInvokeUser error", e);
}
if (invokeUser == null) {
return handleNoAuth(response);
}
// 如果两次签名不一样,则拒绝此次请求
String genSign = SignUtils.genSign(body, invokeUser.getSecretKey());
if (!Objects.equals(genSign, sign)) {
return handleNoAuth(response);
}
if (Long.parseLong(nonce) > 10000L) {
return handleNoAuth(response);
}
// 时间和当前时间不能超过 1 分钟
Long currentTime = System.currentTimeMillis() / 1000;
final Long ONE_MINUTES = 60 * 1L;
if ((currentTime - Long.parseLong(timestamp)) >= ONE_MINUTES) {
return handleNoAuth(response);
}
// nonce不能存在redis,否则视为重放
if (redisTemplate.hasKey(nonce)) {
return handleNoAuth(response);
}
// 添加gatewayKey,防止前台绕过网关向后台发请求
PathContainer pathContainer = request.getPath().pathWithinApplication();
// 添加gatewayKey,防止下游接口直接被访问
ServerHttpRequest.Builder mutate = request.mutate();
mutate.header(GATEWAY_KEY, GATEWAY_VALUE);
exchange.mutate().request(mutate.build()).build();
// 将nonce写入redis
// redis缓存时间要略大于时间戳设置的时间
redisTemplate.boundValueOps(nonce).set(nonce,2, TimeUnit.MINUTES);
// 实际情况中是从数据库中查出 secretKey
String secretKey = invokeUser.getSecretKey();
String serverSign = SignUtils.genSign(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);
}
// 查询该用户是否还存在调用次数
boolean leftInvokeCount = innerUserInterfaceInfoService.leftInvokeCount(interfaceInfo.getId(), invokeUser.getId());
if (!leftInvokeCount) {
return handleInvokeError(response);
}
// 5. 请求转发,调用模拟接口 + 响应日志
// Mono<Void> filter = chain.filter(exchange);
// return filter;
return handleResponse(exchange, chain, interfaceInfo.getId(), invokeUser.getId());
}
-
需要注意的细节有哪些
-
- 千万不要明文将sk放到请求中
- 用什么方式加密就用什么方式解密
- redis缓存的时间一定要略大于设置timestamp过期的时间