OAuth 2.0服务器例程:https://github.com/authlib/example-oauth2-server
RFC6749中文版:http://colobu.com/2017/04/28/oauth2-rfc6749/
还有一篇比较好的文章:https://blog.csdn.net/tclzsn7456/article/details/79550249
- response_type:授权类型,固定值‘code’
- client_id:客户端id
- redirect_uri:重定向URI
- scope:申请的权限范围
- grant_type:使用哪种授权模式
- code:code
- redirect_uri:指重定向URI
- client_id:指客户端id
- client_secret:指客户端密钥
- access_token:访问令牌
- token_type:令牌类型,bearer类型 / mac类型
- expires_in:过期时间,单位秒
- refresh_token:更新令牌,用即将过期token换取新token
- scope:权限范围
这篇文章主要是了解Flask实现的工作原理以及OAuth 2.0程序中的一些技术细节。
在一开始,我们需要对OAuth 2.0规范有一些基本的了解。首先阅读OAuth 2.0授权框架。
如果是在localhost上进行开发,请记住设置环境变量:
export AUTHLIB_INSECURE_TRANSPORT=true
授权服务器(Authorization Server)
授权服务器为授权,签发令牌,刷新令牌和撤销令牌提供了多个端点。当资源所有者(用户)授予授权时,此服务器将向客户端发出访问令牌。
在创建授权服务器之前,我们需要了解几个概念:
资源所有者(Resource Owner)
资源所有者是使用您的服务的用户。资源所有者可以使用用户名/电子邮件和密码或其他方法登录您的网站。
一个资源所有者SHOULD实现get_user_id()方法,让我们以SQLAlchemy模型为例:
class User(Model):
id = Column(Integer, primary_key=True)
# other columns
def get_user_id(self):
return self.id
客户(Client)
客户端是代表资源所有者及其授权发出受保护资源请求的应用程序。它至少包含三个信息:
- 客户端标识符,通常称为client_id
- 客户端密码,通常称为client_secret
- 客户端令牌端点认证方法
Authlib为SQLAlchemy提供了一个mixin,用这个mixin定义客户端:
from authlib.flask.oauth2.sqla import OAuth2ClientMixin
class Client(Model, OAuth2ClientMixin):
id = Column(Integer, primary_key=True)
user_id = Column(
Integer, ForeignKey('user.id', ondelete='CASCADE')
)
user = relationship('User')
客户端由您的网站上的用户(开发人员)注册。如果您决定自己实现所有缺少的方法,请深入了解 ClientMixinAPI参考。
令牌(Token)
注意
现在只支持Bearer Token。MAC令牌仍在草拟中,它将在进入RFC时可用。
关于Bearer Token使用的的博客https://www.cnblogs.com/XiongMaoMengNan/p/6785155.html
令牌用于访问用户的资源。发出的令牌具有有效的持续时间,有限的范围等。它至少包含:
- access_token:用于授权http请求的令牌。
- refresh_token :(可选)用于交换新访问令牌的令牌
- client_id:此令牌发布给哪个客户端
- expires_at:此令牌何时到期
- scope:此令牌可以访问的有限资源范围
使用Authlib提供的SQLAlchemy mixin:
from authlib.flask.oauth2.sqla import OAuth2TokenMixin
class Token(db.Model, OAuth2TokenMixin):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(
db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')
)
user = db.relationship('User')
令牌与资源所有者相关联。它没有确定的名称,我们称之为它user,但它可以是其他任何东西。
服务器(Server)
Authlib提供了一个随时可用的功能,AuthorizationServer 它具有处理请求和响应的内置工具:
from authlib.flask.oauth2 import AuthorizationServer
def query_client(client_id):
return Client.query.filter_by(client_id=client_id).first()
def save_token(token, request):
if request.user:
user_id = request.user.get_user_id()
else:
# client_credentials grant_type
user_id = request.client.user_id
# or, depending on how you treat client_credentials
user_id = None
item = Token(
client_id=request.client.client_id,
user_id=user_id,
**token
)
db.session.add(item)
db.session.commit()
# or with the helper
from authlib.flask.oauth2.sqla import (
create_query_client_func,
create_save_token_func
)
query_client = create_query_client_func(db.session, Client)
save_token = create_save_token_func(db.session, Token)
server = AuthorizationServer(
app, query_client=query_client, save_token=save_token
)
它也可以使用init_app进行懒惰初始化:
server = AuthorizationServer()
server.init_app(app, query_client=query_client, save_token=save_token)
尽管没有以下这些配置它也很好用。但是,它还是可以使用以下设置进行配置:
配置 | 解释 |
---|---|
OAUTH2_TOKEN_EXPIRES_IN | expires_in为每个拨款定义的字典 |
OAUTH2_ACCESS_TOKEN_GENERATOR | 用于导入要生成的函数的函数或模块路径字符串 access_token |
OAUTH2_REFRESH_TOKEN_GENERATOR | 用于导入要生成的函数的函数或模块路径字符串refresh_token。它也可以True/False |
OAUTH2_ERROR_URIS | (error,error_uri)的元组列表 |
以下是配置OAUTH2_TOKEN_EXPIRES_IN的一个简单例子:
OAUTH2_TOKEN_EXPIRES_IN = {
'authorization_code': 864000,
'implicit': 3600,
'password': 864000,
'client_credentials': 864000
}
以下是配置OAUTH2_ACCESS_TOKEN_GENERATOR的一个简单例子:
def gen_access_token(client, grant_type, user, scope):
return create_some_random_string()
配置OAUTH2_REFRESH_TOKEN_GENERATOR时也接受相同的参数。
现在定义授权端点。此端点由以下内容使用授权码模式(authorization_code) 和授权码简化模式(implicit)授予:
from flask import request, render_template
from your_project.auth import current_user
@app.route('/oauth/authorize', methods=['GET', 'POST'])
def authorize():
# Login is required since we need to know the current resource owner.
# It can be done with a redirection to the login page, or a login
# form on this authorization page.
if request.method == 'GET':
grant = server.validate_consent_request(end_user=current_user)
return render_template(
'authorize.html',
grant=grant,
user=current_user,
)
confirmed = request.form['confirm']
if confirmed:
# granted by resource owner
return server.create_authorization_response(current_user)
# denied by resource owner
return server.create_authorization_response(None)
这是一个简单的演示,真实案例应该更复杂。
令牌端点更容易:
@app.route('/oauth/token', methods=['POST'])
def issue_token():
return server.create_token_response()
但是,路线无法正常工作。我们需要为他们注册支持的拨款。
注册授权
RFC6749定义了四种授权类型,您也可以创建自己的扩展授权。将支持的授权类型注册到授权服务器。
授权码授权(Authorization Code Grant)
授权码授权是一种非常常见的授权类型,几乎每个OAuth 2提供商都支持它。它使用授权代码来交换访问令牌。在这种情况下,我们需要一个存储授权代码的地方。它可以保存在数据库或像redis这样的缓存中。这是AuthorizationCode的SQLAlchemy mixin :
from authlib.flask.oauth2.sqla import OAuth2AuthorizationCodeMixin
class AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(
db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')
)
user = db.relationship('User')
通过子类实现授权码授权:
from authlib.specs.rfc6749 import grants
from authlib.common.security import generate_token
class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
def create_authorization_code(self, client, grant_user, request):
# you can use other method to generate this code
code = generate_token(48)
item = AuthorizationCode(
code=code,
client_id=client.client_id,
redirect_uri=request.redirect_uri,
scope=request.scope,
user_id=grant_user.get_user_id(),
)
db.session.add(item)
db.session.commit()
return code
def parse_authorization_code(self, code, client):
item = AuthorizationCode.query.filter_by(
code=code, client_id=client.client_id).first()
if item and not item.is_expired():
return item
def delete_authorization_code(self, authorization_code):
db.session.delete(authorization_code)
db.session.commit()
def authenticate_user(self, authorization_code):
return User.query.get(authorization_code.user_id)
# register it to grant endpoint
server.register_grant(AuthorizationCodeGrant)
需要注意的是,授权码授权(AuthorizationCodeGrant)也是最复杂的授权。
默认允许令牌端点身份验证方法是:
- client_secret_basic
- client_secret_post
- none
您可以在子类中更改它,例如删除none身份验证方法:
class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post']
隐式授予/授权码简化授权(implicit Grant)
隐式授权类型通常在浏览器中使用,当资源所有者授予访问权限时,访问令牌在重定向URI中发出,没有遗漏的实现,这意味着它可以很容易地注册:
from authlib.specs.rfc6749 import grants
# register it to grant endpoint
server.register_grant(grants.ImplicitGrant)
Implicit Grant由没有client_secret的公共客户端使用。仅允许令牌端点身份验证方法:none
资源所有者密码凭证授予(Resource Owner Password Credentials Grant)
资源所有者使用他的用户名和密码来交换访问令牌,只有当客户端值得信任时才应使用此授权类型,并使用以下子类实现它ResourceOwnerPasswordCredentialsGrant:
from authlib.specs.rfc6749 import grants
class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant):
def authenticate_user(self, username, password):
user = User.query.filter_by(username=username).first()
if user.check_password(password):
return user
# register it to grant endpoint
server.register_grant(PasswordGrant)
默认允许令牌端点身份验证方法:client_secret_basic。您可以在子类中添加更多内容:
class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant):
TOKEN_ENDPOINT_AUTH_METHODS = [
'client_secret_basic', 'client_secret_post'
]
客户端凭证授权(Client Credentials Grant)
客户端凭据授予类型可以访问公共资源并可以使用客户端的创建者资源,具体取决于您如何向此授权类型发出令牌。它可以很容易地注册:
from authlib.specs.rfc6749 import grants
# register it to grant endpoint
server.register_grant(grants.ClientCredentialsGrant)
默认允许令牌端点身份验证方法:client_secret_basic。您可以在子类中添加更多内容:
class ClientCredentialsGrant(grants.ClientCredentialsGrant):
TOKEN_ENDPOINT_AUTH_METHODS = [
'client_secret_basic', 'client_secret_post'
]
刷新令牌(Refresh Token)
许多OAuth2提供程序尚未实现刷新令牌端点。Authlib将其作为授权类型提供,使用以下子类实现它 RefreshTokenGrant:
from authlib.specs.rfc6749 import grants
class RefreshTokenGrant(grants.RefreshTokenGrant):
def authenticate_refresh_token(self, refresh_token):
item = Token.query.filter_by(refresh_token=refresh_token).first()
# define is_refresh_token_expired by yourself
if item and not item.is_refresh_token_expired():
return item
def authenticate_user(self, credential):
return User.query.get(credential.user_id)
# register it to grant endpoint
server.register_grant(RefreshTokenGrant)
默认允许令牌端点身份验证方法:client_secret_basic。您可以在子类中添加更多内容:
class RefreshTokenGrant(grants.RefreshTokenGrant):
TOKEN_ENDPOINT_AUTH_METHODS = [
'client_secret_basic', 'client_secret_post'
]
其他令牌端点(Other Token Endpoints)
Flask OAuth 2.0授权服务器有一个注册其他令牌端点的方法:authorization_server.register_endpoint。找到可用的端点:
- 注册撤销端点(Register Revocation Endpoint)
- 注册自省端点(Register Introspection Endpoint)
保护资源(Protect Resources)
保护用户资源,以便只有具有授权访问令牌的授权客户端才能访问给定的范围资源。
资源服务器可以是授权服务器以外的其他服务器。以下是保护用户资源的方法:
from flask import jsonify
from authlib.flask.oauth2 import ResourceProtector, current_token
from authlib.specs.rfc6750 import BearerTokenValidator
class MyBearerTokenValidator(BearerTokenValidator):
def authenticate_token(self, token_string):
return Token.query.filter_by(access_token=token_string).first()
def request_invalid(self, request):
return False
def token_revoked(self, token):
return token.revoked
# only bearer token is supported currently
ResourceProtector.register_token_validator(MyBearerTokenValidator())
# you can also create BearerTokenValidator with shortcut
from authlib.flask.oauth2.sqla import create_bearer_token_validator
BearerTokenValidator = create_bearer_token_validator(db.session, Token)
ResourceProtector.register_token_validator(BearerTokenValidator())
require_oauth = ResourceProtector()
@app.route('/user')
@require_oauth('profile')
def user_profile():
user = current_token.user
return jsonify(user)
如果资源不受作用域保护,请使用None:
@app.route('/user')
@require_oauth()
def user_profile():
user = current_token.user
return jsonify(user)
# or with None
@app.route('/user')
@require_oauth(None)
def user_profile():
user = current_token.user
return jsonify(user)
它current_token是您在上面定义的Token模型的代理。由于存在user对令牌模型的关系,我们可以访问此 user用current_token.user。
如果喜欢使用装饰器,那么还有一个with声明给你使用:
@app.route('/user')
def user_profile():
with require_oauth.acquire('profile') as token:
user = token.user
return jsonify(user)
MethodView与瓶的RESTful
你也可以使用require_oauth装饰器flask.views.MethodView 和flask_restful.Resource:
from flask.views import MethodView
class UserAPI(MethodView):
decorators = [require_oauth('profile')]
from flask_restful import Resource
class UserAPI(Resource):
method_decorators = [require_oauth('profile')]
注册错误(Register Error URIs)
要为调试创建更好的开发人员体验,建议您为错误创建一些文档。这是一个内置错误列表 。
您可以设计一个文档页面,其中包含每个错误的描述。例如,有一个网页invalid_client:
https://developer.your-company.com/errors#invalid-client
在这种情况下,您可以使用OAUTH2_ERROR_URIS 配置注册错误URI :
OAUTH2_ERROR_URIS = [
('invalid_client', 'https://developer.your-company.com/errors#invalid-client'),
# other error URIs
]
如果没有OAUTH2_ERROR_URIS,则错误响应将不包含任何 error_uri数据。
关于错误的
也可以添加i18n支持error_description。该功能已在0.8版本中实现,但仍有工作要做。
自定义授权类型(Custom Grant Types)
也可以创建自己的授权类型。在Authlib中,Grant 支持两个端点:
1、授权端点:可以处理请求response_type。
2、令牌端点:发出令牌的端点。
使用BaseGrant创建自定义授权类型:
from authlib.specs.rfc6749 import grants
class MyCustomGrant(grants.BaseGrant):
AUTHORIZATION_ENDPOINT = False # if you want to support it
TOKEN_ENDPOINT = True # if you want to support it
GRANT_TYPE = 'custom-grant-type-name'
def validate_authorization_request(self):
# only needed if AUTHORIZATION_ENDPOINT = True
def create_authorization_response(self, grant_user):
# only needed if AUTHORIZATION_ENDPOINT = True
def validate_token_request(self):
# only needed if TOKEN_ENDPOINT = True
def create_token_response(self):
# only needed if TOKEN_ENDPOINT = True
为了更好地理解,您可以阅读内置授权类型的源代码。还有其他规范定义的扩展授权类型:
使用JWT作为授权授权