Restful API AK/SK认证
Posted 李某乐
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Restful API AK/SK认证相关的知识,希望对你有一定的参考价值。
AK/SK简介
AK(Access Key ID,用于标识用户)/SK(Secret Access Key,是用户用于加密认证的字符串和验证认证字符串的密钥,SK必须保密),主要用于对用户的调用行为进行鉴权和认证,相当于专用的用户名和密码
AK/SK认证流程
客户端根据双方协商好的规则算法生成Signature认证字符串,并将生成的Signature认证字符串设置到header中。当API网关/服务端接收到请求后,判断请求中是否包含Signature认证字符串。如果包含认证字符串,则执行下一步操作。
基于HTTP请求信息,使用与客户端相同的规则算法,生成Signature字符串并于与客户端提供的Signature字符串进行比对,如果内容不一致,则认为认证失败,拒绝该请求;如果内容一致,则表示认证成功,系统将按照客户端的请求内容进行操作。
客户端:
构建http请求(包含 access key);
使用请求内容和 使用secret access key计算的签名(signature);
发送请求到服务端。
服务端:
根据发送的access key 查找数据库获得对应的secret-key;
使用一样的算法将请求内容和 secret-key一块儿计算签名(signature),和步骤2同样;
对比用户发送的签名和服务端计算的签名,二者相同则认证经过,不然失败。
实现基本思路
- 客户端需要在认证服务器中预先设置(AK 或叫 app ID) 和 SK。
- 获取当前时间时间戳并生成请求唯一标识(随机码)
- 在调用API前,客户端需要将对 时间戳、请求标识、请求参数结合SK进行签名生成一个额外的sign字符串
- 将时间戳、请求标识、AK以及生成的sign字符串设置到请求header中
- 服务端收到客户端的请求后,先判断header中设置的四类认证数据是否存在。
- 根据header中的时间戳与当前时间比对判断是否该请求以过期,防止抓包后的恶意请求
- 根据header中的请求标识判断出该请求是否唯一(每次请求将唯一标识保存,待下次请求进来后进行比对判断。可设置保存时长)
- 根据AK获取客户端预先在认证服务器设置好的SK
- 将时间戳、请求标识、请求参数结合客户端预先设置好的SK使用与客户端相同的签名生成方式生成一个临时的sign字符串并与客户端请求中包含的sign字符串比较。
- 5、6、7、8、9这五步全部通过继续执行下一步操作,否则认证失败返回错误码
代码实现
基于上面的实现思路,大致写下代码,代码中加的有详细注释,逻辑就不一一解释了,写的比较简单。
拦截器懒得写了哈,我这就直接通过AOP 前置通知来实现认证信息的获取以及认证
@Before("executePointcut()")
public void before(JoinPoint joinPoint){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Object[] args = joinPoint.getArgs();
JSONObject json = (JSONObject) args[args.length - 1];
Map sortedMap = JsonToMap.sortParams(json);
Long timeStamp = Long.parseLong(request.getHeader("TimeStamp"));
String nonce = request.getHeader("nonce");
String s = map.get(nonce);
if (s!=null){
log.error("重复的请求...");
Asserts.fail("Repeat request");
}
map.put(nonce,nonce);
//开启守护线程 清除请求唯一标识
executorService.execute(new RemoveMapRunnable(nonce));
String sign = request.getHeader("sign");
if (timeStamp==null||timeStamp<1||StringUtils.isNotEmpty(nonce)
||StringUtils.isNotEmpty(sign)){
long endTime = System.currentTimeMillis();
if (endTime-timeStamp > l){
log.error("请求过期失效..");
Asserts.fail("Request expired");
}
}else{
log.error("认证参数缺失..");
Asserts.fail("Missing authentication parameters");
}
if(!SignUtil.checkReqInfo(timeStamp, nonce, sign, sortedMap)){
log.error("认证失败,sign={}",sign);
Asserts.fail("Authentication failed");
}
log.info("认证成功...");
}
private class RemoveMapRunnable implements Runnable{
private String nonce;
public RemoveMapRunnable(String nonce){
this.nonce = nonce;
}
@Override
public void run() {
synchronized (this){
try {
Thread.sleep(l);
map.remove(this.nonce);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* @Author lijl
* @MethodName wrapperHeader
* @Description 通过请求参数,包装请求header信息(含签名信息)
* @Date 16:14 2021/11/11
* @Version 1.0
* @param reqParam
* @return: {sign=02C89AD7CEC9C05831520015CD7C3413F1DE03822D2DA015A7B353B7E7F38E7D, nonce=6b10f2ee-aba6-4032-bc9f-ca82c76b30d1, TimeStamp=1636684729852}
**/
public static Map<String, Object> wrapperHeader(Map<String, Object> reqParam) {
Long ts = System.currentTimeMillis();
String nonce = UUID.randomUUID().toString();
Map<String, Object> header = new HashMap<>();
//进行接口调用时的时间戳,即当前时间戳(毫秒),服务端会校验时间戳,例如时间差超过30秒则认为请求无效,防止重复请求的攻击
header.put("TimeStamp", ts);
//每个请求提供一个唯一的标识符,服务器能够防止请求被多次使用
header.put("nonce", nonce);
//按签名算法获取sign
String sign = getSign(appSecret, ts, nonce, reqParam);
header.put("sign", sign);
return header;
}
/**
* @Author lijl
* @MethodName getSign
* @Description 按签名算法获取sign
* @Date 16:04 2021/11/11
* @Version 1.0
* @param appSecret
* @param ts
* @param nonce
* @param reqParam
* @return: java.lang.String
**/
private static String getSign(String appSecret, Long ts, String nonce, Map<String, Object> reqParam) {
// 计算签名规则:sign = HMACSHA256("ts=1623388123195&noce=d50e301d-ee2c-446e-8f28-013f0fee09fb&appSecret=1ZLAzEgQHfBd19vSapdL8lxzA&1=2&1=2")
// 1.请求参数key升序
// 2.待加密字符串
StringBuffer s = new StringBuffer();
s.append("&ts=").append(ts).append("&noce=").append(nonce).append("&appSecret=").append(appSecret);
reqParam.forEach((k, v) -> s.append("&").append(k).append("=").append(v));
// 3.对待加密字符串进行加密(对字符串HMACSHA256处理,得到sign值)
try {
return HMACSHA256(s.toString());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* @Author lijl
* @MethodName checkReqInfo
* @Description 验证请求是否有效
* @Date 10:36 2021/11/12
* @Version 1.0
* @param ts
* @param nonce
* @param sign
* @param reqParam
* @return: 是否有效(方便测试我用Boolean,可根据业务需要,返回对应错误信息,不一定用Boolean)
**/
public static Boolean checkReqInfo(Long ts, String nonce, String sign,Map<String, Object> reqParam) {
String srvSign = getSign(appSecret, ts, nonce, reqParam);
// 目前能想到的安全验证就这些,或许大家还能想到其他验证,让接口更加安全
return sign.equalsIgnoreCase(srvSign);
}
/**
* @Author lijl
* @MethodName HMACSHA256
* @Description HMAC-SHA256算法
* @Date 10:32 2021/11/12
* @Version 1.0
* @param data
* @return: java.lang.String
**/
public static String HMACSHA256(String data) throws Exception {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(appSecret.getBytes("UTF-8"), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
public static void main(String[] args) {
Map<String, Object> reqParam = new HashMap<String, Object>();
reqParam.put("1", "2");
reqParam.put("2", "1");
//请求头(行sign值等信息)
Map<String, Object> reqHeader = wrapperHeader(reqParam);
System.out.println(reqHeader);
// ==================客户端发起请求,参数param,并把header带入请求中
// ============================服务器端,收到请求
// 1.验证请求信息
// 2处理业务逻辑
// 3.返回数据到客户端
long ts = (long) reqHeader.get("TimeStamp");
String nonce = (String) reqHeader.get("nonce");
String sign = (String) reqHeader.get("sign");
Boolean valid = checkReqInfo(ts,nonce,sign,reqParam);
if (valid){
System.out.println("有效请求,继续处理...");
}else {
System.out.println("无效");
}
}
以上是关于Restful API AK/SK认证的主要内容,如果未能解决你的问题,请参考以下文章