使用Python解析HTTP请求中的Authorization头部信息

14

我需要获取这样的标题:

 Authorization: Digest qop="chap",
     realm="testrealm@host.com",
     username="Foobear",
     response="6629fae49393a05397450978507c4ef1",
     cnonce="5ccc069c403ebaf9f0171e9517f40e41"

使用Python将其解析为以下内容:

{'protocol':'Digest',
  'qop':'chap',
  'realm':'testrealm@host.com',
  'username':'Foobear',
  'response':'6629fae49393a05397450978507c4ef1',
  'cnonce':'5ccc069c403ebaf9f0171e9517f40e41'}

有没有一个库可以完成这个任务,或者我可以参考一些东西来启发自己?

我正在使用Google App Engine,不确定Pyparsing库是否可用,但如果它是最好的解决方案,也许我可以将其包含在我的应用程序中。

目前我正在创建自己的MyHeaderParser对象,并在标题字符串上使用reduce()。 它能够工作,但非常脆弱。

nadia下面的解决方案非常出色:

import re

reg = re.compile('(\w+)[=] ?"?(\w+)"?')

s = """Digest
realm="stackoverflow.com", username="kixx"
"""

print str(dict(reg.findall(s)))

到目前为止,这个解决方案不仅非常干净,而且非常健壮。虽然它不是 RFC 的最规范实现,但我还没有构建出一个返回无效值的测试用例。然而,我 使用它来解析授权标头,我感兴趣的其他标头需要解析,因此这可能不是一个好的通用 HTTP 标头解析器的解决方案。 - Kris Walker
我来到这里是为了寻找一个完整的RFC解析器。您的问题以及@PaulMcG的回答让我走上了正确的道路(请参见我的答案)。谢谢你们两个! - biscuit314
10个回答

14

一个简单的正则表达式:

import re
reg=re.compile('(\w+)[:=] ?"?(\w+)"?')

>>>dict(reg.findall(headers))

{'username': 'Foobear', 'realm': 'testrealm', 'qop': 'chap', 'cnonce': '5ccc069c403ebaf9f0171e9517f40e41', 'response': '6629fae49393a05397450978507c4ef1', 'Authorization': 'Digest'}

哇,我喜欢Python。“授权:”实际上并不是头字符串的一部分,所以我改成了这个:#! /usr/bin/env python import redef mymain(): reg = re.compile('(\w+)[=] ?"?(\w+)"?') s = """Digest realm="fireworksproject.com", username="kristoffer" """ print str(dict(reg.findall(s)))if name == 'main': mymain()我没有得到“摘要”协议声明,但我也不需要它。本质上只有3行代码...太棒了!!! - Kris Walker
我认为使用原始字符串或\更加明确。 - Bastien Léonard
如果您找到并使用它,请确保在"?(\w+)"内部添加另一个问号,使其变成"?(\w+)?"。这样,如果您将某些内容传递为"",它将返回参数,而值未定义。如果您真的想要Digest:/(\w+)(?:([:=]) ?"?(\w+)?"?)?/,请检查匹配中是否存在=,如果是,则为键:值,否则为其他内容。 - Nijikokun
1
实际上,"并不是必需的(例如,algorithm通常不使用"来分隔其值),而一个值本身可以包含转义的". "?有点冒险 =) (我曾经在PHP中问过同样的问题。 - Rudie
2
更宽容的版本:re.compile(r'(\w+)[:=][\s"]?([^",]+)"?') - Sam Alba
显示剩余2条评论

10

你也可以像 [CheryPy][1] 一样使用 urllib2。

这是代码片段:

input= """
 Authorization: Digest qop="chap",
     realm="testrealm@host.com",
     username="Foobear",
     response="6629fae49393a05397450978507c4ef1",
     cnonce="5ccc069c403ebaf9f0171e9517f40e41"
"""
import urllib2
field, sep, value = input.partition("Authorization: Digest ")
if value:
    items = urllib2.parse_http_list(value)
    opts = urllib2.parse_keqv_list(items)
    opts['protocol'] = 'Digest'
    print opts

它的输出结果为:

{'username': 'Foobear', 'protocol': 'Digest', 'qop': 'chap', 'cnonce': '5ccc069c403ebaf9f0171e9517f40e41', 'realm': 'testrealm@host.com', 'response': '6629fae49393a05397450978507c4ef1'}

[1]: https://web.archive.org/web/20130118133623/http://www.google.com:80/codesearch/p?hl=en#OQvO9n2mc04/CherryPy-3.0.1/cherrypy/lib/httpauth.py&q=Authorization 是一个使用Python语言的Digest http验证的示例代码。


1
在Python 3中,这些函数仍然存在(尽管它们没有记录),但它们位于urllib.request而不是urllib2中。 - kbolino
警告:urllib.request 是 Python 标准库中最重的导入之一。如果你只是使用这两个函数,那么可能不值得这样做。 - Pyprohly

3

这是我的pyparsing尝试:

text = """Authorization: Digest qop="chap",
    realm="testrealm@host.com",     
    username="Foobear",     
    response="6629fae49393a05397450978507c4ef1",     
    cnonce="5ccc069c403ebaf9f0171e9517f40e41" """

from pyparsing import *

AUTH = Keyword("Authorization")
ident = Word(alphas,alphanums)
EQ = Suppress("=")
quotedString.setParseAction(removeQuotes)

valueDict = Dict(delimitedList(Group(ident + EQ + quotedString)))
authentry = AUTH + ":" + ident("protocol") + valueDict

print authentry.parseString(text).dump()

这会打印出:

['Authorization', ':', 'Digest', ['qop', 'chap'], ['realm', 'testrealm@host.com'],
 ['username', 'Foobear'], ['response', '6629fae49393a05397450978507c4ef1'], 
 ['cnonce', '5ccc069c403ebaf9f0171e9517f40e41']]
- cnonce: 5ccc069c403ebaf9f0171e9517f40e41
- protocol: Digest
- qop: chap
- realm: testrealm@host.com
- response: 6629fae49393a05397450978507c4ef1
- username: Foobear

我不熟悉RFC,但希望这能让你开始。


这个解决方案是我最初想到的使用pyparsing,据我所知,它产生了不错的结果。 - Kris Walker

2

这是一个较旧的问题,但我认为非常有帮助。

(edit after recent upvote) I've created a package that builds on this answer (link to tests to see how to use the class in the package).

pip install authparser

我需要一个解析器来处理任何符合RFC7235(如果你喜欢阅读ABNF,请举手)中定义的正确格式的授权头。

Authorization = credentials

BWS = <BWS, see [RFC7230], Section 3.2.3>

OWS = <OWS, see [RFC7230], Section 3.2.3>

Proxy-Authenticate = *( "," OWS ) challenge *( OWS "," [ OWS
 challenge ] )
Proxy-Authorization = credentials

WWW-Authenticate = *( "," OWS ) challenge *( OWS "," [ OWS challenge
 ] )

auth-param = token BWS "=" BWS ( token / quoted-string )
auth-scheme = token

challenge = auth-scheme [ 1*SP ( token68 / [ ( "," / auth-param ) *(
 OWS "," [ OWS auth-param ] ) ] ) ]
credentials = auth-scheme [ 1*SP ( token68 / [ ( "," / auth-param )
 *( OWS "," [ OWS auth-param ] ) ] ) ]

quoted-string = <quoted-string, see [RFC7230], Section 3.2.6>

token = <token, see [RFC7230], Section 3.2.6>
token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" )
 *"="

PaulMcG的回答开始,我得出了以下结论:

import pyparsing as pp

tchar = '!#$%&\'*+-.^_`|~' + pp.nums + pp.alphas
t68char = '-._~+/' + pp.nums + pp.alphas

token = pp.Word(tchar)
token68 = pp.Combine(pp.Word(t68char) + pp.ZeroOrMore('='))

scheme = token('scheme')

header = pp.Keyword('Authorization')
name = pp.Word(pp.alphas, pp.alphanums)
value = pp.quotedString.setParseAction(pp.removeQuotes)
name_value_pair = name + pp.Suppress('=') + value
params = pp.Dict(pp.delimitedList(pp.Group(name_value_pair)))

credentials = scheme + (token68('token') ^ params('params'))

auth_parser = header + pp.Suppress(':') + credentials

这允许解析任何授权头:

parsed = auth_parser.parseString('Authorization: Basic Zm9vOmJhcg==')
print('Authenticating with {0} scheme, token: {1}'.format(parsed['scheme'], parsed['token']))

它的输出结果为:

Authenticating with Basic scheme, token: Zm9vOmJhcg==

将所有内容整合到一个 Authenticator 类中:
import pyparsing as pp
from base64 import b64decode
import re

class Authenticator:
    def __init__(self):
        """
        Use pyparsing to create a parser for Authentication headers
        """
        tchar = "!#$%&'*+-.^_`|~" + pp.nums + pp.alphas
        t68char = '-._~+/' + pp.nums + pp.alphas

        token = pp.Word(tchar)
        token68 = pp.Combine(pp.Word(t68char) + pp.ZeroOrMore('='))

        scheme = token('scheme')

        auth_header = pp.Keyword('Authorization')
        name = pp.Word(pp.alphas, pp.alphanums)
        value = pp.quotedString.setParseAction(pp.removeQuotes)
        name_value_pair = name + pp.Suppress('=') + value
        params = pp.Dict(pp.delimitedList(pp.Group(name_value_pair)))

        credentials = scheme + (token68('token') ^ params('params'))

        # the moment of truth...
        self.auth_parser = auth_header + pp.Suppress(':') + credentials


    def authenticate(self, auth_header):
        """
        Parse auth_header and call the correct authentication handler
        """
        authenticated = False
        try:
            parsed = self.auth_parser.parseString(auth_header)
            scheme = parsed['scheme']
            details = parsed['token'] if 'token' in parsed.keys() else parsed['params']

            print('Authenticating using {0} scheme'.format(scheme))
            try:
                safe_scheme = re.sub("[!#$%&'*+-.^_`|~]", '_', scheme.lower())
                handler = getattr(self, 'auth_handle_' + safe_scheme)
                authenticated = handler(details)
            except AttributeError:
                print('This is a valid Authorization header, but we do not handle this scheme yet.')

        except pp.ParseException as ex:
            print('Not a valid Authorization header')
            print(ex)

        return authenticated


    # The following methods are fake, of course.  They should use what's passed
    # to them to actually authenticate, and return True/False if successful.
    # For this demo I'll just print some of the values used to authenticate.
    @staticmethod
    def auth_handle_basic(token):
        print('- token is {0}'.format(token))
        try:
            username, password = b64decode(token).decode().split(':', 1)
        except Exception:
            raise DecodeError
        print('- username is {0}'.format(username))
        print('- password is {0}'.format(password))
        return True

    @staticmethod
    def auth_handle_bearer(token):
        print('- token is {0}'.format(token))
        return True

    @staticmethod
    def auth_handle_digest(params):
        print('- username is {0}'.format(params['username']))
        print('- cnonce is {0}'.format(params['cnonce']))
        return True

    @staticmethod
    def auth_handle_aws4_hmac_sha256(params):
        print('- Signature is {0}'.format(params['Signature']))
        return True

测试这个类:

tests = [
    'Authorization: Digest qop="chap", realm="testrealm@example.com", username="Foobar", response="6629fae49393a05397450978507c4ef1", cnonce="5ccc069c403ebaf9f0171e9517f40e41"',
    'Authorization: Bearer cn389ncoiwuencr',
    'Authorization: Basic Zm9vOmJhcg==',
    'Authorization: AWS4-HMAC-SHA256 Credential="AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request", SignedHeaders="host;range;x-amz-date", Signature="fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024"',
    'Authorization: CrazyCustom foo="bar", fizz="buzz"',
]

authenticator = Authenticator()

for test in tests:
    authenticator.authenticate(test)
    print()

这将输出:

Authenticating using Digest scheme
- username is Foobar
- cnonce is 5ccc069c403ebaf9f0171e9517f40e41

Authenticating using Bearer scheme
- token is cn389ncoiwuencr

Authenticating using Basic scheme
- token is Zm9vOmJhcg==
- username is foo
- password is bar

Authenticating using AWS4-HMAC-SHA256 scheme
- signature is fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024

Authenticating using CrazyCustom scheme 
This is a valid Authorization header, but we do not handle this scheme yet.

如果未来我们想处理CrazyCustom,只需添加以下内容:

def auth_handle_crazycustom(params):

1

HTTP摘要授权头字段有点奇怪。它的格式类似于rfc 2616的Cache-Control和Content-Type头字段,但又不同到足以不兼容。如果您仍在寻找比正则表达式更智能、更易读的库,您可以尝试使用str.split()删除Authorization: Digest部分,并使用Werkzeug的http模块中的parse_dict_header()解析其余部分。(Werkzeug可安装在App Engine上。)


非常感谢。我可能会用这个替换那个正则表达式。它看起来更加健壮。 - Kris Walker

1

您最初使用PyParsing的想法是最好的方法。您隐含地要求的是需要语法...也就是说,正则表达式或简单的解析例程总是会很脆弱,这似乎是您试图避免的事情。

看起来在Google应用引擎上获取pyparsing很容易:我如何在Google应用引擎上设置PyParsing?

所以我会选择这个,然后从rfc2617实现完整的HTTP身份验证/授权头支持。


1
我决定采用这种方法,并尝试使用RFC规范实现一个完全符合要求的Authorization头解析器。这项任务似乎比我预期的要困难得多。你选择简单的正则表达式,虽然不是严格正确的,但可能是最好的实用解决方案。如果我最终获得了一个完全功能的头部解析器,我会在这里报告的。 - Jason R. Coombs
1
是的,看到更加严谨正确的东西会很不错。 - Kris Walker
嗨,Jason - 如果你还在寻找,可以看看我的回答。PyParsing 真是太棒了! - biscuit314

1
Nadia的正则表达式只匹配参数值中的字母数字字符。这意味着它无法解析至少两个字段,即uri和qop。根据RFC 2617,uri字段是请求行中的字符串副本(即HTTP请求的第一行)。如果qop的值为“auth-int”,由于非字母数字字符“-”,它将无法正确解析。
此修改后的正则表达式允许URI(或任何其他值)包含除空格、引号或逗号之外的任何内容。这可能比它需要的更宽容,但不应该对正确形成的HTTP请求造成任何问题。
reg re.compile('(\w+)[:=] ?"?([^" ,]+)"?')

额外提示:从那里开始,将RFC-2617中的示例代码转换为Python相当简单。使用Python的md5 API,“MD5Init()”变成“m = md5.new()”,“MD5Update()”变成“m.update()”,“MD5Final()”变成“m.digest()”。

1
如果这些组件将始终存在,那么正则表达式就可以解决问题:
test = '''Authorization: Digest qop="chap", realm="testrealm@host.com", username="Foobear", response="6629fae49393a05397450978507c4ef1", cnonce="5ccc069c403ebaf9f0171e9517f40e41"'''

import re

re_auth = re.compile(r"""
    Authorization:\s*(?P<protocol>[^ ]+)\s+
    qop="(?P<qop>[^"]+)",\s+
    realm="(?P<realm>[^"]+)",\s+
    username="(?P<username>[^"]+)",\s+
    response="(?P<response>[^"]+)",\s+
    cnonce="(?P<cnonce>[^"]+)"
    """, re.VERBOSE)

m = re_auth.match(test)
print m.groupdict()

产生:

{ 'username': 'Foobear', 
  'protocol': 'Digest', 
  'qop': 'chap', 
  'cnonce': '5ccc069c403ebaf9f0171e9517f40e41', 
  'realm': 'testrealm@host.com', 
  'response': '6629fae49393a05397450978507c4ef1'
}

这个解决方案在我所能看到的范围内产生了正确的结果。 - Kris Walker

1
我建议找到一个正确的解析HTTP头的库,但很遗憾我想不起来任何一个。 :(
暂时可以检查下面的代码片段(它应该大部分工作):
input= """
 Authorization: Digest qop="chap",
     realm="testrealm@host.com",
     username="Foob,ear",
     response="6629fae49393a05397450978507c4ef1",
     cnonce="5ccc069c403ebaf9f0171e9517f40e41"
"""

field, sep, value = input.partition(":")
if field.endswith('Authorization'):
   protocol, sep, opts_str = value.strip().partition(" ")

   opts = {}
   for opt in opts_str.split(",\n"):
        key, value = opt.strip().split('=')
        key = key.strip(" ")
        value = value.strip(' "')
        opts[key] = value

   opts['protocol'] = protocol

   print opts

0
如果您的响应以单个字符串形式返回,且从不变化,并且有与要匹配的表达式一样多的行,则可以在换行符上将其拆分为一个名为authentication_array的数组,并使用正则表达式:
pattern_array = ['qop', 'realm', 'username', 'response', 'cnonce']
i = 0
parsed_dict = {}

for line in authentication_array:
    pattern = "(" + pattern_array[i] + ")" + "=(\".*\")" # build a matching pattern
    match = re.search(re.compile(pattern), line)         # make the match
    if match:
        parsed_dict[match.group(1)] = match.group(2)
    i += 1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接