腾讯云 API 网关应用认证之 JavaScript 实现的踩坑经历

1,072 阅读3分钟

这是我参与8月更文挑战的第19天,活动详情查看:8月更文挑战

腾讯云的 API 网关支持认证后请求,目的是为了保护 API 不被恶意攻击,目前,API 网关支持以下几种认证方式,免鉴权、EIAM 认证、OAuth2.0、应用认证和密钥对认证,其中密钥对认证已不推荐使用,成为了历史功能。 这里选用的是应用认证方式,实现语言为 JavaScript,虽然官方文档有详细的认证过程算法介绍,以及 JavaScript 的实现方式,但对于不熟悉认证过程的新手来说,会有一些踩坑的阶段,这篇文章介绍其一。

应用认证算法流程

使用应用认证的前置条件是在调用 API 之前就已经申请了 API 的签名密钥对(ApiAppKey 和 ApiAppSecret)并授权了对应的 API。 算法的第一步是提取签名串,即需要参与加密计算的请求信息,包含以下内容

Headers
HTTPMethod
Accept
Content-Type
Content-MD5
PathAndParameters

以上 6 个字段构成整个签名串,之间采用\n间隔连接,大小写敏感,且 Headers 必须包含x-date 客户端从 HTTP 请求中提取出关键数据组装成签名串后,需要对签名串进行加密及编码处理,形成最终的签名。步骤如下:

  • 将签名串(signing_str 签名内容)使用 UTF-8 解码后得到 Byte 数组。
  • 使用加密算法对 Byte 数组进行加密。
  • 使用 Base64 算法进行编码,形成最终的签名。

JavaScript 实现

以下是官方文档中 JavaScript 实现 JSON 请求示例代码

const https = require('https')
const crypto = require('crypto')

// 应用 ApiAppKey
const apiAppKey = 'APIDLIA6tMfqsinsadaaaaaaaapHLkQ1z0kO5n5P'
// 应用 ApiAppSecret
const apiAppSecret = 'Dc44ACV2Da3Gm9JVaaaaaaaaumYRI4CZfVG8Qiuv'

const dateTime = new Date().toUTCString()
const body = {
  arg1: 'a',
  arg2: 'b',
}
const md5 = crypto.createHash('md5').update(JSON.stringify(body), 'utf8').digest('hex')
const contentMD5 = Buffer.from(md5).toString('base64')
const options = {
  hostname: 'service-xxxxxxxx-1234567890.gz.apigw.tencentcs.com',
  port: 443,
  path: '/data',
  method: 'POST',
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
    'Content-MD5': contentMD5,
    'Content-Length': JSON.stringify(body).length,
    'x-date': dateTime,
  },
}

const signingStr = [
  `x-date: ${dateTime}`,
  options.method,
  options.headers.Accept,
  options.headers['Content-Type'],
  contentMD5,
  options.path,
].join('\n')
const signing = crypto.createHmac('sha1', apiAppSecret).update(signingStr, 'utf8').digest('base64')
const sign = `hmac id="${apiAppKey}", algorithm="hmac-sha1", headers="x-date", signature="${signing}"`
options.headers.Authorization = sign

const req = https.request(options, (res) => {
  console.log(`STATUS: ${res.statusCode}`)
  res.on('data', (chunk) => {
    console.log('BODY: ' + chunk)
  })
})
req.on('error', (error) => {
  console.error(error)
})
req.write(JSON.stringify(body))
req.end()

401 签名校验失败问题

如果严格按照上面示例请求 post 接口一切正常,但如果是 get 请求,则会返回以下提示:

"message":
"HMAC signature does not match, Server StringToSign:........."

签名校验失败,错误代码为 401,具体原因为文档里不太起眼的一条:

只有在请求存在 Body 且 Body 为非 Form 形式时才计算Content-MD5 头 也就是说,如果请求中不包含 body 信息,那么对应的签名串中的Content-MD5Content-Length则需要置空,不能不传。即对应的 headers 为:

 headers: {
    Accept: 'application/json',
    // 'Content-Type': 'application/json',
    // 'Content-MD5': contentMD5,
    // 'Content-Length': JSON.stringify(body).length,
    'x-date': dateTime,
  },

提取的签名串内容为:

const signingStr = [
  `x-date: ${dateTime}`,
  options.method,
  options.headers.Accept,
  "",
  "",
  options.path,
].join('\n')

欢迎阅读其它文章