尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

AI代理安全登录网站办杂务:不泄露密码的令牌与保险箱工程方案

AI代理安全登录网站办杂务:不泄露密码的令牌与保险箱工程方案 最近在接一个内部工具项目时一直绕不开这么一个问题团队希望让 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 cryptography3.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_tokenOAuth 2.0 方案中代理从授权服务器获取两个核心令牌access_token短期有效的身份凭证代理每次调用业务网站 API 时带上它。refresh_token长期有效的“恢复凭证”但它不能直接访问业务资源只能用于换取新的 access_token。典型生命周期如下用户授权后代理拿到 access_token有效期 30 分钟和 refresh_token代理用 access_token 访问业务网站access_token 过期后代理返回 401代理调用授权服务器的刷新接口用 refresh_token 换取新的 access_token用户主动撤回授权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(port5001)这个网站只认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(port5002)注意这里的密码模式只是为了演示令牌交换流程。真实系统应该避免密码模式优先用授权码模式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: fBearer {self.access_token}} resp requests.get(f{BUSINESS_SITE}/api/orders/report, headersheaders) if resp.status_code 401: # 令牌过期刷新后重试一次 self._refresh_access_token() headers {Authorization: fBearer {self.access_token}} resp requests.get(f{BUSINESS_SITE}/api/orders/report, headersheaders) 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_USERzhangsan export AGENT_PASSWORD123456最后运行代理服务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: fBearer {token}}如果 token 没有过期但仍然 401需要确认 token 的 audience 与目标业务网站是否匹配。OAuth2 中 access_token 通常绑定特定资源服务器不能跨系统使用。6.3 refresh_token 刷新失败常见原因refresh_token 因为用户改密、登录异常已被服务端撤销refresh_token 过期客户端 IP 或设备信息发生变化触发安全策略refresh_token 存的是旧值解密失败。排查时先看授权服务器返回的错误码再检查令牌保险箱中的 refresh_token 是否被意外重新生成。6.4 代理日志里出现密码这是最需要警惕的问题。出现原因通常有三种代码里把请求参数完整打印出来异常对象被直接转为字符串后记录日志环境变量中密码被某个通用日志组件捕获。一旦日志中出现密码必须立即从日志系统删除对应记录撤销相关 refresh_token修改用户密码增加日志脱敏组件例如用***替换 password 字段审计是否还有其他敏感字段被记录。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会话 IDuser_id授权用户agent_id代理实例action操作名称target_resource目标 URL/APIresult成功/失败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 代理真正适合的定位是“大脑”和“执行者”而不是“密码保管员”。如果接下来你想继续深入建议按这个顺序学习把 OAuth 2.0 和 OIDC 的授权码流程完整跑通尤其是 PKCE 的随机码生成逻辑研究一下企业级 SSO 方案掌握 SAML、LDAP 与 OIDC 的对接差异学习密钥管理服务的用法把令牌保险箱的密钥交给专门的 KMS 管理在真实业务中给代理增加“高风险操作二次确认”机制并建立配套审计报表。最后给你一句项目落地建议不要等系统上线后再补安全设计而是先把“密码不经过代理”这条红线画好再开始写第一行业务代码。如果你在代理登录方案上还有更具体的场景比如旧系统改造、多租户隔离或扫码登录对接欢迎在评论区交流我会针对实际问题继续展开。
返回列表