Python Aiohttp同域多端口Cookie行为异常问题求助
我在用Aiohttp处理Cookie时遇到了一个奇怪的问题:Aiohttp的CookieJar类遵循RFC6265规范,该规范明确了两点:
- 给定主机的Cookie应跨该主机的所有端口共享
- Cookie不提供端口隔离,同一服务器不同端口的服务可读取同一Cookie
但我搭建了两个运行在localhost不同端口的Aiohttp服务:一个提供登录接口并返回Cookie,另一个的接口需要验证Cookie才能访问。测试时发现,直接传递登录响应的cookies对象访问受保护接口返回401,而将Cookie转为键值对字典传递则返回200,请问这是什么原因?
以下是使用aiohttp、pytest、pytest-aiohttp编写的测试用例:
import functools import pytest from aiohttp import web pytestmark = pytest.mark.asyncio def attach_session(f): @functools.wraps(f) async def wrapper(request: web.Request): session_id = request.cookies.get("testcookie") request["mysession"] = session_id response = await f(request) response.set_cookie("testcookie", session_id) return response return wrapper def is_logged_in(f): @functools.wraps(f) @attach_session async def wrapper(request: web.Request): session = request["mysession"] if not session: raise web.HTTPUnauthorized return await f(request) return wrapper async def login(_: web.Request): response = web.Response() response.set_cookie("testcookie", "somerandomstring") return response @is_logged_in async def some_endpoint(request: web.Request): return web.Response(text="sweet") @pytest.fixture def auth_client(event_loop, aiohttp_client): app = web.Application() app.router.add_post("/login", login) return event_loop.run_until_complete(aiohttp_client(app)) @pytest.fixture def core_client(event_loop, aiohttp_client): app = web.Application() app.router.add_get("/some_endpoint", some_endpoint) return event_loop.run_until_complete(aiohttp_client(app)) async def test_login(auth_client): resp = await auth_client.post("/login") assert resp.status == 200 assert resp.cookies.get("testcookie").value == "somerandomstring" async def test_some_endpoint_anonymous(core_client): resp = await core_client.get("/some_endpoint") assert resp.status == 401 async def test_some_endpoint_as_logged_in(auth_client, core_client): resp1 = await auth_client.post("/login") resp2 = await core_client.get("/some_endpoint", cookies=resp1.cookies) assert resp2.status == 401 async def test_some_endpoint_as_logged_in_again(auth_client, core_client): resp1 = await auth_client.post("/login") _cookie = list(resp1.cookies.values())[0] resp2 = await core_client.get( "/some_endpoint", cookies={_cookie.key: _cookie.value} ) assert resp2.status == 200
问题根源:Cookie对象附带的端口属性导致匹配失败
当你从auth_client的登录响应中拿到resp1.cookies时,这里面的每个Cookie都是包含完整属性的Morsel对象(来自http.cookies.SimpleCookie),其中会自动带上port属性,值为auth_client服务运行的端口。
而aiohttp的客户端在发送请求时,会严格按照Cookie的属性进行匹配:如果Cookie的port属性和目标请求的端口不一致,这个Cookie就不会被发送到服务器。因为core_client运行在另一个端口,所以直接传递resp1.cookies时,这个带端口的Cookie被客户端过滤掉了,后端服务拿不到testcookie,自然返回401。
而当你把Cookie转为{key: value}的字典传递时,相当于手动构造了只包含键值对的Cookie信息,没有附带port等限制属性。此时aiohttp客户端会根据RFC6265规范,自动允许该Cookie跨localhost的不同端口共享,所以Cookie会被正常发送到core_client的服务,后端能验证通过,返回200。
你可以在测试中打印resp1.cookies["testcookie"]["port"],就能看到它的值是auth_client的端口,和core_client的端口不一致,这就是问题的核心。
内容的提问来源于stack exchange,提问作者Clément de Nailly

