API接口的安全性主要是为了保证数据不会被篡改和重复调用,实现方案主要围绕Token、时间戳和Sign三个机制展开设计。
1. Token授权机制
用户使用用户名密码登录后服务器给客户端返回一个Token(必须要保证唯一,可以结合UUID和本地设备标示),并将Token-UserId以键值对的形式存放在缓存服务器中(我们是使用Redis),并要设置失效时间。服务端接收到请求后进行Token验证,如果Token不存在,说明请求无效。Token是客户端访问服务端的凭证。
# uuid 是手机设备的唯一标示
String token = UUID.randomUUID().toString() + "_" + uuid;
2. 时间戳超时机制
用户每次请求都带上当前时间的时间戳timestamp,服务端接收到timestamp后跟当前时间进行比对,如果时间差大于一定时间(比如30秒),则认为该请求失效。时间戳超时机制是防御重复调用和爬取数据的有效手段。
当然这里需要注意的地方是保证客户端和服务端的“当前时间”是一致的,我们采取的对齐方式是客户端第一次连接服务端时请求一个接口获取服务端的当前时间A1,再和客户端的当前时间B1做一个差异化计算(A1-B1=AB),得出差异值AB,客户端再后面的请求中都是传B1+AB给到服务端。
// timeStamp是客户端从Header传过来的值
Long timeStamp = RequestHeaderContext.getInstance().getTimeStamp();
boolean checkTime = checkTime(timeStamp, 30 * 1000);
if (!checkTime) {
return responseErrorAPISecurity(response);
}
// checkTime方法
public static boolean checkTime(Long time, Integer variable){
Long currentTimeMillis = System.currentTimeMillis();
Long addTime = currentTimeMillis + variable;
Long subTime = currentTimeMillis - variable;
if (addTime > time && time > subTime){
return true;
}
return false;
}
3. API签名机制
将“请求的API参数”+“时间戳”+“盐”进行MD5算法加密,加密后的数据就是本次请求的签名signature,服务端接收到请求后以同样的算法得到签名,并跟当前的签名进行比对,如果不一样,说明参数被更改过,直接返回错误标识。签名机制保证了数据不会被篡改。
// 请求的API参数,如果是再body,则MD5;如果是param,则原字符串
StringBuffer urlSign = new StringBuffer();
if ("POST".equals(request.getMethod()) || "PUT".equals(request.getMethod())) {
String bodyStr = RequestReaderUtil.ReadAsChars(request);
String bodySign = "";
if (!StringUtils.isEmpty(bodyStr)){
bodySign = DigestUtils.md5DigestAsHex((bodyStr).getBytes());
}
urlSign = new StringBuffer(bodySign);
} else if ("GET".equals(request.getMethod()) || "DELETE".equals(request.getMethod())) {
String params = request.getQueryString();
if (params == null){
params = "";
}
urlSign = new StringBuffer(params);
}
// “请求的API参数”+“时间戳”+“盐”进行MD5算法加密
String sign = DigestUtils.md5DigestAsHex(urlSign.append(timeStamp).append(salt).toString().getBytes());
// signature是客户端从Header传过来的值
if (signature.equals(sign)) {
return true;
} else {
return false;
}
4. 注意事项
1、因为用户登录的Token是和设备唯一标示绑定的,所以一个用户有可能会有多个有效的Token,那么当用户在修改登录密码时需要把所有的Token删除,我的做法是在Redis保存了一个value是List(值是该用户所有的有效的Token)的Key,当修改密码时会把该Key下的所有Token干掉。
2、客户端每次请求,在Header里面有timeStamp的值,签名中也是用这个timeStamp组合签名的,要确保这两个值是一致的。因为我们在实际开发中,发现客户端的同事在加密时通过函数获取了当前时间A,在请求时也通过函数获取了当前时间B,有时候这两个当前时间会差几毫秒,导致签名校验失败。
/**
* 登录后由服务端生成并返回
*/
private String token;
/**
* 安全校验字段(接口参数+时间戳+加盐:取MD5生成)
*/
private String signature;
/**
* 设备唯一标识
*/
private String udid;
/**
* 时间戳,13位,比如:1532942172000
*/
private Long timeStamp;
5. 安全保障总结
在以上机制下,
如果有人劫持了请求,并对请求中的参数进行了修改,签名就无法通过;
如果有人使用已经劫持的URL进行DOS攻击和爬取数据,那么他也只能最多使用30s;
如果签名算法都泄露了怎么办?可能性很小,因为这里的“盐”值只有我们的CTO知道。