urllib.request带Bearer认证重定向请求失败的问题排查
Python urllib请求GitHub工件API返回403认证失败
场景重现
以下curl命令可以正常从GitHub工作流下载工件:
curl -L -H "Authorization: Bearer ghp_XXXX" -o arti.zip \ https://api.github.com/repos/OWNER/REPO/actions/artifacts/ID/zip
但使用相同URL和令牌的Python urllib代码却返回403错误:
import os, urllib.request req = urllib.request.Request('https://api.github.com/repos/OWNER/REPO/actions/artifacts/ID/zip') req.add_header('Authorization', 'Bearer ghp_XXXX') with urllib.request.urlopen(req) as input: with open('arti.zip', 'wb') as output: output.write(input.read())
错误信息:
urllib.error.HTTPError: HTTP Error 403: Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
问题根源
GitHub工件API的初始请求会重定向到第三方存储域名(比如Azure Blob存储的域名),而urllib的默认规则是不会把敏感头(比如Authorization)传递给不同主机的重定向请求——哪怕官方文档提到add_header的头会跟随重定向,这个规则只适用于同主机的重定向。
curl的-L参数之所以能成功,是因为它对GitHub相关的重定向目标做了特殊处理,允许传递认证头;但urllib遵循更严格的安全规范,直接跳过了跨主机的敏感头传递,导致重定向后的请求没有携带令牌,触发403。
基于urllib的修复方案
要让认证头跟随跨主机重定向,需要自定义重定向处理器,手动把Authorization头添加到重定向请求中:
方案1:全局修改opener(影响所有urllib请求)
import urllib.request class AuthRedirectHandler(urllib.request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): # 调用父类生成默认的重定向请求 new_req = super().redirect_request(req, fp, code, msg, headers, newurl) # 手动复制原请求的Authorization头到新请求 if 'Authorization' in req.headers: new_req.add_header('Authorization', req.headers['Authorization']) return new_req # 安装自定义处理器 opener = urllib.request.build_opener(AuthRedirectHandler()) urllib.request.install_opener(opener) # 正常发起请求 req = urllib.request.Request('https://api.github.com/repos/OWNER/REPO/actions/artifacts/ID/zip') req.add_header('Authorization', 'Bearer ghp_XXXX') with urllib.request.urlopen(req) as input: with open('arti.zip', 'wb') as output: output.write(input.read())
方案2:仅针对当前请求使用自定义opener(更安全)
如果不想影响全局的urllib行为,可以直接用自定义opener发起请求:
import urllib.request class AuthRedirectHandler(urllib.request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): new_req = super().redirect_request(req, fp, code, msg, headers, newurl) if 'Authorization' in req.headers: new_req.add_header('Authorization', req.headers['Authorization']) return new_req # 创建opener但不全局安装 opener = urllib.request.build_opener(AuthRedirectHandler()) req = urllib.request.Request('https://api.github.com/repos/OWNER/REPO/actions/artifacts/ID/zip') req.add_header('Authorization', 'Bearer ghp_XXXX') # 使用自定义opener发起请求 with opener.open(req) as input: with open('arti.zip', 'wb') as output: output.write(input.read())
注意事项
- 这种方式绕过了urllib的跨主机敏感头限制,只建议在明确信任重定向目标的场景下使用(比如GitHub官方的工件存储服务)。
- 虽然requests库能更简洁地处理这个问题(默认跟随重定向并携带认证头),但如果必须使用标准库urllib,以上方案就是最直接的解决办法。
内容的提问来源于stack exchange,提问作者Thierry Lelegard
相关产品推荐
相关产品推荐

