Python Requests库安全实战指南:从SSL验证到防重放攻击

1. 项目概述:为什么我们需要一份“终极”安全指南?

如果你在用Python做网络请求,那Requests库几乎是你绕不开的工具。它简洁、优雅,一句 requests.get(url) 就能搞定大部分需求。但正是这种“简单”,让很多开发者,包括我自己,在项目初期忽略了其背后复杂的安全考量。直到某次线上服务因为一个未经验证的SSL证书被中间人攻击,或者因为接口被恶意重放攻击刷爆,我们才意识到,网络请求的安全配置不是“锦上添花”,而是“生死攸关”。

这个项目,或者说这篇总结,源于我过去几年在多个生产环境中踩过的坑、交过的“学费”。我发现,关于Requests的安全实践,网上的资料要么过于零散,只讲SSL验证;要么过于理论,把OAuth、JWT讲一遍,却没说清楚怎么和Requests结合。我需要一份能直接“抄作业”的指南,从最基础的证书验证,到应对“429 Too Many Requests”这类限流错误,再到构建防重放攻击的完整请求逻辑。所以,我决定结合最新的网络环境和常见攻击手法,整理出这份“终极实战指南”。它不仅仅是告诉你怎么配置,更重要的是解释 为什么 要这么配置,以及在不同场景下如何权衡。

无论你是正在处理支付接口的金融开发者,还是需要调用第三方API的数据工程师,抑或是担心爬虫被反爬机制拦截的爱好者,这里面的内容都能让你对Requests的安全使用有全新的认识。我们不止要让请求“发得出去”,更要让请求“发得安全、发得稳健”。

2. 核心安全风险与Requests的防御机制解析

在深入具体配置之前,我们必须先搞清楚对手是谁,以及Requests为我们提供了哪些“武器”。网络请求的安全风险是立体的,从传输层到应用层,威胁无处不在。

2.1 传输层安全:SSL/TLS证书验证是基石

这是最基础,也最容易被错误配置的一环。SSL/TLS协议的目标是保证数据在传输过程中不被窃听和篡改。而证书验证,就是确认你正在通信的服务器,确实是它声称的那一个,而不是一个钓鱼网站或中间人伪装的服务器。

Requests默认是开启SSL证书验证的( verify=True )。当你遇到著名的 SSLError CertificateError 时,很多快速但不安全的“解决方案”是简单地设置 verify=False 请绝对不要在生产环境中这样做! 这等同于拆掉了你家大门最核心的那把锁。正确的做法是理解证书验证失败的根源:

  1. 自签名证书 :常见于内网、测试环境或一些老旧系统。服务器使用的证书不是由受信任的根证书颁发机构(CA)签发的。
  2. 证书域名不匹配 :证书中的 Common Name (CN) Subject Alternative Name (SAN) 与你要访问的URL域名不一致。
  3. 证书链不完整 :服务器没有提供完整的中间证书链,导致客户端无法构建一条到受信根证书的信任路径。
  4. 系统根证书库过时/缺失 :你的Python环境或操作系统可能没有包含最新的CA根证书。

Requests依赖于 certifi 这个包来提供可靠的CA根证书库。在绝大多数情况下,保持 certifi 更新就能解决因CA证书过期导致的问题。

2.2 应用层安全:重放攻击与参数篡改

即使传输是加密的,攻击者仍然可以在应用层作恶。 重放攻击(Replay Attack) 是其中一种典型手段。攻击者拦截你的一次合法请求(例如一个支付请求),然后原封不动地、反复地向服务器重放这个请求。如果服务器没有防御机制,就会多次执行支付操作,造成资产损失。

防御重放攻击的核心在于让每个请求变得“唯一”且“过期”。常用方法包括:

  • Nonce(随机数) :每次请求携带一个服务器未曾见过的随机字符串,服务器缓存已使用的Nonce,拒绝重复的。
  • 时间戳(Timestamp) :请求中携带当前时间戳,服务器校验时间戳是否在可接受的时间窗口内(如±5分钟),拒绝过期的请求。
  • 请求签名(Signature) :将请求参数、时间戳、Nonce等按特定规则拼接后,用密钥(如API Secret)生成一个签名。服务器用同样规则验签,任何参数被篡改或请求被重放,签名都会对不上。

许多开放的API(如微信支付、阿里云)都采用了这种“签名+时间戳”的模式。我们的任务就是用Requests库,正确地构建这样的安全请求。

2.3 连接与请求控制:避免滥用与资源耗尽

除了恶意攻击,一些配置不当或程序缺陷也可能导致安全问题。例如:

  • 连接池管理不当 :未复用连接或未及时关闭连接,可能导致端口耗尽或服务器连接数超限。
  • 超时设置缺失 :没有设置超时,一个慢响应或挂死的请求会永远阻塞你的线程。
  • 重试机制野蛮 :遇到错误就无脑重试,特别是对 429 Too Many Requests (请求过多)或 5xx 服务器错误,可能加剧服务器压力,形成拒绝服务攻击(DoS)的帮凶。

Requests的 Session 对象、 timeout 参数以及配合 urllib3 的重试策略,是管理这些风险的利器。

3. 实战配置:从零构建一个安全的Requests会话

理论讲完了,我们直接上代码。我将展示如何一步步配置一个可用于生产环境的、安全的Requests会话对象。这个会话将集成证书验证、重试策略、超时控制等最佳实践。

3.1 基础安全会话搭建

首先,我们创建一个安全的会话基础。这里会使用到 requests.Session urllib3 PoolManager 进行一些底层配置。

import requests
from requests.adapters import HTTPAdapter
from urllib3.poolmanager import PoolManager
import ssl

class CustomHTTPAdapter(HTTPAdapter):
    """自定义适配器,用于更精细的SSL配置"""
    def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
        # 创建一个使用自定义SSL上下文的连接池管理器
        self.poolmanager = PoolManager(
            num_pools=connections,
            maxsize=maxsize,
            block=block,
            ssl_version=ssl.PROTOCOL_TLS_CLIENT, # 指定使用TLS客户端协议
            # 强烈建议使用默认的证书验证(verify=True)并依赖certifi
            # 此处仅为展示自定义上下文的位置,通常不需要修改
            **pool_kwargs
        )

def create_secure_session():
    """
    创建一个预配置了安全参数的Requests会话。
    返回: requests.Session 对象
    """
    session = requests.Session()
    
    # 1. 强制启用SSL证书验证,并指定证书包(默认使用certifi)
    session.verify = True  # 这是默认值,显式写出以示强调
    
    # 2. 设置默认超时(连接超时,读取超时)
    # 连接超时:建立TCP连接的最长等待时间
    # 读取超时:从服务器接收响应数据的最大允许间隔时间
    session.timeout = (3.05, 30)  # (连接超时, 读取超时) 单位:秒
    
    # 3. 使用自定义适配器,替换默认适配器
    adapter = CustomHTTPAdapter()
    session.mount('https://', adapter)
    session.mount('http://', adapter)
    
    # 4. 设置默认请求头,避免一些基础的信息泄露或兼容性问题
    session.headers.update({
        'User-Agent': 'MySecureClient/1.0 (Compatible; Python-Requests)',
        'Accept': 'application/json',
        'Accept-Encoding': 'gzip, deflate',
    })
    
    return session

# 使用示例
secure_session = create_secure_session()
try:
    response = secure_session.get('https://api.secure-example.com/data')
    response.raise_for_status()  # 如果状态码不是200,抛出HTTPError
    data = response.json()
except requests.exceptions.SSLError as e:
    print(f"SSL证书验证失败: {e}")
    # 此处应记录日志并触发告警,而不是静默失败
except requests.exceptions.Timeout as e:
    print(f"请求超时: {e}")
    # 根据业务逻辑决定是否重试
except requests.exceptions.RequestException as e:
    print(f"请求发生错误: {e}")

注意 session.timeout 是一个全局默认值,但你在发起具体请求时(如 session.get(url, timeout=(5, 60)) )仍然可以覆盖它。为不同的操作设置不同的超时是更精细的做法。

3.2 处理SSL证书异常的正确姿势

verify=True 遇到证书问题时,我们应该如何安全地处理?以下是几种场景及对策。

场景一:使用特定的自签名证书或内部CA证书

如果你的公司有内部CA,或者你信任某个特定的自签名证书,你应该将证书文件(通常是 .pem .crt 文件)提供给Requests,而不是关闭验证。

session = requests.Session()
# 指定自定义的CA证书包路径或单个证书文件路径
session.verify = '/path/to/your/certificate_bundle.pem'

# 或者,如果你有客户端的证书和私钥(双向认证)
session.cert = ('/path/to/client.cert', '/path/to/client.key')

场景二:临时调试与测试(仅限非生产环境!)

在开发或测试环境,如果不得不暂时绕过证书验证,务必将其限制在最小范围,并添加明确的警告。

import warnings
import urllib3
# 禁用不安全的请求警告(但这不是关闭验证)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

session = requests.Session()
# !!! 警告:仅用于测试 !!!
# 通过环境变量或配置严格控制,确保不会意外部署到生产环境
if DEBUG_MODE:
    session.verify = False
    # 记录一条高级别警告日志
    logging.warning("SSL certificate verification is DISABLED for debugging.")

场景三:解决证书链不完整问题

有时服务器配置不当,没有发送完整的中间证书。你可以尝试让Requests使用系统更全的证书库,或者手动将中间证书添加到信任链。

# 首先,更新你的certifi包
pip install --upgrade certifi

如果问题依旧,你可以获取缺失的中间证书(例如从 https://whatsmychaincert.com/ 获取),并将其与你的证书包合并。

3.3 实现智能重试与回退机制

无脑重试是危险的。一个健壮的重试机制应该考虑错误类型、遵循服务器的指示(如 Retry-After 头部),并采用指数退避策略。

我们将使用 urllib3.util.Retry 来配置一个智能重试器,并将其挂载到会话适配器上。

from urllib3.util import Retry
from requests.adapters import HTTPAdapter

def create_session_with_retry():
    session = requests.Session()
    
    # 定义重试策略
    retry_strategy = Retry(
        total=3,  # 最大重试次数(包括第一次请求)
        backoff_factor=1,  # 指数退避因子,等待时间 = {backoff factor} * (2 ** ({重试次数} - 1)) 秒
        status_forcelist=[429, 500, 502, 503, 504], # 遇到这些状态码才重试
        # 429 (Too Many Requests) 非常重要!需要配合respect_retry_after_header
        allowed_methods=["GET", "POST", "PUT", "DELETE"], # 默认只重试幂等方法(GET, HEAD, PUT, DELETE, OPTIONS, TRACE),这里加上POST需谨慎
        respect_retry_after_header=True, # 遵守服务器返回的Retry-After头部
    )
    
    # 创建适配器并挂载
    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount("https://", adapter)
    session.mount("http://", adapter)
    
    # 设置一个比总重试时间更长的读取超时
    # 假设每次退避等待最多 1*2^(3-1)=4秒,3次重试总等待可能约15秒,加上响应时间,设置30秒较安全
    session.timeout = (3.05, 30)
    
    return session

# 使用示例
smart_session = create_session_with_retry()
try:
    # 这个请求在遇到500错误或429错误时,会自动按照策略重试
    response = smart_session.get('https://api.example.com/unstable-endpoint')
except requests.exceptions.RetryError as e:
    print(f"在多次重试后请求仍然失败: {e}")
    # 此时应该触发降级逻辑或向上游报告错误

实操心得 :对于 POST 请求是否重试要格外小心。如果请求是 非幂等 的(例如,创建一个订单,扣减库存),重试可能导致重复执行。对于这类请求,更好的做法是在业务层实现 等幂性 (例如,通过唯一的业务流水号让服务器能识别重复请求),或者仅在连接失败等低层错误时重试,而不要在收到 5xx 状态码后重试。上面的配置将 POST 加入 allowed_methods 是假设你的API是等幂设计的,否则应该移除。

4. 构建防重放攻击的请求体系

现在,我们进入更高级的应用层安全:如何用Requests发送一个能抵御重放攻击的请求。这里我们模拟一个常见的“签名认证”API。

假设API要求如下:

  1. 每个请求必须携带 API-Key (身份标识)。
  2. 每个请求必须携带 Timestamp (UTC时间戳,格式为ISO 8601)。
  3. 每个请求必须携带 Nonce (一次性随机字符串,长度至少16位)。
  4. 签名生成规则:将请求方法、请求路径、排序后的查询参数、时间戳、Nonce用 & 连接,然后用 API-Secret 进行HMAC-SHA256签名,结果转为十六进制小写。

4.1 生成安全请求参数

我们先编写一个辅助函数来生成必要的安全头信息。

import hmac
import hashlib
import time
import uuid
from urllib.parse import urlparse, urlencode

def generate_secure_headers(api_key, api_secret, method, url, body_params=None, query_params=None):
    """
    生成防重放攻击所需的请求头。
    
    参数:
        api_key: 你的API Key
        api_secret: 你的API Secret
        method: HTTP方法,如 'GET', 'POST'
        url: 请求的完整URL
        body_params: POST/PUT等的JSON体字典 (可选)
        query_params: URL查询参数字典 (可选)
    
    返回:
        包含认证头部的字典
    """
    # 1. 生成时间戳和Nonce
    timestamp = int(time.time())  # 当前Unix时间戳
    nonce = uuid.uuid4().hex      # 生成一个随机的UUID作为Nonce
    
    # 2. 解析URL,获取路径和查询字符串
    parsed_url = urlparse(url)
    path = parsed_url.path
    # 合并传入的query_params和URL中已有的查询参数(如果有)
    existing_query = dict()
    if parsed_url.query:
        existing_query = dict(pair.split('=') for pair in parsed_url.query.split('&') if '=' in pair)
    if query_params:
        existing_query.update(query_params)
    # 对查询参数按键进行排序,并编码为字符串
    sorted_query_str = urlencode(sorted(existing_query.items())) if existing_query else ""
    
    # 3. 准备待签名的消息
    # 格式: METHOD&PATH&QUERY_STRING&TIMESTAMP&NONCE
    # 如果有请求体,可以将其JSON序列化后也加入签名串(确保顺序一致)
    message_parts = [
        method.upper(),
        path,
        sorted_query_str,
        str(timestamp),
        nonce
    ]
    if body_params and method in ['POST', 'PUT', 'PATCH']:
        # 重要:JSON序列化必须保证键的顺序一致(sort_keys=True),且无多余空格(separators)
        import json
        body_str = json.dumps(body_params, sort_keys=True, separators=(',', ':'))
        message_parts.append(body_str)
    
    message = '&'.join(message_parts)
    
    # 4. 使用HMAC-SHA256生成签名
    signature = hmac.new(
        api_secret.encode('utf-8'),
        message.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    # 5. 构造认证头部(格式可根据API要求调整)
    headers = {
        'API-Key': api_key,
        'Timestamp': str(timestamp),
        'Nonce': nonce,
        'Signature': signature,
        'Content-Type': 'application/json'  # 假设API接受JSON
    }
    return headers

# 你的密钥(应从安全的环境变量或配置中心读取,切勿硬编码)
API_KEY = "your_public_api_key_here"
API_SECRET = "your_secret_api_key_here".encode('utf-8')  # 保持bytes格式方便签名

4.2 发送带签名的请求

有了头部生成函数,我们就可以安全地调用API了。

secure_session = create_secure_session()  # 使用之前创建的带重试的会话

def make_signed_request(method, url, **kwargs):
    """
    发送一个带签名的请求。
    """
    # 提取可能存在的json数据或params
    json_data = kwargs.pop('json', None)
    params = kwargs.pop('params', None)
    
    # 生成安全头部
    auth_headers = generate_secure_headers(
        api_key=API_KEY,
        api_secret=API_SECRET,
        method=method,
        url=url,
        body_params=json_data,
        query_params=params
    )
    
    # 更新请求头
    headers = kwargs.pop('headers', {})
    headers.update(auth_headers)
    kwargs['headers'] = headers
    
    # 发送请求
    response = secure_session.request(method, url, json=json_data, params=params, **kwargs)
    return response

# 示例1:发送一个GET请求
try:
    resp = make_signed_request('GET', 'https://api.secure-service.com/v1/users', params={'page': 1})
    resp.raise_for_status()
    users = resp.json()
    print("GET请求成功")
except requests.exceptions.HTTPError as e:
    print(f"HTTP错误: {e.response.status_code} - {e.response.text}")

# 示例2:发送一个POST请求(创建资源)
new_order = {"product_id": 123, "quantity": 2}
try:
    resp = make_signed_request('POST', 'https://api.secure-service.com/v1/orders', json=new_order)
    if resp.status_code == 201:
        order = resp.json()
        print(f"订单创建成功: {order['id']}")
    else:
        # 处理业务逻辑错误
        print(f"创建失败: {resp.status_code} - {resp.text}")
except requests.exceptions.RequestException as e:
    print(f"网络请求异常: {e}")

通过这种方式,每个请求都因唯一的 Timestamp Nonce 而不同,并且任何对URL、参数或请求体的篡改都会导致签名验证失败,从而有效防止了重放攻击和参数篡改。

5. 高级话题与疑难杂症排查

即使配置了所有安全措施,在实际生产中仍会遇到各种奇怪的问题。这里记录一些我遇到过的“坑”和解决方案。

5.1 应对“429 Too Many Requests”与速率限制

429 状态码是服务器对你进行速率限制的明确信号。处理它不能只靠重试,更需要一个 退让(backoff) 队列 的策略。

策略一:遵守 Retry-After 头部 我们之前配置的 Retry 策略已经包含了 respect_retry_after_header=True ,这能处理服务器明确告知等待时间的场景。

策略二:实现自适应速率限制 对于没有明确 Retry-After 头部,但有明确速率限制(如每分钟60次)的API,需要在客户端实现限流。

import time
from threading import Lock

class RateLimiter:
    """一个简单的令牌桶速率限制器"""
    def __init__(self, requests_per_minute):
        self.rate = requests_per_minute / 60.0  # 转换为每秒令牌数
        self.tokens = self.rate * 10  # 初始桶容量,假设为10秒的配额
        self.max_tokens = self.rate * 30  # 最大桶容量,30秒的配额
        self.last_update = time.time()
        self.lock = Lock()
    
    def acquire(self):
        with self.lock:
            now = time.time()
            elapsed = now - self.last_update
            # 根据时间流逝补充令牌
            self.tokens = min(self.max_tokens, self.tokens + elapsed * self.rate)
            self.last_update = now
            
            if self.tokens >= 1:
                self.tokens -= 1
                return True  # 可以立即执行
            else:
                # 计算需要等待的时间
                wait_time = (1 - self.tokens) / self.rate
                return wait_time  # 返回需要等待的秒数

limiter = RateLimiter(requests_per_minute=60)  # 限制为每分钟60次

def make_limited_request(session, method, url, **kwargs):
    """一个受速率限制的请求包装器"""
    wait = limiter.acquire()
    if wait is not True:
        time.sleep(wait)
    return session.request(method, url, **kwargs)

# 使用示例
session = create_secure_session()
for i in range(100):
    # 这个循环会自动根据速率限制进行休眠
    response = make_limited_request(session, 'GET', 'https://api.rate-limited.com/data')
    # ... 处理响应

策略三:处理“exceeded retry limit, last status: 429” 当你看到这个错误时,意味着重试策略已经用尽,但请求仍然失败(通常是持续收到429)。此时,你的代码应该:

  1. 立即停止对该端点的进一步请求 ,进入“熔断”状态。
  2. 记录错误并触发告警 ,通知开发或运维人员。
  3. 执行降级逻辑 ,例如返回缓存数据、使用备用服务或向用户显示友好提示。

5.2 连接池与资源管理

不当的连接管理会导致“ ConnectionError ”或端口耗尽。最佳实践是复用 Session 对象,并在适当的时候清理。

# 正确做法:在应用程序生命周期内复用同一个Session
# 例如,在Flask应用中,可以将其存储在应用上下文中或使用单例
app_session = create_secure_session()

# 错误做法:为每个请求都创建一个新的Session
def bad_request():
    session = requests.Session()  # 每次创建新Session,连接不被复用
    return session.get(url)

# 在长时间运行的程序结束时,可以显式关闭连接池(通常不是必须的,但更规范)
app_session.close()

5.3 常见错误排查速查表

错误信息/现象 可能原因 排查步骤与解决方案
SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] 1. 自签名证书
2. 证书过期
3. 证书域名不匹配
4. 中间证书缺失
1. 检查URL域名与证书域名是否一致。
2. 使用 openssl s_client -connect host:443 -showcerts 查看服务器证书链。
3. 更新 certifi 包: pip install --upgrade certifi
4. 如为内部服务,获取正确的CA证书文件并设置 session.verify 为其路径。
ModuleNotFoundError: No module named 'requests' Python环境中未安装requests库。 1. 确认虚拟环境已激活。
2. 运行 pip install requests 安装。
3. 检查PyCharm等IDE的Python解释器设置。
ReadTimeout / ConnectTimeout 1. 网络不稳定或服务器响应慢。
2. 超时时间设置过短。
1. 使用 session.timeout 或单次请求的 timeout 参数适当增加超时时间。
2. 实现重试机制(见3.3节)。
3. 检查服务器状态和网络链路。
ConnectionError: [Errno 61] Connection refused [Errno 111] Connection refused 目标服务器端口未开放或服务未启动。 1. 使用 telnet nc 命令测试目标主机和端口是否可达。
2. 检查防火墙规则。
3. 确认服务进程是否在运行。
TooManyRedirects 请求陷入了重定向循环。 1. 检查请求的URL是否正确。
2. 使用 session.get(url, allow_redirects=False) 禁止重试以查看中间响应。
3. 检查服务器端重定向逻辑。
间歇性 RemoteDisconnected / ProtocolError 服务器端主动关闭了连接,可能由于Keep-Alive超时或服务器负载高。 1. 在适配器中调整连接池参数,如减少 pool_connections pool_maxsize
2. 为请求添加更完善的异常捕获和重试逻辑。
3. 联系服务器端检查其配置。
请求成功但数据乱码 服务器返回的编码与Requests推断的编码不一致。 1. 手动指定响应编码: response.encoding = 'utf-8'
2. 从响应头 Content-Type 中获取编码信息。
3. 使用 response.content 获取bytes,再手动解码。

5.4 调试与日志记录

在生产环境中,详细的日志记录是排查问题的生命线。为你的Requests会话配置日志。

import logging
import http.client

# 启用urllib3的调试日志(级别很高,通常只在调试时开启)
http.client.HTTPConnection.debuglevel = 1
logging.basicConfig(level=logging.DEBUG)

# 更实用的做法:配置一个自定义的日志处理器,记录请求摘要和错误
def setup_request_logging(session):
    """为Session配置请求/响应日志"""
    import curlify  # 一个方便的库,可以将请求转换为cURL命令,便于调试。需要安装:pip install curlify
    
    def response_hook(response, *args, **kwargs):
        # 记录请求摘要
        logging.info(f"Request: {response.request.method} {response.request.url} - Status: {response.status_code}")
        # 记录耗时
        if hasattr(response, 'elapsed'):
            logging.info(f"Request took: {response.elapsed.total_seconds():.2f}s")
        # 如果状态码不是2xx,记录更多信息
        if not response.ok:
            logging.error(f"Request failed. cURL equivalent: {curlify.to_curl(https://blog.csdn.net/response.request)}")
            logging.error(f"Response body: {response.text[:500]}")  # 只记录前500字符
        return response
    
    # 将钩子函数添加到Session
    session.hooks['response'].append(response_hook)
    return session

# 使用
logged_session = setup_request_logging(create_secure_session())
response = logged_session.get('https://httpbin.org/status/404')

安全不是一次性的配置,而是一个持续的过程。从强制SSL证书验证开始,到精心设计重试与限流策略,再到为每个关键请求加上防重放的签名,每一层都在加固你的应用防线。Requests库的强大之处在于它的可扩展性,允许我们通过 Session Adapter Hooks 等机制,将所有这些最佳实践封装成一套稳固的请求体系。下次当你写下 requests.get() 时,不妨先花几分钟想想,这个请求是否足够“安全”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值