☰
AI代理安全登录网站办杂务:不泄露密码的令牌与保险箱工程方案
2026/10/24 15:22:03 网站建设 项目流程

最近在接一个内部工具项目时,一直绕不开这么一个问题:团队希望让 AI 代理自己去登录几个业务网站,定时把报表下载下来、把表单填好、把待办事项处理掉。一开始大家想得最简单——直接把网站账号密码写在配置里,让脚本登录就行。但只要稍微往生产环境推一步,就发现这条路根本走不通:密码放谁那里都危险,代理权限没法收敛,审计日志完全没有,出问题根本说不清是谁在什么时候干了什么。

后来我把整套方案重新设计了一遍,核心原则就一条:AI 代理可以代替你“办事”,但绝不能让密码因为代理而流动起来。文章会从身份鉴权模型、令牌生命周期、密码托管、审计日志几个角度拆解,并给出一套可以用 Python 实现的完整示例,覆盖“代理登录网站办杂务但不泄露密码”的真实工程诉求。适合正在做 AI Agent、自动化运维、RPA 替代方案,或者打算把 ChatGPT / Codex 这类 AI 能力接入内部业务系统的读者。

1. 背景与核心概念

1.1 什么是“代理登录网站办杂务”

“代理登录网站办杂务”并不是一个新概念。早年的 RPA(机器人流程自动化)就是让软件机器人模拟人工操作,自动填写网页表单、下载报表、批量上传文件。现在大语言模型能力变强之后,这类任务可以交给 AI 代理来完成,例如:

  • 登录内部 OA 系统,自动提交周报;
  • 登录云管理后台,拉取账单或资源清单;
  • 登录电商卖家后台,同步订单状态;
  • 登录招聘网站,筛选简历并标记候选人;
  • 登录财务系统,下载银行流水并归类。

这些场景的共同点是:代理需要“代表一个真实用户”去访问某个受保护的网站,执行一系列重复、规则清晰、人工操作成本高的任务。

关键问题在于:网站只认得用户名和密码,而代理是程序,程序处理密码的方式和人类完全不同。如果简单地把密码交给代理脚本,风险会急剧上升。换句话说,真正需要实现的不只是“代理能登录”,而是“代理在最小授权范围内登录,并且密码全程不暴露给代理自身和其他中间环节”。

1.2 为什么“不泄露密码”如此重要

密码一旦被代理脚本读取,就等于被写进了环境变量、配置文件、容器镜像、日志目录等任何它可能经过的位置。出现下面任何一种情况,密码都会快速扩散:

  • 脚本异常时打印了请求参数,密码出现在堆栈日志里;
  • 配置文件被提交到 Git 仓库,密码进入版本历史;
  • 代理被提示注入攻击,把环境变量内容回显给用户;
  • 服务器被入侵后,攻击者直接读取进程环境变量拿到密码。

密码是一次性泄露、长期有效的凭据。一旦泄露,除非人工改密,否则攻击者可以无限期使用。而代理场景下,会话通常是短期的、有明确作用域的,因此更合理的方式,是让代理拿到临时身份凭证,用完即失效,而不是掌握永久的密码。

1.3 “ChatGPT Work”能做什么,不能做什么

在 ChatGPT 生态逐步扩展后,很多同学开始关注“ChatGPT Work”或 Codex 是不是可以帮你自动登录网站、代办杂务。需要明确一点:大模型本身不做账号登录,它作为一个代理大脑,负责理解任务、规划步骤、调用工具;真正的登录动作仍然要由运行环境中的工具链完成。

所以“ChatGPT Work 可代登录网站办杂务且不泄露密码”这句话,正确的理解应该是:

  • 由 ChatGPT 这类模型负责任务编排和理解;
  • 由独立的身份代理组件负责与目标网站的认证交互;
  • 密码被隔离在密码保险箱中,模型、代理脚本、日志都不能直接看到明文;
  • 最终通过短期令牌、委托授权等方式让目标网站承认代理的“临时身份”。

一句话概括:模型负责聪明地干活,安全模块负责规矩地登录。

2. 安全模型与核心设计原则

2.1 从“保存密码”转向“委托授权”

最传统的方式是“代理保存你的密码”,这相当于你把房子的钥匙直接交给了一个陌生人。更安全的模型是“委托授权”,类似于你去酒店前台临时办理一张房卡,卡上写明了可以进哪些房间、几点失效、能不能带人进入。

互联网协议中提供了一套标准的委托授权机制,即 OAuth 2.0。用户通过授权页面明确同意代理“代操作”某些资源,授权服务器颁发 access_token 给代理。代理拿着 access_token 去访问业务网站,业务网站只验证 access_token 的有效性和权限范围,不需要知道用户名和密码。

如果目标网站支持 OAuth 2.0、OIDC 或企业内部的 SSO 协议,优先采用这种方案。如果目标网站只支持普通账号密码登录,则需要在代理架构中增加一层“受控密码托管与短期会话”设计,让代理尽量不直接接触明文密码。

2.2 最小权限原则

代理代办杂务时,必须遵循最小权限原则。例如:

  • 只需要下载报表,就不给提交审批的权限;
  • 只需要读取订单,就不给修改订单的权限;
  • 只需要处理某个部门的数据,就不给全公司数据权限;
  • 只需要运行 10 分钟的任务,就不给 7 天有效的令牌。

这听起来很基础,但在实际代理设计中很容易被忽略。很多团队为了省事,直接给代理一个管理员账号,等于把整个系统暴露给 AI。即使 AI 本身没有恶意,提示注入攻击也可能诱导代理执行越权操作。

2.3 会话隔离与临时身份

代理的每一次任务都应该运行在独立的会话中。即便代理在任务中被诱导读取了 access_token,令牌的有效期很短,攻击者能利用的窗口被压缩到分钟级。同时,因为令牌绑定了 scope(权限范围)和 audience(目标服务),攻击者拿到令牌后也无法跨系统复用。

会话隔离还有利于审计。每个会话有独立 ID,可以把日志中的操作行为与某次任务、某个代理实例关联起来。一旦发现异常,可以快速撤回对应的会话,而不是推翻整个系统重建凭据。

3. 环境准备与工程结构

3.1 运行环境

本文示例以常见 Python 3.9+ 环境为例,操作系统可以是 Windows、macOS 或 Linux。具体版本需要根据你的项目实际情况调整,以下只是演示环境:

组件说明
Python3.9 或更高版本
Flask用来实现演示用的业务网站和授权服务
requests代理端发起 HTTP 请求
cryptography用于加密保存刷新令牌等敏感信息
SQLite演示用数据库,生产可替换为 MySQL/PostgreSQL

安装依赖:

pip install flask requests cryptography

3.2 演示项目结构

ai-agent-demo/ ├── auth_server.py # 模拟 OAuth 授权服务器 ├── business_site.py # 模拟目标业务网站 ├── agent_service.py # AI 代理代办服务 ├── token_vault.py # 加密令牌保险箱 ├── config.py # 基础配置 ├── requirements.txt └── README.md

这个结构只是为了把概念讲清楚。生产环境通常还需要接入 Redis、数据库、消息队列,并把代理服务拆成独立部署单元,但核心安全思路完全一致。

4. 核心技术拆解

4.1 令牌生命周期:access_token 与 refresh_token

OAuth 2.0 方案中,代理从授权服务器获取两个核心令牌:

  • access_token:短期有效的身份凭证,代理每次调用业务网站 API 时带上它。
  • refresh_token:长期有效的“恢复凭证”,但它不能直接访问业务资源,只能用于换取新的 access_token。

典型生命周期如下:

  1. 用户授权后,代理拿到 access_token(有效期 30 分钟)和 refresh_token;
  2. 代理用 access_token 访问业务网站;
  3. access_token 过期后,代理返回 401;
  4. 代理调用授权服务器的刷新接口,用 refresh_token 换取新的 access_token;
  5. 用户主动撤回授权,refresh_token 立即失效。

这里有一个容易被忽略的工程点:refresh_token 的保存方式比 access_token 更重要。access_token 生命周期短,泄露损失可控;refresh_token 生命周期长,一旦泄露就等于获得了“无限续期”的能力。所以 refresh_token 必须加密存储,并且与代理实例、用户、设备绑定。

4.2 密码保险箱:代理不直接接触明文密码

有些老旧业务网站没有 OAuth 能力,只有账号密码。这时我们要在架构中增加一个“密码保险箱”(Token Vault)。密码保险箱负责:

  • 加密存储用户的账号密码;
  • 每次任务启动时,由保险箱自动完成登录,获取业务网站的 session cookie;
  • 只把短期有效的 session cookie 交给代理;
  • 明文密码不出保险箱,不进入代理进程内存。

这个思路本质上是把“密码使用”和“密码存储”分离。代理只需要关心 session cookie,不用关心密码怎么来的。

4.3 审计日志与可追溯性

安全设计不能只看“是否能登录”,还要看“登录后做了什么”。代理代办杂务时,每一步操作都应记录:

  • 操作人:实际上是哪个用户授权的;
  • 代理身份:是哪个代理实例;
  • 会话 ID:本次任务的唯一标识;
  • 目标资源:访问了哪个 URL、哪个 API;
  • 操作时间:精确到毫秒;
  • 结果:成功或失败,失败原因是什么。

审计日志不仅是安全合规的要求,也是排查问题的主要手段。当业务方质疑“这个操作是不是 AI 自动做的”时,审计日志能给出明确答案。

5. 完整实战案例:让代理代办订单报表下载任务

下面我们实现一个完整的迷你系统。场景是:有一个模拟业务网站,用户希望代理“登录网站并下载订单报表”,但代理拿不到用户密码。

5.1 模拟业务网站

文件:business_site.py

# business_site.py """ 模拟业务网站,只负责校验访问令牌,不关心用户名密码。 """ from functools import wraps from flask import Flask, jsonify, request app = Flask(__name__) # 模拟一份有效的 access_token,生产环境应由授权服务器签发并验签 VALID_TOKENS = { "demo-access-token": { "user": "zhangsan", "scope": ["order:read"] } } def require_token(f): @wraps(f) def wrapper(*args, **kwargs): auth = request.headers.get("Authorization", "") token = auth.replace("Bearer ", "") if auth.startswith("Bearer ") else "" token_info = VALID_TOKENS.get(token) if not token_info: return jsonify({"error": "invalid_token"}), 401 request.token_info = token_info return f(*args, **kwargs) return wrapper @app.route("/api/orders/report") @require_token def order_report(): """只有 order:read 权限的代理才能访问。""" scope = request.token_info.get("scope", []) if "order:read" not in scope: return jsonify({"error": "insufficient_scope"}), 403 return jsonify({ "user": request.token_info["user"], "report": "2025-Q1 order summary", "rows": [{"order_id": "1001", "amount": 299}, {"order_id": "1002", "amount": 199}] }) if __name__ == "__main__": app.run(port=5001)

这个网站只认Authorization请求头中的 access_token。如果令牌无效,返回 401;如果令牌权限不够,返回 403。用户密码在这个网站中根本不存在。

5.2 模拟授权服务器

文件:auth_server.py

# auth_server.py """ 模拟简化版授权服务器:负责颁发 access_token 和 refresh_token。 演示仅做基础校验,生产中请使用正规 OAuth2/OIDC 服务器。 """ import time import uuid from flask import Flask, jsonify, request app = Flask(__name__) # 演示用存储:refresh_token -> 用户信息 REFRESH_TOKENS = {} # 模拟用户密码校验 USERS = { "zhangsan": "123456" # 仅演示用,生产环境绝对不能存明文密码 } @app.route("/oauth/token", methods=["POST"]) def issue_token(): grant_type = request.form.get("grant_type") # 密码模式:演示用,生产环境不推荐 if grant_type == "password": username = request.form.get("username") password = request.form.get("password") if USERS.get(username) != password: return jsonify({"error": "invalid_grant"}), 401 refresh_token = uuid.uuid4().hex access_token = uuid.uuid4().hex REFRESH_TOKENS[refresh_token] = {"user": username, "scope": ["order:read"]} return jsonify({ "access_token": access_token, "token_type": "Bearer", "expires_in": 1800, "refresh_token": refresh_token, "scope": "order:read" }) # 刷新模式:用 refresh_token 换新的 access_token if grant_type == "refresh_token": refresh_token = request.form.get("refresh_token") info = REFRESH_TOKENS.get(refresh_token) if not info: return jsonify({"error": "invalid_grant"}), 401 new_access_token = uuid.uuid4().hex return jsonify({ "access_token": new_access_token, "token_type": "Bearer", "expires_in": 1800, "refresh_token": refresh_token, "scope": info["scope"] }) return jsonify({"error": "unsupported_grant_type"}), 400 if __name__ == "__main__": app.run(port=5002)

注意:这里的密码模式只是为了演示令牌交换流程。真实系统应该避免密码模式,优先用授权码模式(Authorization Code + PKCE)。

5.3 加密令牌保险箱

文件:token_vault.py

# token_vault.py """ 令牌保险箱:加密保存 refresh_token,避免明文落盘。 """ import os from cryptography.fernet import Fernet class TokenVault: def __init__(self, key: bytes = None): if key is None: key = os.environ.get("VAULT_KEY", "").encode() if not key: raise ValueError("必须提供 VAULT_KEY 环境变量") self.cipher = Fernet(key) def save_refresh_token(self, user: str, refresh_token: str): encrypted = self.cipher.encrypt(refresh_token.encode()) # 生产环境写入数据库,这里写入本地文件演示 with open(f"{user}.token", "wb") as f: f.write(encrypted) def load_refresh_token(self, user: str) -> str: with open(f"{user}.token", "rb") as f: encrypted = f.read() return self.cipher.decrypt(encrypted).decode() if __name__ == "__main__": # 生成一个密钥,生产环境请妥善保管,不能写死在代码里 key = Fernet.generate_key() print(key.decode())

生产环境中,VAULT_KEY应该放在密钥管理服务中,例如云厂商的 KMS、Vault 或环境变量管理平台,并且定期轮换。

5.4 AI 代理代办服务

文件:agent_service.py

# agent_service.py """ AI 代理代办服务:代替用户下载订单报表,但全程不接触密码。 """ import os import requests from token_vault import TokenVault BUSINESS_SITE = "http://127.0.0.1:5001" AUTH_SERVER = "http://127.0.0.1:5002" class AgentService: def __init__(self, user: str, password: str): self.user = user self.password = password self.vault = TokenVault() self.access_token = None def _obtain_access_token(self): """ 第一步:向授权服务器换取 access_token 和 refresh_token。 这里的密码由用户输入后立即使用,不会被代理保存。 """ resp = requests.post(f"{AUTH_SERVER}/oauth/token", data={ "grant_type": "password", "username": self.user, "password": self.password, }) resp.raise_for_status() data = resp.json() self.access_token = data["access_token"] # 刷新令牌加密保存,而不是直接放在内存或配置文件 self.vault.save_refresh_token(self.user, data["refresh_token"]) return self.access_token def _refresh_access_token(self): """ 第二步:访问令牌过期后,用 refresh_token 换取新的 access_token。 """ refresh_token = self.vault.load_refresh_token(self.user) resp = requests.post(f"{AUTH_SERVER}/oauth/token", data={ "grant_type": "refresh_token", "refresh_token": refresh_token, }) resp.raise_for_status() data = resp.json() self.access_token = data["access_token"] return self.access_token def download_report(self): """ 核心任务:下载订单报表。 代理只负责发起请求,业务网站只认 access_token。 """ if not self.access_token: self._obtain_access_token() headers = {"Authorization": f"Bearer {self.access_token}"} resp = requests.get(f"{BUSINESS_SITE}/api/orders/report", headers=headers) if resp.status_code == 401: # 令牌过期,刷新后重试一次 self._refresh_access_token() headers = {"Authorization": f"Bearer {self.access_token}"} resp = requests.get(f"{BUSINESS_SITE}/api/orders/report", headers=headers) resp.raise_for_status() return resp.json() if __name__ == "__main__": user = os.environ.get("AGENT_USER", "zhangsan") password = os.environ.get("AGENT_PASSWORD", "123456") service = AgentService(user, password) report = service.download_report() print("报表下载成功:") print(report)

5.5 运行与验证

先启动业务网站和授权服务器:

python business_site.py # 新开一个终端 python auth_server.py

生成一个加密密钥,并设置环境变量:

python -c "from token_vault import Fernet; print(Fernet.generate_key().decode())" export VAULT_KEY='上面生成的密钥' export AGENT_USER='zhangsan' export AGENT_PASSWORD='123456'

最后运行代理服务:

python agent_service.py

预期输出:

报表下载成功: {'user': 'zhangsan', 'report': '2025-Q1 order summary', 'rows': [{'order_id': '1001', 'amount': 299}, {'order_id': '1002', 'amount': 199}]}

整个过程里,agent_service.py确实在构造请求时短暂处理过密码,但密码没有进入配置文件、没有写入日志、没有保存到数据库。任务完成后,代理内存中只留有 access_token,而长期存活能力的 refresh_token 被加密后放在令牌保险箱中。

5.6 这个示例说明了什么

这个示例的核心价值在于,把“登录”和“办事”拆成了两个安全边界不同的环节:

  • 登录环节由密码交换成短期令牌,密码使用后即销毁;
  • 办事环节只看到短期令牌,令牌过期后自动刷新;
  • 刷新令牌存储在加密保险箱,代理进程即使被 dump 也无法直接获取明文凭据。

如果在真实系统中接入 ChatGPT 代理,那么“任务理解、步骤规划、结果总结”全部交给大模型,而“访问目标网站”统一通过这个代理网关完成。大模型永远拿不到密码,也拿不到 refresh_token,最多只能在需要调用业务 API 时获得一个短期的 access_token。

6. 常见问题与排查思路

6.1 业务网站不支持 OAuth 怎么办

如果业务网站只支持账号密码登录,并且你能控制该网站的前端代码,建议尽快为网站增加 OAuth/OIDC 能力。如果网站是第三方系统,无法改造,可以考虑由密码保险箱统一完成登录并导出短期 session cookie,代理只使用 cookie,不知晓密码。

6.2 代理拿到 401 异常

先检查 access_token 是否过期,然后检查授权头格式是否正确:

headers = {"Authorization": f"Bearer {token}"}

如果 token 没有过期但仍然 401,需要确认 token 的 audience 与目标业务网站是否匹配。OAuth2 中 access_token 通常绑定特定资源服务器,不能跨系统使用。

6.3 refresh_token 刷新失败

常见原因:

  • refresh_token 因为用户改密、登录异常已被服务端撤销;
  • refresh_token 过期;
  • 客户端 IP 或设备信息发生变化,触发安全策略;
  • refresh_token 存的是旧值,解密失败。

排查时先看授权服务器返回的错误码,再检查令牌保险箱中的 refresh_token 是否被意外重新生成。

6.4 代理日志里出现密码

这是最需要警惕的问题。出现原因通常有三种:

  • 代码里把请求参数完整打印出来;
  • 异常对象被直接转为字符串后记录日志;
  • 环境变量中密码被某个通用日志组件捕获。

一旦日志中出现密码,必须立即:

  1. 从日志系统删除对应记录;
  2. 撤销相关 refresh_token;
  3. 修改用户密码;
  4. 增加日志脱敏组件,例如用***替换 password 字段;
  5. 审计是否还有其他敏感字段被记录。

6.5 用户主动撤回授权后代理还能继续访问

如果用户已经在授权服务器侧撤销了 refresh_token,代理在刷新时会收到invalid_grant。但要注意,原有 access_token 在过期前仍然有效。OAuth2 的撤销机制不是实时切断正在进行的会话,而是阻止未来续期。需要立即切断时,需要在业务网站侧维护一个黑名单或调用即时吊销接口。

7. 最佳实践与工程建议

7.1 优先使用授权码模式

前文示例为了演示方便使用了密码模式,但生产环境非常不建议。推荐使用Authorization Code + PKCE模式,授权码模式下用户密码只在登录页输入一次,代理全程接触不到密码。即使代理所在的服务器被完全控制,攻击者也拿不到用户密码。

7.2 access_token 有效期尽可能短

把 access_token 的有效期设置成 10 到 30 分钟,足够完成一次代办任务即可。有效期越短,令牌泄露的破坏窗口越窄。对于耗时较长的任务,让代理通过刷新接口不断换取新令牌,而不是一开始就申请一个几小时有效的令牌。

7.3 refresh_token 必须绑定客户端身份

refresh_token 不能是孤立的字符串。要把它绑定到:

  • 用户 ID;
  • 客户端 ID;
  • 设备指纹;
  • IP 段。

任何一项不匹配,都拒绝刷新。这样可以避免 refresh_token 被复制到其他机器后长期滥用。

7.4 建立完善的审计体系

表结构可以参考:

字段说明
id主键
session_id会话 ID
user_id授权用户
agent_id代理实例
action操作名称
target_resource目标 URL/API
result成功/失败
error_msg错误信息
created_at创建时间

每次代理调用业务 API 前,先写入“准备请求”审计记录;请求结束后更新结果字段。这样既能追踪行为,也能根据失败率快速定位网络或权限问题。

7.5 对 AI 代理进行提示注入防护

AI 代理最大的安全风险不是密码存储,而是提示注入。当代理要访问网页内容、处理外部输入时,恶意内容可能诱导代理读取环境变量、绕过权限检查、执行未授权操作。建议:

  • 代理的“工具调用”权限必须和“内容理解”分离;
  • 任何外部输入都不能直接变成工具调用的参数;
  • 高风险操作(转账、删除、发布)必须二次确认;
  • 代理进程运行在独立容器,使用非 root 用户,文件系统只读。

7.6 定期轮换密钥与令牌

即使没有发现泄露,也应该定期轮换:

  • 业务网站的 access_token 有效期调短;
  • refresh_token 每 30 天强制重新授权一次;
  • 令牌保险箱的加密密钥每 90 天轮换;
  • 数据库中的历史令牌定期清理。

7.7 安全测试

上线前建议做这几类测试:

  • 模拟 access_token 泄露后能否在过期前被滥用;
  • 模拟 refresh_token 被复制到其他 IP 后是否还能刷新;
  • 恶意网页内容诱导代理读取环境变量,系统是否能够拦截;
  • 日志中是否出现 password、token 等敏感字段。

8. 总结与学习路线

本文从“让 AI 代理代替用户登录网站办杂务”的真实需求出发,梳理了一套“不泄露密码”的安全方案。核心思路是不要把密码交给代理,而是通过短期令牌、委托授权、加密保险箱和审计日志,把身份能力与业务执行彻底分离。ChatGPT / Codex 这类 AI 代理真正适合的定位是“大脑”和“执行者”,而不是“密码保管员”。

如果接下来你想继续深入,建议按这个顺序学习:

  1. 把 OAuth 2.0 和 OIDC 的授权码流程完整跑通,尤其是 PKCE 的随机码生成逻辑;
  2. 研究一下企业级 SSO 方案,掌握 SAML、LDAP 与 OIDC 的对接差异;
  3. 学习密钥管理服务的用法,把令牌保险箱的密钥交给专门的 KMS 管理;
  4. 在真实业务中给代理增加“高风险操作二次确认”机制,并建立配套审计报表。

最后给你一句项目落地建议:不要等系统上线后再补安全设计,而是先把“密码不经过代理”这条红线画好,再开始写第一行业务代码。

如果你在代理登录方案上还有更具体的场景,比如旧系统改造、多租户隔离或扫码登录对接,欢迎在评论区交流,我会针对实际问题继续展开。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询