Servant 客户端如何处理收到的 cookie?

问题描述

我想使用 Servant 客户端首先调用登录端点以获取会话 cookie,然后针对需要 cookie 身份验证的端点发出请求。

API 是(简化的)

import qualified Servant                       as SV
import qualified Servant.Auth.Server           as AS
import qualified Servant.Client                as SC

-- Authentication and X-CSRF cookies
type CookieHeader = ( SV.Headers '[SV.Header "Set-Cookie" AS.SetCookie,SV.Header "Set-Cookie" AS.SetCookie]
                      SV.NoContent )

type LoginEndpoint = "login" :> SV.ReqBody '[SV.JSON] Login :> SV.Verb 'SV.POST 204 '[SV.JSON] CookieHeader
type ProtectedEndpoint = "protected" :> SV.Get '[SV.JSON]
-- The overall API
type Api = LoginEndpoint :<|> (AS.Auth '[AS.Cookie,AS.JWT] User :> ProtectedEndpoint)

apiProxy :: Proxy Api
apiProxy = Proxy

我定义客户端如下:

loginClient :: Api.Login -> SC.ClientM Api.CookieHeader
protectedClient :: AC.Token -> SC.ClientM Text :<|> SC.ClientM SV.NoContent
loginClient :<|> protectedClient = SC.client Api.apiProxy

客户端如何处理认证cookie?我可以想到两种方法。在 ClientM monad 中执行请求时,例如

do
  result <- SC.runclientM (loginClient (Login "user" "password")) clientEnv
  [..]

其中 Login登录请求正文,clientEnv 类型为 Servant.Client.ClientEnv,cookie 可以是 result 的一部分,它可以在 {{1} cookieJar 内的 tvar,或两者兼而有之。我会假设 tvar 被更新,以便具有相同 clientEnv 的后续请求将一起发送接收到的 cookie。但是,我尝试使用 Network.HTTP.Client.destroyCookieJar 读取 tvar 并检查其内容时发现了一个空数组。这是故意的吗?我在文档中找不到任何内容

因此,要进行经过身份验证的调用,我需要从 clientEnv 中的标头中提取 cookie(如何?),更新 tvar,创建一个引用此 tvar 的新 result,并使用这个新环境进行经过身份验证的调用。这确实是建议的程序吗?我问是因为我认为用例是如此标准,应该有一个更简化的解决方案。有没有?我错过了什么吗?

解决方法

经过一些实验,我发现 Servant 客户端确实在 cookieJar 中维护了 cookie,它是 clientEnv 的一部分。更准确地说,clientEnv 包含字段 cookieJar,其类型为 Maybe (TVar CookieJar)。它是客户端根据后续请求的 Set-Cookie 指令更新的 TVar。在发出第一个请求之前,由开发人员创建和初始化 TVar;否则,Servant 客户端将丢弃请求之间的 cookie。

此外,还可以通过与请求正文相同的方式来检索 cookie。为此,必须将要检索的 cookie 定义为 API 类型的一部分,就像我原始问题的示例一样:

type LoginEndpoint = "login" :> SV.ReqBody '[SV.JSON] Login :> SV.Verb 'SV.POST 204 '[SV.JSON] CookieHeader

一开始拆解返回的东西有点棘手,因为我需要弄清楚Servant的类型级机制产生的最终类型。最终,我做了以下事情:

SV.Headers resp h <- tryRequest clientEnv (loginClient (Api.Login "user" "pwd"))
let headers = SV.getHeaders h

其中 tryRequest 是执行 runClientM 并提取 Right 部分的助手。模式匹配 resp 包含返回值(此处为 NoContent),而 h 是不同标头的 HList。可以使用 Servant 的 Network.HTTP.Types.Header 函数将其转换为 getHeaders 的常规列表。

然后可以通过向 cookieJar TVar 添加新标头来更改或生成新标头并通过新请求提交它们(请参阅 Network.HTTP.Client 中的 cookie-manipulating functions)。