本文最后更新于:2021年6月15日 晚上
工作当中不免要与其他的公司进行打交道,比如调用对方的接口完成某项操作,或者提供接口给对方调用,这些接口可能使用者有很多公司,为了保证接口的安全性,需要设计一些方式来对接口进行保护,常见的保护措施有 IP 白名单与接口签名。 IP 白名单这种方式就不多说很简单,判断接口调用者 IP 是否在设定的白名单 IP 之中即可。但是 IP 白名单这种方式有个弊端就是维护白名单 IP 列表成了体力活,调用方增加服务器或者减少服务器就要更新白名单,对于接口提供方不是很友好,最好使用签名的方式一劳永逸,因此本文主要讲讲常用的接口签名方式,主要应用与服务端与服务端之间的接口交互。 因为生成签名过程中使用到了 appSecret,因此这种方式最好不要用于客户端与服务端之间的接口加密,appSecret 写死在 APP 中,逆向技术获取不是太困难的事情,当然非要使用,appSecret 最好通过登录时动态生成,写死的方式一向是不推荐的做法。
接口签名作用 接口签名解决了如下这些问题:
防止接口非法调用
防止接口参数被篡改
防止接口过期参数请求
防止接口请求重放
接口签名方式 一般来说,接口签名方式主要是这样的,所有的接口都需要传递这几个公共参数appKey
、 sign
、timestamp
、nonce
,sign 的计算规则为
拼接接口的所有参数,参数名按照 ASCII 码从小到大排序(字典序),拼接的格式如 k1=v1&k2=v2&k3=v3 得到 params
Base64(HMAC_SHA1(params, appSecret)),得到 sign 值
sign 加到参数中,发送请求目标接口 以上就是生成签名的过程。
验证签名的过程与之相同,就是从请求中提取所有的参数(除去 sign),然后同样的方式生成签名,然后将签名结果与请求中的 sign 参数进行比较,如果一致则验签成功否则失败。为了保证接口参数的时效性,一般会在验签之前校验 timestamp
参数是否超时,比如与当前时间相差 10 分钟则直接提示验签失败。另外为了防止请求重放,即相同参数不可重复请求,可以通过将 nonce 参数进行缓存,比如防止到 Redis 当中,设置 10 分钟的有效期,如果 nonce 存在与缓存中则提示验签失败,这样便通过 nonce 配合 timestamp 实现了请求重放。
话不多少,开始实践一下~
为了方便演示,就再 main 方法中实现签名与验签的逻辑。 在日常开发中,可以集成为 springboot starter,通过拦截器进行统一校验。如何自定义 springboot starter,可以参考我上一篇文章:实用主义之自定义 SpringBootStarter
依赖添加 因为加密算法使用到了开源工具包 commons-codec
,先添加这个依赖
<dependency > <groupId > commons-codec</groupId > <artifactId > commons-codec</artifactId > <version > 1.14</version > </dependency >
生成签名与验签 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 import org.apache.commons.codec.binary.Base64;import org.apache.commons.codec.digest.HmacAlgorithms;import org.apache.commons.codec.digest.HmacUtils;import java.time.Duration;import java.time.Instant;import java.time.LocalDateTime;import java.time.ZoneOffset;import java.util.*;import static java.time.temporal.ChronoUnit.SECONDS;public class AuthTest { public static void main (String[] args) { System.out.println("********************************* 签名 *********************************" ); long timestamp = System.currentTimeMillis(); String appKey = "testtest" ; String appSecret = "123456" ; Map<String, String> map = new HashMap<>(); map.put("appKey" , appKey); map.put("k1" , "k1" ); map.put("k2" , "k2" ); map.put("timestamp" , String.valueOf(timestamp)); String outSignData = getSignData(map); byte [] hmac = new HmacUtils(HmacAlgorithms.HMAC_SHA_1, appSecret).hmac(outSignData); String sign = new String(Base64.encodeBase64(hmac)); map.put("sign" , sign); System.out.println("outSign: " + sign); System.out.println("outSignData: " + outSignData); String outParams = JSONObject.toJSONString(map); System.out.println("outParams: " + outParams); System.out.println("\n\n********************************* 验签 *********************************" ); Map<String, String> inMap = JSONObject.parseObject(outParams, new TypeReference<Map<String, String>>() { }); String inTimeStamp = inMap.getOrDefault("timestamp" , "0" ); LocalDateTime inTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(inTimeStamp)), ZoneOffset.ofHours(8 )); Duration duration = Duration.between(inTime, LocalDateTime.now()); long seconds = duration.get(SECONDS); System.out.println("seconds: " + seconds); if (seconds > 10 * 60 ) { System.out.println("请求超时" ); return ; } String inSignData = getSignData(inMap); System.out.println("inSignData: " + inSignData); byte [] inHmac = new HmacUtils(HmacAlgorithms.HMAC_SHA_1, appSecret).hmac(inSignData); String sign2 = new String(Base64.encodeBase64(inHmac)); System.out.println("sign2: " + sign2); System.out.println("验签结果: " + sign.equals(sign2)); } public static String getSignData (Map<String, String> params) { StringBuilder content = new StringBuilder(); List<String> keys = new ArrayList<>(params.keySet()); Collections.sort(keys); for (int i = 0 ; i < keys.size(); i++) { String key = keys.get(i); if ("sign" .equals(key)) { continue ; } String value = params.get(key); if (value != null ) { content.append(i == 0 ? "" : "&" ).append(key).append("=" ).append(value); } else { content.append(i == 0 ? "" : "&" ).append(key).append("=" ); } } return content.toString(); } }
运行结果:
**** **** **** **** **** **** **** **** * 签名 **** **** **** **** **** **** **** **** * outSign: eADUCnVqcArdexcdMVcHkPJQyXU= outSignData: appKey=testtest&k1=k1&k2=k2×tamp=1590996917340 outParams: {"k1":"k1","k2":"k2","sign":"eADUCnVqcArdexcdMVcHkPJQyXU=","appKey":"testtest","timestamp":"1590996917340"}**** **** **** **** **** **** **** **** * 验签 **** **** **** **** **** **** **** **** * seconds: 0 inSignData: appKey=testtest&k1=k1&k2=k2×tamp=1590996917340 sign2: eADUCnVqcArdexcdMVcHkPJQyXU= 验签结果: true