创建提供程序

OAuthLib是一个独立于框架的库,可以与任何Web框架一起使用。也就是说,有一些特定于框架的帮助器库可以让您的工作更轻松。

  • 对于长颈鹿来说,有 flask-oauthlib

如果没有对您最喜欢的框架的支持,而您有兴趣提供它,那么您来对了地方。OAuthLib可以处理OAuth逻辑,让您支持几个框架和设置特定任务,例如将请求对象编组到URI、头和正文参数中,并为后端提供存储令牌、客户端等的接口。

1.创建数据存储区模型

这些模型将代表各种特定于OAuth的概念。它们之间有一些重要的联系,OAuth的安全是基于这些联系的。下面是对模型的建议,以及为什么你需要某些属性。还有一些示例SQLAlChemy模型字段,它们应该可以直接转换为其他ORM,如Django和AppEngine数据存储。

1.1用户(或资源所有者)

您站点的用户,根据用户的授权,客户端可以访问哪些资源。下面是一个粗略的用户模型示例,您的模型可能会有所不同,并且结构并不重要。用户如何进行身份验证也不重要,只要它在授权之前这样做:

Base = sqlalchemy.ext.declarative.declarative_base()
class ResourceOwner(Base):
    __tablename__ = "users"

    id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
    name = sqlalchemy.Column(sqlalchemy.String)
    email = sqlalchemy.Column(sqlalchemy.String)
    password = sqlalchemy.Column(sqlalchemy.String)

1.2客户(或消费者)

对访问受保护资源感兴趣的客户端。

Client Identifier / Consumer key

必需的。客户端将在OAuth工作流期间使用的标识符。结构由您决定,可以是简单的UID::

client_key = sqlalchemy.Column(sqlalchemy.String)
Client secret

HMAC-SHA1和明文所必需的。客户端在OAuth工作流期间验证请求时将使用的密码。必须以明文(即非散列)的形式访问,因为它用于重新创建和验证签名的请求::

client_secret = sqlalchemy.Column(sqlalchemy.String)
Client public key

RSA-SHA1需要。用于验证由客户端私钥签名的请求的公钥::

rsa_key = sqlalchemy.Column(sqlalchemy.String)
User

推荐。通常的做法是将每个客户端链接到现有用户之一。无论您是否关联客户端和用户,请确保您能够保护自己免受恶意客户端的攻击:

user = Column(Integer, ForeignKey("users.id"))
Realms

必需的。客户端可能请求访问的领域列表。虽然领域的使用在很大程度上没有记录在规范中,但您可能会认为它们与OAuth 2作用域非常相似。

# You could represent it either as a list of keys or by serializing
# the scopes into a string.
realms = sqlalchemy.Column(sqlalchemy.String)

# You might also want to mark a certain set of scopes as default
# scopes in case the client does not specify any in the authorization
default_realms = sqlalchemy.Column(sqlalchemy.String)
Redirect URIs

这些是客户端在授权后可以用来重定向到的绝对URI。您永远不应允许客户端重定向到以前未注册的URI:

# You could represent the URIs either as a list of keys or by
# serializing them into a string.
redirect_uris = sqlalchemy.Column(sqlalchemy.String)

# You might also want to mark a certain URI as default in case the
# client does not specify any in the authorization
default_redirect_uri = sqlalchemy.Column(sqlalchemy.String)

1.3请求令牌+验证器

在OAuth 1工作流中,第一步是获取/提供请求令牌。此令牌捕获有关客户端、其回调URI和请求的领域的信息。OAuth2中不存在此步骤,因为这些凭据是在授权步骤中直接提供的。

第一次创建请求令牌时,用户是未知的。在授权步骤期间,用户与请求令牌相关联。在成功授权之后,向客户端提供验证码(应该链接到请求令牌)作为授权的证明。该验证器代码稍后用于获取访问令牌。

Client

与向其提供请求令牌的客户端的关联:

client = Column(Integer, ForeignKey("clients.id"))
User

与此令牌请求访问受保护资源的用户的关联::

user = Column(Integer, ForeignKey("users.id"))
Realms

令牌绑定到的领域。尝试访问这些领域之外的受保护资源将被拒绝::

# You could represent it either as a list of keys or by serializing
# the scopes into a string.
realms = sqlalchemy.Column(sqlalchemy.String)
Redirect URI

用户授权完成后用于重定向回客户端的回调URI::

redirect_uri = sqlalchemy.Column(sqlalchemy.String)
Request Token

无法猜测的唯一字符串::

request_token = sqlalchemy.Column(sqlalchemy.String)
Request Token Secret

无法猜测的唯一字符串。这是HMAC-SHA1和明文签名方法在以后获取访问令牌时使用的临时密钥::

request_token_secret = sqlalchemy.Column(sqlalchemy.String)
Authorization Verifier

无法猜测的唯一字符串。此代码断言用户已授权客户端访问请求的领域。客户端第一步获取请求令牌时初始为空,第二步用户授权后设置:

verifier = sqlalchemy.Column(sqlalchemy.String)

1.4访问令牌

访问令牌被提供给能够与其关联的验证器一起呈现有效请求令牌的客户端。它将允许客户端访问受保护的资源,并且通常与到期无关。尽管您应该考虑将它们过期,因为这会极大地提高安全性。

需要将用户和领域从请求令牌传输到访问令牌。授权领域的列表可能小于请求领域的列表。客户端可以通过比较 oauth_realms 令牌响应中给出的参数。这种指示域更改的方式是从OAuth2作用域行为返回的,并且不在OAuth 1规范中。

Client

与向其提供访问令牌的客户端的关联:

client = Column(Integer, ForeignKey("clients.id"))
User

与此令牌授予访问权限的受保护资源的用户的关联::

user = Column(Integer, ForeignKey("users.id"))
Realms

令牌绑定到的领域。尝试访问这些领域之外的受保护资源将被拒绝::

# You could represent it either as a list of keys or by serializing
# the scopes into a string.
realms = sqlalchemy.Column(sqlalchemy.String)
Access Token

无法猜测的唯一字符串::

access_token = sqlalchemy.Column(sqlalchemy.String)
Access Token Secret

无法猜测的唯一字符串。访问受保护资源时,HMAC-SHA1和明文签名方法使用此密码:

access_token_secret = sqlalchemy.Column(sqlalchemy.String)

2.实现验证器

实现OAuth 1提供程序所涉及的大部分工作与将各种验证和持久性方法映射到存储后端有关。您需要实现的命名不太准确的接口称为 RequestValidator (欢迎提出命名建议)。

的一个非常基本的实现的示例 validate_client_key 方法如下所示:

from oauthlib.oauth1 import RequestValidator

# From the previous section on models
from my_models import Client

class MyRequestValidator(RequestValidator):

    def validate_client_key(self, client_key, request):
        try:
            Client.query.filter_by(client_key=client_key).one()
            return True
        except NoResultFound:
            return False

中提供了您需要实现的完整API RequestValidator 一节。您可能不需要实现所有方法,具体取决于您希望支持哪些签名方法。

相关章节包括:

3.创建您的复合端点

每个端点都可以彼此独立地工作,但是对于本例来说,更容易将它们视为一个单元。符合OAuth 1 RFC的预配置All-In-One示例 [1] 终结点如下:

# From the previous section on validators
from my_validator import MyRequestValidator

from oauthlib.oauth1 import WebApplicationServer

validator = MyRequestValidator()
server = WebApplicationServer(validator)

相关章节包括:

4.创建您的端点视图

标准的3条腿OAuth需要4个视图、请求和访问令牌,以及授权前和授权后。此外,应该定义一个错误视图,在该视图中可以通知用户无效/恶意的授权请求。

该示例使用了Flask.exe,但应该可以移植到任何框架。

from flask import Flask, redirect, Response, request, url_for
from oauthlib.oauth1 import OAuth1Error
import urlparse


app = Flask(__name__)


@app.route('/request_token', methods=['POST'])
def request_token():
    h, b, s = provider.create_request_token_response(request.url,
            http_method=request.method,
            body=request.data,
            headers=request.headers)
    return Response(b, status=s, headers=h)


@app.route('/authorize', methods=['GET'])
def pre_authorize():
    realms, credentials = provider.get_realms_and_credentials(request.url,
            http_method=request.method,
            body=request.data,
            headers=request.headers)
    client_key = credentials.get('resource_owner_key', 'unknown')
    response = '<h1> Authorize access to %s </h1>' % client_key
    response += '<form method="POST" action="/authorize">'
    for realm in realms or []:
        response += ('<input type="checkbox" name="realms" ' +
                        'value="%s"/> %s' % (realm, realm))
    response += '<input type="submit" value="Authorize"/>'
    return response


@app.route('/authorize', methods=['POST'])
def post_authorize():
    realms = request.form.getlist('realms')
    try:
        h, b, s = provider.create_authorization_response(request.url,
                http_method=request.method,
                body=request.data,
                headers=request.headers,
                realms=realms)
        if s == 200:
            return 'Your verifier is: ' + str(urlparse.parse_qs(b)['oauth_verifier'][0])
        else:
            return Response(b, status=s, headers=h)
    except OAuth1Error as e:
        return redirect(e.in_uri(url_for('/error')))


@app.route('/access_token', methods=['POST'])
def access_token():
    h, b, s = provider.create_access_token_response(request.url,
            http_method=request.method,
            body=request.data,
            headers=request.headers)
    return Response(b, status=s, headers=h)


@app.route('/error', methods=['GET'])
def error():
    # Invalid request token will be most likely
    # Could also be an attempt to change the authorization form to try and
    # authorize realms outside the allowed for this client.
    return 'client did something bad'

5.使用领域保护您的API

让我们定义一个可以用来保护视图的装饰器。

def oauth_protected(realms=None):
    def wrapper(f):
        @functools.wraps(f)
        def verify_oauth(*args, **kwargs):
            validator = OAuthValidator()  # your validator class
            provider = ResourceEndpoint(validator)
            v, r = provider.validate_protected_resource_request(request.url,
                    http_method=request.method,
                    body=request.data,
                    headers=request.headers,
                    realms=realms or [])
            if v:
                return f(*args, **kwargs)
            else:
                return abort(403)
        return verify_oauth
    return wrapper

此时,您已经准备好使用OAuth保护您的API视图了。花些时间想出一组很好的域,因为它们在控制访问方面可能非常强大。

@app.route('/secret', methods=['GET'])
@oauth_protected(realms=['secret'])
def protected_resource():
    return 'highly confidential'

6.使用快速CLI客户端试用您的提供商

此示例假定您使用客户端密钥 key 和客户端密码 secret 以及在端口上本地运行您的FlASK服务器 5000

$ pip install requests requests-oauthlib
>>> key = 'abcdefghijklmnopqrstuvxyzabcde'
>>> secret = 'foo'

>>> # OAuth endpoints given in the Bitbucket API documentation
>>> request_token_url = 'http://127.0.0.1:5000/request_token'
>>> authorization_base_url = 'http://127.0.0.1:5000/authorize'
>>> access_token_url = 'http://127.0.0.1:5000/access_token'

>>> # 2. Fetch a request token
>>> from requests_oauthlib import OAuth1Session
>>> oauth = OAuth1Session(key, client_secret=secret,
>>>         callback_uri='http://127.0.0.1/cb')
>>> oauth.fetch_request_token(request_token_url)

>>> # 3. Redirect user to your provider implementation for authorization
>>> authorization_url = oauth.authorization_url(authorization_base_url)
>>> print 'Please go here and authorize,', authorization_url

>>> # 4. Get the authorization verifier code from the callback url
>>> redirect_response = raw_input('Paste the full redirect URL here:')
>>> oauth.parse_authorization_response(redirect_response)

>>> # 5. Fetch the access token
>>> oauth.fetch_access_token(access_token_url)

>>> # 6. Fetch a protected resource, i.e. user profile
>>> r = oauth.get('http://127.0.0.1:5000/secret')
>>> print r.content

7.让我们知道进展如何!

在我们的 Gitter OAuthLib community 或打开一个 GitHub issue =)

如果遇到问题,启用调试日志记录会很有帮助:

import logging
import oauthlib
import sys
oauthlib.set_debug(True)
log = logging.getLogger('oauthlib')
log.addHandler(logging.StreamHandler(sys.stdout))
log.setLevel(logging.DEBUG)