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,服务端接收到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参数”+“时间戳”+“盐”进行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. 注意事项
/** * 登录后由服务端生成并返回 */ private String token; /** * 安全校验字段(接口参数+时间戳+加盐:取MD5生成) */ private String signature; /** * 设备唯一标识 */ private String udid; /** * 时间戳,13位,比如:1532942172000 */ private Long timeStamp;
5. 安全保障总结
在以上机制下,
如果有人劫持了请求,并对请求中的参数进行了修改,签名就无法通过;
如果有人使用已经劫持的URL进行DOS攻击和爬取数据,那么他也只能最多使用30s;
如果签名算法都泄露了怎么办?可能性很小,因为这里的“盐”值只有我们自己知道。
在以上机制下,
如果有人劫持了请求,并对请求中的参数进行了修改,签名就无法通过;
如果有人使用已经劫持的URL进行DOS攻击和爬取数据,那么他也只能最多使用30s;
如果签名算法都泄露了怎么办?可能性很小,因为这里的“盐”值只有我们自己知道。