对外提供http+json接口,一般需要附带接口鉴权,往往使用的鉴权方案就是秘钥。为啥需要这么麻烦?因为我们不信任网络,人之初,性本恶。
秘钥是服务端告知调用方的一串字符串,像AB09Eadf之类的随机码。它由服务端告诉调用方,好比我们知道有个亲戚常来串门,那么就给他留一把备份钥匙,这样他随时想来玩就来,没有钥匙的人我们是不给他进门的。
钥匙自然不能落在陌生人手里,所以不能用明文在网络上传输。钥匙就是一件信物,只有调用方和服务端知道。服务端往往还会要求提供更完整的信息来做签名,比如服务端的接口url,调用方的请求消息体等。它们连同秘钥一起通过MD5加密后作为一个token发往服务端。
而服务端接口url、调用方的请求内容和秘钥,对服务端来说都是已知的,所以它再去做一个MD5加密,就得到了另一个token。拿服务端的token跟调用方的一比,相同就是鉴权通过,反之不通过。
下面看下鉴权的实现:
1、API:
/** * 提供给ngoc退货同步 * * @param timestamp * @param sign * @param returnOrder * @return */ @RequestMapping(value = "/order/status") Result returnOrder(@RequestHeader Long timestamp, @RequestHeader String sign, @RequestBody ReturnOrder returnOrder);
2、Controller做鉴权:
@Override public Result returnOrder(Long timestamp, String sign, ReturnOrder returnOrder) { Result result = new Result(0, "success"); try { orderService.signAuth(getUrl(), timestamp, sign, returnOrder.getOrderId(), migu_apiSecret); orderService.insertReturnOrder(returnOrder, timestamp); } catch (OrderException e) { result.setCode(e.getCode()); result.setMsg(e.getMsg()); } return result; } /** * 请求鉴权 * * @param url 服务url * @param timestamp 时间戳 * @param sign 调用方签名 * @param arg 请求消息体 * @param apiSecret 秘钥 * @throws OrderException */ public void signAuth(String url, Long timestamp, String sign, String arg, String apiSecret) { String seed = url + timestamp + arg + apiSecret; String mySign = getSign(seed); if (!mySign.equals(sign)) { log.error("request sign != md5 sign. request sign is : {}, mySign is : {}", sign, mySign); throw new OrderException(1018, "Token鉴权失败."); } long currentTime = System.currentTimeMillis(); if ((timestamp + expire_second * 1000) < currentTime) { log.error("The token has expired.request time is {}, currentTime is : {}", timestamp, currentTime); throw new OrderException(1019, "时间戳过期."); } }
有了签名,基本上数据的合法性就得到了保障。只要坏人没有秘钥就无法伪造调用方的请求。但坏人依然可以截取调用方的请求,不停的发送给服务端,造成服务端资源紧张、数据错误等不好的结果。
为了防止同样的请求多次发送,我们往往还要求调用方提供一个唯一的序列号,比如UUID、时间戳之类的。服务端需要再收到请求的第一时间校验该唯一序列号。我们可以通过spring的切面或者拦截器来做,详见springboot拦截器配置、消息头校验、重复请求过滤。