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 知道.
来源: http://www.jianshu.com/p/10fedb2e8f3f