这篇文章上次修改于 839 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
本文作者: 张强
本文链接: https://i-m.dev/posts/20190223-214629.html

之前在腾讯云注册了域名,最近想让自己的一台主机(有公网 IP,但不固定)也能通过域名访问。于是就想腾讯云有没有相关接口来实现域名解析记录的修改更新操作,这样的话,一旦本地主机的 IP 地址发生了变化,就可以了通过它自动完成记录的修改。结果确实有的,文档参见:https://cloud.tencent.com/document/api/302/4032

但是这套 API 的使用却很复杂,估计这也就是为什么腾讯云现在搞了一套 API 3.0 的东西。看了下,3.0 版本的 API 直接提供了相关 SDK,相对来说就很好使用了。但是 API 3.0 没有域名解析相关的 API!所以还得用 2.0

1. 签名鉴权

API 调用方面,其他部分倒也不是什么问题,关键在于请求参数中 Signature 参数的计算。该参数用来验证调用者的身份合法性,但签名的过程却比较复杂,经常会出错。网上的教程包括官方文档都是从怎么生成这个签名的角度进行讲解,看了半天也是知其然不知其所以然。这里反向地看待这个问题,看看调用请求被腾讯云服务器接收后,它会怎么进行验证。

假设腾讯云收到了我们发出了一个请求:

https://cns.api.qcloud.com/v2/index.php?Bar=1111&Foo=222&Signature=xxxxxx&SignatureMethod=HmacSHA256

首先作为服务器,他可以知道请求的方式(GET/POST

然后它先会把 Signature 的值(xxxxxx)记录下来

最后对除了 Signature 外的所有请求参数(key=value),按 key 的字典顺序进行排序

可以生成一个如下格式(忽略空格)的字符串(称为签名原文字符串):

请求方式 请求地址 ? 参数1=值1 & 参数2=值2 ……

如上述示例中为:

GETcns.api.qcloud.com/v2/index.php?Bar=1111&Foo=222&SignatureMethod=HmacSHA256

注意以下几点:

  • 严格区分大小写
  • 不能有任何其他无关符号
  • 参数必须按照字典序排序
  • Signature 参数不在其中
  • 请求地址不包括协议(https://
    然后,腾讯服务器会将上述字符串进行加密,加密方式(HmacSHA256/HmacSHA1)由请求参数中的 SignatureMethod 指定,加密使用的密钥为请求者的 SecretKey,然后对加密后的数据进行 base64 编码。到此服务器可以得到一个签名,将这个签名与请求参数中的 Signature 比较,如果一致,认证通过。

因为 HmacSHA 加密的不可逆性,不难知道为什么在生成上述签名原文字符串时为什么必须严格遵守那些约定,因为任何一个字节的错误都会导致加密所得结果完全不同。

2. 代码实现

2.1. API 请求

假设已经申请了如下的 API 密钥:

SecretId: AKIDz8krbsJ5yKBZQpn74WFkmLPx3gnPhESA

SecreKey: Gu5t9xGARNpq86cd98joQYCN3Cozk1qA

import base64         
import hashlib
import hmac
import json
import random
from datetime import datetime
from urllib.parse import urlencode
from urllib.request import urlopen

host = 'cns.api.qcloud.com/v2/index.php'
secret_id = 'AKIDz8krbsJ5yKBZQpn74WFkmLPx3gnPhESA'
secret_key = 'Gu5t9xGARNpq86cd98joQYCN3Cozk1qA'


def request(action, region=None, dict_arg=None, **kw_arg):
    params = dict(dict_arg) if dict_arg is not None else {}
    params.update(kw_arg)

    # 公共参数
    params['Action'] = action
    if region is not None:
        params['Region'] = region
    params['Timestamp'] = int(datetime.timestamp(datetime.now()))
    params['Nonce'] = random.randint(1, 2 ** 16 - 1)
    params['SecretId'] = secret_id
    params['SignatureMethod'] = 'HmacSHA256'
    params = {str(k): str(v) for k, v in params.items()}

    # 签名原文字符串
    text = 'GET' + host + '?' + '&'.join(k + '=' + v for k, v in sorted(params.items()))
    # 加密
    signature = hmac.new(secret_key.encode(), text.encode(), hashlib.sha256).digest()
    # base64编码并添加到参数列表中
    params['Signature'] = base64.b64encode(signature).decode()

    # 生成url,参数中可能会有特殊字符,所以需要urlencode
    url = 'https://' + host + '?' + urlencode(sorted(params.items()))
    # 发出请求
    contents = json.loads(urlopen(url).read().decode('unicode-escape'))
    # 校验是否成功
    if contents['code'] != 0:
        raise Exception(contents['message'])
    # 返回数据
    return contents['data']

参数:

  • action 根据腾讯云文档确定
  • 对于域名解析相关 API 操作,region 不用填写(毕竟又不像云主机在某个地区的机房)
  • dict_arg 已字典的形式传入接口请求参数(不需要公共请求参数)
  • kw_arg 作用与 dict_arg 等价,但是可以关键字参数的形式指定

返回:API 返回数据中的 data 字段

异常:当 API 返回的 code 不为 0(调用失败)时

2.2. 获取本机 IP

Python 标准库没有直接的实现,不过可以利用 Socket 建立一个外部连接,间接获取本机的 IP 地址。

至于外部连接的目标可以任意,比如可以使用谷歌的 DNS 服务器:

import socket
family = socket.AF_INET    # ipv6时改为socket.AF_INET6
server = '8.8.8.8'         # ipv6时改为'2001:4860:4860::8888'
s = socket.socket(family, socket.SOCK_DGRAM)
s.connect((server, 80))
my_ip = s.getsockname()[0]

2.3. 修改记录值

假设你在腾讯云拥有域名 yourdomain.com,然后已经添加了 DNS 解析记录 home(记录值随便填,待会儿修改)。

# 获取原记录值列表
records = request('RecordList', domain=domain)['records']
for record in records:
    # 找到对应的记录值
    if record['name'] == 'home':
        # 如果有修改的必要的话
        if record['value'] != my_ip:
            # 在原记录值的基础上修改
            request('RecordModify',
                    domain=domain,
                    recordId=record['id'],
                    subDomain=record['name'],
                    recordType=record['type'],
                    recordLine=record['line'],
                    value=my_ip,
                    ttl=record['ttl'],
                    mx=record['mx'])

通过运行这段代码,home.yourdomain.com 将被定向到代码中 my_ip 指定的地址,完成记录值修改操作。

最后,将上述程序设置成为每隔一定时间自动运行(linuxcrontab),就可以实现域名到本地可变 IP 主机的绑定。

(转载完)

后记:

  1. 我是用 .net core 进行开发的,用的官方提供的 SDKGithub 地址为 https://github.com/qcloudapi/qcloudapi-sdk-dotnet,使用很简单,看看例子就搞定了。
  2. 域名相关接口:https://cloud.tencent.com/document/product/302/8504
  3. 访问密钥申请地址:https://console.cloud.tencent.com/cam/capi