
本文详解为何基于服务账号的 id token 认证无法与 compute engine vm 的自托管服务(如 redis api)正常通信,并指出根本原因在于 google 的服务账号身份验证机制不适用于直接面向公网 ip 的非托管服务,同时提供正确实现方案与关键避坑指南。
Google Cloud 的 IDTokenCredentials(即 service_account.IDTokenCredentials.from_service_account_file)并非用于向任意公网 IP + 端口发起“带身份认证的 HTTP 请求”,而是专为 Google 托管服务(如 Cloud Run、Cloud Functions、IAP-protected backends)设计的身份验证机制。其核心逻辑是:生成一个短期 JWT ID Token,以 target_audience 作为该 Token 的 aud(受众)声明,由 Google 的身份验证中间件(如 IAP 或 Cloud Run 的内置 Auth)负责校验该 Token 的签名、时效性及 aud 是否匹配其自身服务标识。
然而,你的 Compute Engine VM 上运行的是完全自托管的容器服务(如 Flask/FastAPI + Redis),它不具备任何内置的 Google ID Token 验证能力。你当前的代码:
target_audience = f"https://{ip}:{port}"
creds = service_account.IDTokenCredentials.from_service_account_file(
KEY_FILE, target_audience=target_audience)会生成一个 aud 为 https://
✅ 正确方案:使用服务账号密钥进行服务端身份校验(而非客户端 Token 注入)
服务端(VM 容器内)需主动验证请求来源:
在你的应用中(例如 Python FastAPI),添加中间件,从 Authorization: Bearer提取 JWT,并使用 Google 的公钥集(https://www.googleapis.com/oauth2/v3/certs)验证其签名、aud、iss(应为 https://accounts.google.com)和有效期。 客户端仍可使用 IDTokenCredentials,但 target_audience 必须是服务端可识别的逻辑标识(非 IP):
实际上,target_audience 应设为一个服务标识符(如 my-vm-service),并在服务端硬编码校验该值,而非动态拼接 IP。因为 IP 可能变化,且 Google 不允许 aud 为裸 IP(违反 OIDC 规范)。
示例服务端验证逻辑(Python + PyJWT):
from google.auth import crypt
from google.auth.transport import requests as google_requests
import jwt
import requests
# 获取 Google 公钥(缓存复用)
def get_google_public_keys():
resp = requests.get("https://www.googleapis.com/oauth2/v3/certs")
return resp.json()
def verify_id_token(token: str, expected_audience: str = "my-vm-service") -> dict:
keys = get_google_public_keys()
header = jwt.get_unverified_header(token)
key_id = header.get("kid")
signing_key = None
for key in keys["keys"]:
if key["kid"] == key_id:
signing_key = crypt.RSASigner.from_string(key["x5c"][0])
break
if not signing_key:
raise ValueError("Invalid key ID")
payload = jwt.decode(
token,
signing_key,
algorithms=["RS256"],
audience=expected_audience,
issuer="https://accounts.google.com",
)
return payload
# FastAPI 中间件示例
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return JSONResponse({"error": "Unauthorized"}, status_code=401)
token = auth_header.split(" ")[1]
try:
claims = verify_id_token(token, expected_audience="my-vm-service")
request.state.user_email = claims.get("email")
except Exception as e:
return JSONResponse({"error": "Invalid token"}, status_code=401)
return await call_next(request)客户端保持类似结构,但修正 target_audience:
# ✅ 正确:audience 是逻辑服务名,非 IP
target_audience = "my-vm-service" # 与服务端校验一致
creds = service_account.IDTokenCredentials.from_service_account_file(
"vm-key.json", target_audience=target_audience)
authed_session = AuthorizedSession(creds)
response = authed_session.post(
url=f"http://{ip}:8000/get_attributes", # 注意:服务端若无 HTTPS,此处用 http
json=uuids,
timeout=10 # 增大超时,避免误报 ReadTimeout
)⚠️ 关键注意事项:
- 不要禁用 SSL 验证(verify=False):这会带来严重安全风险;若服务端使用自签名证书,请配置正确的 CA Bundle。
- VPC 防火墙规则与服务账号无关:0.0.0.0/0 规则仅控制网络可达性,不提供身份认证能力;服务账号认证必须由应用层显式实现。
- 避免将服务账号密钥(.json)部署到生产 VM:应改用 VM 关联的服务账号(Compute Engine metadata server),通过 google.auth.default() 自动获取凭据,更安全且免密钥管理。
总结:ReadTimeout 的本质不是网络问题,而是服务端未实现 ID Token 校验逻辑导致请求挂起。真正的解决方案是——将服务账号认证从“客户端单向注入”转变为“服务端主动验证”,并确保 audience 语义清晰、传输协议匹配、超时设置合理。










