很多项目经常会有很多对外开放的接口,这些接口暴露在外网容易被篡改或攻击,因此需要设计一些安全保护的方式来增强接口安全,在网络传输层可添加 SSL 证书,在应用层主要是通过一些加密逻辑来实现,本文主要讲解接口签名的实现方式,借鉴了阿里的摘要签名认证方式 ,也是稍微复杂一点的方案。
客户端生成签名 提取签名串客户端需要从HTTP请求中提取出关键数据,组合成一个签名串,格式如下:
1 2 3 4 5 6 7 HTTPMethod Accept Content-MD5 Content-Type User-Agent Date Query
以上7个字段构成整个签名串,并且须与服务端保持一致,字段之间使用\n间隔,大小写敏感。请求中的Content-MD5头的值,可为空。只有在请求存在Body且Body为非Form形式时才计算Content-MD5头。
计算签名我用 apifox 模拟前端请求,使用它的前置脚本功能生成签名并插入到request.headers:
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 var cryptoJs = require ("crypto-js" );var uuid = require ("uuid" );let secretKey = pm.globals .get ('App-Secret' )let queryParams = pm.request .url .query let headers = pm.request .headers let params = [];let contentType = headers.get ("Content-Type" ) || "application/json;charset=utf-8" ;let ua = headers.get ("User-Agent" )let accept = "*/*" ;let contentMd5 = "" ;let ts = new Date ().getTime ()+"" ;let dateGmt = new Date ().toUTCString ();let uid = uuid.v4 ();let querystring = "" let body = "123456" let paramPair = []let keys = Object .keys (queryParams)keys.sort () queryParams.each (item => { if (!item.disabled && item.value !== undefined && item.value !== '' ) { paramPair.push (item.key +"=" +encodeURIComponent (item.value )); } }); querystring = paramPair.join ("&" ) if (body) { contentMd5 = cryptoJs.MD5 (body).toString () } params.push (pm.request .method ); params.push (accept); params.push (contentMd5) params.push (contentType) params.push (ua) params.push (dateGmt) params.push (querystring) headers.upsert ({key : "Content-MD5" , value : contentMd5}); headers.upsert ({key : "Content-Type" , value : contentType}); headers.upsert ({key : "X-Ca-Timestamp" , value : ts}); headers.upsert ({key : "X-Ca-Nonce" , value : uid }); headers.upsert ({key : "Date" , value : dateGmt }); let sign = cryptoJs.HmacSHA256 (params.join ('\n' ), secretKey).toString ()headers.add ({key : "X-Ca-Signature" ,value : sign})
这个软件功能还是挺强大的,可以为所有接口生成签名,在根目录设置里引用公共脚本即可。 脚本API可以参考帮助文档
传输签名客户端需要将以下四个Header放在HTTP请求中传给服务端,进行签名校验:
X-Ca-Timestamp:请求的时间 X-Ca-Nonce:唯一标识符 uuid x-ca-signature-method:签名算法,可选,默认为HmacSHA256; X-Ca-Signature:签名 服务端验签以Node框架eggjs为例,一般在中间件进行验签,先上代码:
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 'use strict' const crypto = require ('node:crypto' )module .exports = () => { return async function (ctx, next ) { const headers = ctx.headers if (!headers['x-ca-nonce' ] || !headers['x-ca-timestamp' ] || new Date ().getTime () - headers['x-ca-timestamp' ] > 600 ) { ctx.status = 401 return (ctx.body = { code : 401 , msg : '非法访问' , }) } const reqKey = `appname:${headers['x-ca-nonce' ]} ` const arr = [ ctx.method , headers.accept , headers['content-md5' ] || '' , headers['content-type' ], headers['user-agent' ], headers.date , query2string (ctx.query ), ] const signature = genSignature (arr.join ('\n' )) if (signature !== headers['x-ca-signature' ]) { ctx.status = 401 return (ctx.body = { code : 401 , msg : '无效签名' , }) } const val = await ctx.app .redis .get (reqKey) if (val) { ctx.status = 401 return (ctx.body = { code : 401 , msg : '非法访问' , }) } ctx.app .redis .set (reqKey, 1 , 'EX' , 60 * 30 ) await next () } } function genSignature (arr ) { const hmac = crypto.createHmac ('sha256' , 'this is an App Secret' ) hmac.update (arr.join ('\n' )) return hmac.digest ('hex' ) } function query2string (query ) { if (!query) return '' const keys = Object .keys (query) keys.sort () const paramPair = [] for (let i = 0 ; i < keys.length ; i++) { if (query[keys[i]] === '' ) { continue } paramPair.push (`${keys[i]} =${encodeURIComponent (query[keys[i]])} ` ) } return paramPair.join ('&' ) }
上面arr的参数顺序可以随机,join的连接符也可以自定义,但要注意有些平台如小程序处理Accept不一样,可以在客户端写死,验签通过后保存nounce到redis,设置ttl 30min,作用是防止短时间大量的恶意请求对服务端的压力。
加https后还有必要加签吗?有:
保证应用的数据安全和防篡改,并且可以阻止重放攻击。 https 只是在传输层加密了数据,但是可以重复大量的请求。 更多参考资料:使用摘要签名认证方式调用API Open API 中签名的使用 API接口设计:防参数篡改+防二次请求