Python请求 - 重新验证后重试请求

时间:2016-05-07 22:38:24

标签: python python-requests

我正在使用Python 3.5.1与Requests 2.9.1。我的用例如下:我需要从服务T验证(获取令牌)并在向资源服务器R发出请求时将其用作Authorization标头的值。令牌在某个时刻到期并且新的到期需要获取。

我有一个使用requests的应用程序,它首先启动时获取一个令牌并记住它 - 在用于R的请求的Session中设置。从那时起3分钟,一切都可以作为一个魅力。 3分钟后,由于令牌无效,我收到unauthorized个回复。

我对{strong>除了之外的所有请求使用Session进行身份验证;此调用会更新Authorization上的Session标头,以供其他请求使用。

我创建了代码,以便在检测到unauthorized时使用response挂钩(仅在Session上设置)自动重新进行身份验证,以下是代码:

def __hook(self, res, *args, **kwargs):
    if res.status_code == requests.codes.unauthorized:
        print('Token expired, refreshing')
        self.auth() # sets the token on self.__session

        req = res.request
        print('Resending request', req.method, req.url, req.headers)
        req.headers['Authorization'] = self.__session.headers['Authorization'] # why is it needed?

        return self.__session.send(res.request)

基本上,它甚至可以工作。但是有几个问题:

  1. 为什么有必要在请求中重新设置Authorization标头,即使会话已更新并用于重新发送原始请求?如果没有该行,应用程序将继续刷新令牌,因为永远不会使用新令牌,这可以在输出中看到,令牌是导致自动刷新的原始令牌。

  2. 如何使代码更加健壮,即防止无休止的递归(我不确定它是否可能在现实中,但是在重试的请求中设置Authorization标题的行将只是继续)?我正在考虑设置一个自定义标头,如果挂钩发现失败的请求有它,它将不会重新进行身份验证和重新发送。有更好的吗? 编辑:事实证明,如果配置错误,可能会得到(几乎)无限循环(毕竟它是递归的):令牌用于一个环境(如STAGING),但是资源服务器来自另一个(TEST) - auth请求将成功,但令牌实际上对资源服务器不正确。目前我实施了上面提到的“特殊”标题解决方案。

  3. 这是一般的好方法还是更适合requests中的任务?

2 个答案:

答案 0 :(得分:1)

所以我与Requests作者讨论了一个不相关的问题,并询问了这一点,结果证明这种方法还可以。我做的唯一改变是删除自定义标头(它确实工作,也是好的)现在我只是在重新发送请求之前注销钩子内的响应挂钩。这打破了循环,一切都很好。

答案 1 :(得分:0)

我想出的最终代码来解决这个问题。它比OP更完整。它公开了可用于身份验证请求的会话属性。

您必须撤回auth的原因是因为.send仅发送了PreparedRequest。

如果您不喜欢REATTEMPT标头,则添加了该标头以防止无限递归。您也可以像前面提到的OP一样取消注册该钩子(但您必须再次注册该钩子以备将来使用),或者在我的编码类中改为使用self._session而不是self.session进行新请求。 (self._session只是一个没有钩子的会话。)

import requests
from urllib.parse import urljoin

class JWTAuth:

    def __init__(self, user, passw, base):
        self.base = base
        self.user, self.passw = user, passw

        self._session = requests.Session()  # Session for tokens
        self.authenticate()

        self.session = requests.Session()  # Authenticated session
        self.session.auth = self.auth
        self.session.hooks['response'].append(self.reauth)

    def abs_url(self, path):
        """Combine the base url with a path."""
        return urljoin(self.base, path)

    def auth(self, req):
        """Just set the authentication token, on every request."""
        req.headers['Authorization'] = f'JWT {self.access}'
        return req

    def reauth(self, res, *args, **kwargs):
        """Hook to re-authenticate whenever authentication expires."""
        if res.status_code == requests.codes.unauthorized:
            if res.request.headers.get('REATTEMPT'):
                res.raise_for_status()
            self.refresh_auth()
            req = res.request
            req.headers['REATTEMPT'] = 1
            req = self.session.auth(req)
            res = self.session.send(req)
            return res

    def refresh_auth(self):
        """Use the refresh token to get a new access token."""
        res = self._session.post(self.abs_url("api/token/refresh/"), data={"refresh": self.refresh})
        if res.status_code == 200:
            self.access = res.json()['access']
        else:
            # Token expired -> re-authenticate
            self.authenticate()

    def authenticate(self):
        res = self._session.post(
            self.abs_url("api/token/"),
            data={"username": self.user, "password": self.passw},
        )
        res.raise_for_status()
        data = res.json()
        self.refresh, self.access = data['refresh'], data['access']