问题
I'm trying to write a nice auth helper for kraken. I want it to be as automatic as possible, so it needs to:
- add a nonce (
time.time()*1000
) to the POST body - calculate a signature over the POST body
- put the signature into the headers
I wrote the obvious code based on this answer:
class KrakenAuth(AuthBase):
"""a requests-module-compatible auth module for kraken.com"""
def __init__(self, key, secret):
self.api_key = key
self.secret_key = secret
def __call__(self, request):
#print("Auth got a %r" % type(request))
nonce = int(1000*time.time())
request.data = getattr(request, 'data', {})
request.data['nonce'] = nonce
request.prepare()
message = request.path_url + hashlib.sha256(str(nonce) + request.body).digest()
hmac_key = base64.b64decode(self.secret_key)
signature = hmac.new(hmac_key, message, hashlib.sha512).digest()
signature = base64.b64encode(signature)
request.headers.update({
'API-Key': self.api_key,
'API-Sign': signature
})
return request
and them I'm calling it (from a wrapper method on another object) like:
def _request(self, method, url, **kwargs):
if not self._auth:
self._auth = KrakenAuth(key, secret)
if 'auth' not in kwargs:
kwargs['auth'] = self._auth
return self._session.request(method, URL + url, **kwargs)
...but it doesn't work. The commented-out print()
statement shows that it's getting a PreparedRequest
object not a Request
object, and thus the call to request.prepare()
is a call to PreparedRequest.prepare
does nothing useful because there's no request.data
because it's already been converted into a body
attribute.
回答1:
You can't access the data
attribute of the request, because the authentication object is applied to a requests.PreparedRequest() instance, which has no .data
attribute.
The normal flow for a Session.request() call (used by all the request.<method>
and session.<method>
calls), is as follows:
- A
Request()
instance is created with all the same arguments as the original call - The request is passed to Session.prepare_request(), which merges in session-stored base values with the arguments of the original call first, then
- A PreparedRequest() instance is created
- The PreparedRequest.prepare() method is called on that prepared request instance, passing in the merged data from the
Request
instance and the session. - The
prepare()
method calls the variousself.prepare_*
methods, including PreparedRequest.prepare_auth(). PreparedRequest.prepare_auth()
callsauth(self)
to give the authentication object a chance to attach information to the request.
Unfortunately for you, at no point during this flow will the original data
mapping be available to anyone else but PreparedRequest.prepare()
and PreparedRequest.prepare_body()
, and in those methods the mapping is a local variable. You can't access it from the authentication object.
Your options are then:
To decode the body again, and call
prepare_body()
with the updated mapping.To not use an authentication object, but use the other path from my answer; to explicitly create a prepared request and manipulate
data
first.To play merry hell with the Python stack and extract locals from the
prepare()
method that is two frames up. I really can't recommend this path.
To keep the authentication method encapsulated nicely, I'd go with decoding / re-encoding; the latter is simple enough by reusing PreparedRequest.prepare_body():
import base64
import hashlib
import hmac
import time
try:
# Python 3
from urllib.parse import parse_qs
except ImportError:
# Python 2
from urlparse import parse_qs
from requests import AuthBase
URL_ENCODED = 'application/x-www-form-urlencoded'
class KrakenAuth(AuthBase):
"""a requests-module-compatible auth module for kraken.com"""
def __init__(self, key, secret):
self.api_key = key
self.secret_key = secret
def __call__(self, request):
ctheader = request.headers.get('Content-Type')
assert (
request.method == 'POST' and (
ctheader == URL_ENCODED or
requests.headers.get('Content-Length') == '0'
)
), "Must be a POST request using form data, or empty"
# insert the nonce in the encoded body
data = parse_qs(request.body)
data['nonce'] = nonce
request.prepare_body(data, None, None)
body = request.body
if not isinstance(body, bytes): # Python 3
body = body.encode('latin1') # standard encoding for HTTP
message = request.path_url + hashlib.sha256(b'%s%s' % (nonce, body)).digest()
hmac_key = base64.b64decode(self.secret_key)
signature = hmac.new(hmac_key, message, hashlib.sha512).digest()
signature = base64.b64encode(signature)
request.headers.update({
'API-Key': self.api_key,
'API-Sign': signature
})
return request
来源:https://stackoverflow.com/questions/52705158/how-do-i-sign-the-body-of-a-requests-request-in-an-auth-objects-call-method