Java API签名校验
TopDuang 人气:01. 前言
目的:为防止中间人攻击。
场景:
- 项目内部前后端调用,这种场景只需要做普通参数的签名校验和过期请求校验,目的是为了防止攻击者劫持请求 url 后非法请求接口。
- 开放平台向第三方应用提供能力,这种场景除了普通参数校验和请求过期校验外,还要考虑 3d 应用的授权机制,不被授权的应用就算传入了合法的参数也不能被允许请求成功。
2. 签名生成策略
接下来详述场景 2,其实场景 1 也包含在场景 2 内部。
1.举例请求 url:
http://api.abc.com/a-service/orders?orderType=1001&requestFrom=IOS&pageNum=2&pageSize=10
请求参数为:
参数名 | 位置 | 备注 | 举例 |
---|---|---|---|
X-Access-Key | header | 客户端授权码,服务端提供,和 accessSecret 配对(场景 1 无此参数) | app1 |
X-Access-Token | header | 当前登录用户 token | d7b5808c3f443eb5a496225468c7e4a5 |
X-UTCTime | header | 当前发送请求时的时间 | 2022-02-16T09:12:43.083Z |
X-Random | header | 请求随机数 | 341be97d9aff90c9978347f66f945b77 |
orderType | query | 订单类型 | 1001 |
requestFrom | query | 订单来源 | IOS |
pageNum | query | 分页参数 | 10 |
pageSize | query | 分页参数 | 2 |
2.设原始参数为 stringA,stringA 中添加 X-Access-Key、X-UTCTime、X-Random 固定参数,将 stringA 内非空参数值和 header 的参数按照参数名 ASCII 码从小到大排序(字典序),使用 URL 键值对的格式(即 key1=value1&key2=value2…)拼接成字符串 stringB。
注意如下规则:
- 参数名 ASCII 码从小到大排序(字典序);
- 如果参数的值为空不参与签名;
- 参数名区分大小写;
- 验证调用返回或主动通知签名时,传送的 sign 参数不参与签名,将生成的签名与该 sign 值作校验。
// 最终拼接为stringB: orderType=1001X-UTCTime=2022-02-16T09:12:43.083ZpageSize=10X-Access-Key=app1X-Access-Token=d7b5808c3f443eb5a496225468c7e4a5pageNum=2requestFrom=IOSX-Random=341be97d9aff90c9978347f66f945b77
3.在 stringB 最后拼接上 accessSecret (密钥) 得到 stringC 字符串,并对 stringC 进行 MD5 运算,得到 sign 值。
// 最后拼上accessSecret得到stringC: orderType=1001X-UTCTime=2022-02-16T09:12:43.083ZpageSize=10X-Access-Key=app1X-Access-Token=d7b5808c3f443eb5a496225468c7e4a5pageNum=2requestFrom=IOSX-Random=341be97d9aff90c9978347f66f945b77&accessSecret=192006250b4c09247ec02edce69f6a2d // md5加密得到最终签名结果sign: sign=e1a4907ef03adee3fa8d395552814f4e
4.将原始的请求 url 拼接上 sign 形成最终的请求 url。
http://api.abc.com/a-service/orders?orderType=1001&requestFrom=IOS&pageNum=2&pageSize=10&sign=0f5a3cc534961d129a25d52d7ed8d003
5.最终请求 url 如下:
http://api.abc.com/a-service/orders?orderType=1001&requestFrom=IOS&pageNum=2&pageSize=10&sign=0f5a3cc534961d129a25d52d7ed8d003
参数名 | 位置 | 备注 | 举例 |
---|---|---|---|
X-Access-Key | header | 客户端授权码,服务端提供,和 accessSecret 配对(场景 1 无此参数) | app1 |
X-Access-Token | header | 当前登录用户 token | d7b5808c3f443eb5a496225468c7e4a5 |
X-UTCTime | header | 当前发送请求时的时间 | 2022-02-16T09:12:43.083Z |
X-Random | header | 请求随机数 | 341be97d9aff90c9978347f66f945b77 |
orderType | query | 订单类型 | 1001 |
requestFrom | query | 订单来源 | IOS |
pageNum | query | 分页参数 | 10 |
pageSize | query | 分页参数 | 2 |
6.服务端 gateway 同样做 sign 签名加密和校验,如果校验不通过则说明请求非法,直接拒绝,通过则下发到业务服务进行正常请求处理。
3. API 签名算法 Java 实现
public class SignUtil { /** * 生成签名 * * @param accessSecret accessSecret * @param url url * @param headers headers * @param body post的body体 * @param <T> body体泛型 * @return sign */ public static <T> String sign(String accessSecret, String url, Map<String, Object> headers, T body) throws IllegalAccessException { Map<String, Object> signMap = new HashMap<>(); if (headers != null) { signMap.putAll(headers); } Map<String, Object> paramMap = getUrlParams(url); if (paramMap != null) { signMap.putAll(paramMap); } Map<String, Object> bodyMap = getBodyParams(body); if (bodyMap != null) { signMap.putAll(bodyMap); } StringBuffer sb = new StringBuffer(); signMap.forEach((k, v) -> { sb.append(k).append("=").append(v).append("&"); }); sb.append("accessSecret=").append(accessSecret); return stringToMD5(sb.toString()); } private static Map<String, Object> getUrlParams(String url) { if (StringUtils.isBlank(url) || !url.contains("?")) { return null; } Map<String, Object> paramMap = new HashMap<>(); String params = url.split("\\?")[1]; for (String param : params.split("&")) { String[] p = param.split("="); paramMap.put(p[0], p[1]); } return paramMap; } private static <T> Map<String, Object> getBodyParams(T body) throws IllegalAccessException { if (body == null) { return null; } Map<String, Object> bodyMap = new HashMap<>(); for (Field field : body.getClass().getDeclaredFields()) { field.setAccessible(true); bodyMap.put(field.getName(), field.get(body)); } return bodyMap; } private static String stringToMD5(String plainText) { byte[] secretBytes = null; try { secretBytes = MessageDigest.getInstance("md5").digest( plainText.getBytes()); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("没有这个md5算法!"); } return new BigInteger(1, secretBytes).toString(16); } }
4. 测试一下
public class App { public static void main(String[] args) throws IllegalAccessException { String url = "http://api.abc.com/a-service/orders?orderType=1001&requestFrom=IOS&pageNum=2&pageSize=10"; Map<String, Object> headerMap = new HashMap<>(); headerMap.put("X-Access-Key", "app1"); headerMap.put("X-Access-Token", "d7b5808c3f443eb5a496225468c7e4a5"); headerMap.put("X-UTCTime", generateDate()); headerMap.put("X-Random", "341be97d9aff90c9978347f66f945b77"); BodyVO body = new BodyVO(100000001L, "yangcan", new Date()); String sign = SignUtil.sign("sdfsdfdsfdsfds", url, headerMap, body); System.out.println(sign); } /** * 获取当前时间的UTC格式 */ private static String generateDate() { Date now = new Date(); DateFormat format = new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy", Locale.US); return format.format(now); } @Data @AllArgsConstructor public static class BodyVO { private Long ycId; private String ycName; private Date ycTime; } }
输出:
sign = 4f52eb34b30129a8d511dc803044086b
加载全部内容