https
可以保证信道安全,但是不能保证数据源安全,于是对请求头加签名的方案应运而生。
为什么网络请求需要添加签名?
接口中使用签名机制,其实都是为了防止发送的信息被串改,发送方通过将一些字段要素按一定的规则排序后,再转化成字符串(包含仅前后端知道的秘钥),通过MD5加密机制发送,当接收方接受到请求后需要验证该信息是否被篡改过,也需要将对应的字段按照同样的规则生成验签sign,然后在于接收到的进行比对,可以发现信息是否被串改过(仔细想想还是仿君子不防小人,毕竟前端代码都是暴露在网络上的,别有用心的人总有办法在前端海量代码里找出秘钥和加密方式)。
签名流程
签名规则 1、线下分配appid 和appsecret(仅内部系统简单使用可不用数据库管理,添加到配置文件即可),针对不同的调用方分配不同的appid和appsecret
2、加入timestamp (时间戳),10分钟内数据有效
3、加入流水号nonce (防止重复提交),至少为10位。针对查询接口,流水号只用于日志落地,便于后期日志核查。 针对办理类接口需校验流水号在有效期内的唯一性,以避免重复请求。(如果没有防止重复提交的需求此字段后端可以不做处理该逻辑,仅认为signature生成的一部分即可)
4、加入signature ,所有数据的签名信息。
签名生成 signature是有系统参数+业务参数(业务参数根据系统需要,非必须)+appsecrect共同组成,并通过MD5(32)加密生成。
对除签名signature外所有参数(系统参数+业务参数)生成键值对(按照key的ASCLL排序),最终得到:key=value&key=value……&key=value&appsecret=appsecret值(按照key的ascll顺序拼接,字段连接形式按照后端方式定义)。
将上一步顺序拼接好参数和值的字符串进行md5即可得到最的signature
参数列表 字段 类型 必传 说明 appid String 是 后台分发的appid timestamp String 是 当前时间戳 nonce String 是 流水号(后端可用于校验是否重复提交) signature String 是 接口签名,用户接口验证 name String 否 业务参数 age Integer 否 业务参数
实例 系统参数 1 2 3 appid = 142803973 timestamp = 1682041538659 nonce = d89eba6440a14515990aedd69ade61a7
业务参数 1 2 name = "uyynot" age = 18
appSecret
1 appsecret = 2dffc82386a84ff18256c6d2949e58d1b4095385552d4a8084660fb06ff39ee7
生成源串 根据字段名ascall
排序拼接键值,这个源串拼接方式不一定非得以query参数形式去拼接,实际情况按后端提供的规则拼接即可,我这里只是示例演示
1 'age=18&appid=142803973&name=uyynot&nonce=d89eba6440a14515990aedd69ade61a7×tamp=1682041538659'
拼接appSecret
1 'age=18&appid=142803973&name=uyynot&nonce=d89eba6440a14515990aedd69ade61a7×tamp=1682041538659&appsecret=2dffc82386a84ff18256c6d2949e58d1b4095385552d4a8084660fb06ff39ee7'
生成signature 将上述拼接好的源串进行md5加密即可
1 signature = md5('age=18&appid=142803973&name=uyynot&nonce=d89eba6440a14515990aedd69ade61a7×tamp=1682041538659&appsecret=2dffc82386a84ff18256c6d2949e58d1b4095385552d4a8084660fb06ff39ee7')
客户端请求 作为一个后端,模拟客户端我这里直接采用的第三方接口测试工具apipost
,说明一下,该工具没有给我一分广告费,纯属个人觉得好用。
其他几个参数都比较简单,唯一麻烦点的就是生成signature。apipost支持预执行脚本,所以我这里的timestamp和signature我是通过脚本动态写入请求头的,nonce我暂时以一个固定的字符串去模拟,另外为了简化测试后端校验签名只通过系统参数,业务参数实际应用中可以加进来。
先添加两个固定参数,timestamp和signature会通过预执行脚本动态添加到header
预执行脚本 我们将预执行的脚本写入到这里,代码如下:
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 let timestamp = Date .now ();function _create_signature ( ){ _params = { 'appid' : request.headers ['appid' ], 'nonce' : request.headers ['nonce' ] } order_params = {} Object .keys (_params)?.sort ().forEach (key => { if (_params[key]){ order_params[key]=_params[key] } }) query_params = "" for (let k in order_params){ query_params += `${k} =${order_params[k]} &` } let appsecret = '2dffc82386a84ff18256c6d2949e58d1b4095385552d4a8084660fb06ff39ee7' query_params += `appsecret=${secret} ` console .log ("query" , query_params) return $.md5 (query_params); } let x_signature = _create_signature ()apt.setRequestHeader ("timestamp" , x_timestamp); apt.setRequestHeader ("signature" , x_signature);
发送请求 点击发送按钮即可将请求发送至后端
服务器校验 服务端我这边采用的是python,可以以在中间件里拦截要校验的接口请求,流程其实跟客户端一样的,根据参数生成签名,与客户端传来的签名做校对即可,代码直接如下:
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 def create_sign (params: typing.Dict [str , str ], filter_null: bool = False ) -> str : """ 生成签名参数字符串 - 并按照参数名ASCII字典序排序 - filter_null 过滤掉字段为空时 不加入排序 :param typing.Dict[str, str] params: 需参数名 ASCII 字典序排序 的 map :param bool filter_null: 是否需要过滤字段值为空的不计入签名计算 :returns: 根据params得到排序后的签名字符串 :rtype: str """ order_params = OrderedDict() for key in sorted (params.keys()): if filter_null: if params[key]: order_params.update({key: params[key]}) else : order_params.update({key: params[key]}) params_str = "&" .join([f"{key} ={vl} " for key, vl in order_params.items()]) return params_str def validate_params (appid: str , app_secret: str , nonce: str , timestamp: int , signature: str , threshold: int = 3 * 60 * 1000 ) -> bool : """校验参数签名 :param str appid: 签名用户信息主体字段键名,由 self.key_name 控制 :param str app_secret: 签名参与计算的 密钥,不会参与网络请求明文传输 :param str nonce: 签名参与计算的 随机字符串 :param int timestamp: 签名参与计算的 13位时间戳 精确到ms :param str signature: sign签名数据值,由其他签名参数计算生成 :param int threshold: 解析计算入参 时间戳 timestamp 的 有效阈值,默认3分钟的有效期时延,即 请求后该签名在三分钟内才有效 :returns: 是否通过签名校验 :retype: bool """ _params = { 'appid' : appid, 'timestamp' : f"{timestamp} " , 'nonce' : nonce } sub_timestamp = int (time.time() * 1000 ) - timestamp val_timestamp_result = (0 <= sub_timestamp <= threshold) if not val_timestamp_result: return False _sign = create_sign(_params) _signature = hashlib.md5(f"{_sign} &appsecret={app_secret} " .encode('utf-8' )).hexdigest().lower() _params['signature' ] = _signature b = (_signature == signature) return b if __name__ == '__main__' : appid, nonce, timestamp, signature = request.META.get("appid" , "" ), request.META.get("nonce" , "" ), request.META.get("timestamp" , "" ), request.META.get("signature" , "" ) if not any ([appid, nonce, timestamp, signature]): raise AuthenticationFailed(_('Invalid token params.' )) if not validate_params(appid, "2dffc82386a84ff18256c6d2949e58d1b4095385552d4a8084660fb06ff39ee7" , nonce, timestamp, signature): raise AuthenticationFailed(_('Invalid token .' ))
以上便是整个流程及前后端的实现