ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

使用 Authlib 在 Django 中构建 OAuth 1.0 授权服务器:CacheAuthorizationServer 完整实战指南

使用 Authlib 在 Django 中构建 OAuth 1.0 授权服务器:CacheAuthorizationServer 完整实战指南

【免费下载链接】authlib

The ultimate Python library in building OAuth, OpenID Connect clients and servers. JWS, JWE, JWK, JWA, JWT included.

项目地址:https://gitcode.com/gh_mirrors/au/authlib
点击查看免费下载

导读

本文基于 Authlib 官方文档 docs/oauth1/provider/django/authorization-server.rst,完整讲解如何在 Django 项目中搭建 OAuth 1.0(RFC 5849)授权服务器。你将学会:用CacheAuthorizationServer一行代码完成服务端初始化,通过三个视图函数实现"临时凭证签发 → 资源所有者授权 → 令牌凭证兑换"的完整流程,并结合仓库源码理解临时凭证缓存、nonce 防重放、签名校验等底层机制。

授权服务器:三个端点,一次初始化

OAuth 1.0 授权服务器(Authorization Server)对外提供三类关键端点:

  1. 临时凭证端点(Temporary Credential Endpoint):客户端通过它获取临时凭证(request token);
  2. 资源所有者授权端点(Authorization Endpoint):客户端将用户重定向到此页面,由资源所有者(用户)批准或拒绝授权请求;
  3. 令牌凭证端点(Token Endpoint):客户端使用临时凭证和用户授权的oauth_verifier,兑换正式的令牌凭证。

当资源所有者批准授权后,授权服务器即向客户端签发令牌凭证(token credential),客户端凭此访问受保护资源。

Authlib 的 Django 实现大量依赖缓存(cache)。这意味着你无需自行维护临时凭证、时间戳(timestamp)和 nonce 的去重逻辑——这些都已内置实现(源码见 authlib/integrations/django_oauth1/authorization_server.py)。

创建授权服务器非常简单,只需要提供Client和Token两个模型:

from your_project.models import Client, Token from authlib.integrations.django_oauth1 import CacheAuthorizationServer authorization_server = CacheAuthorizationServer(Client, Token)

从源码结构看,django_oauth1集成模块(authlib/integrations/django_oauth1/init.py)对外导出BaseServer、CacheAuthorizationServer和ResourceProtector,其中CacheAuthorizationServer继承自BaseServer,而BaseServer又继承自 RFC 5849 的通用AuthorizationServer(authlib/oauth1/rfc5849/authorization_server.py)。

开发小贴士:在本地(localhost)开发时,OAuth 1.0 要求 HTTPS 传输。若在本地调试,需要先设置环境变量(详见 docs/oauth1/provider/django/index.rst):

export AUTHLIB_INSECURE_TRANSPORT=true

Resource Owner:直接复用 Django 内置 User

资源所有者(Resource Owner)就是正在使用你服务的用户,他可以通过用户名/邮箱加密码,或其他方式登录你的网站。在 Django 中,直接使用内置的contrib用户模型即可:

from django.contrib.auth.models import User

Client:实现 ClientMixin 的三个方法

客户端(Client)是一个"代表资源所有者、并征得其授权后发起受保护资源请求"的应用程序。它至少包含三类信息:

  • Client Identifier,通常称为client_id;
  • Client Password,通常称为client_secret;
  • Client RSA Public Key(如果支持 RSA-SHA1 签名方法)。

Authlib 没有内置 Django 的 Client 模型,需要你自行实现。结合ClientMixin(定义见 authlib/oauth1/rfc5849/models.py),标准写法如下:

from django.db import models from django.contrib.auth.models import User from authlib.oauth1 import ClientMixin class Client(models.Model, ClientMixin): user = models.ForeignKey(User, on_delete=CASCADE) client_id = models.CharField(max_length=48, unique=True, db_index=True) client_secret = models.CharField(max_length=48, blank=True) default_redirect_uri = models.TextField(blank=False, default='') def get_default_redirect_uri(self): return self.default_redirect_uri def get_client_secret(self): return self.client_secret def get_rsa_public_key(self): return None

ClientMixin要求实现的方法与用途:

方法用途
get_default_redirect_uri()返回客户端默认回调地址(当临时凭证未携带oauth_callback或为oob时使用)
get_client_secret()返回client_secret,用于 HMAC-SHA1 / PLAINTEXT 签名计算
get_rsa_public_key()返回 RSA 公钥,仅在使用 RSA-SHA1 签名方法时需要,否则返回None

客户端由网站上的开发者(即某个User)注册。仓库测试中的示例模型(tests/django/test_oauth1/models.py)与上述写法一致,区别仅在于测试版get_rsa_public_key()会返回测试密钥文件。

Token:不设过期时间的持久化凭证

令牌凭证(token credential)用于访问资源所有者的资源。与 OAuth 2.0 不同,OAuth 1.0 的令牌凭证不会过期。因此它应当保存到持久化数据库,而非缓存。Django 中的实现示例如下:

from django.db import models from django.contrib.auth.models import User from authlib.oauth1 import TokenCredentialMixin class Token(models.Model, TokenCredentialMixin): user = models.ForeignKey(User, on_delete=CASCADE) client_id = models.CharField(max_length=48, db_index=True) oauth_token = models.CharField(max_length=84, unique=True, db_index=True) oauth_token_secret = models.CharField(max_length=84) def get_oauth_token(self): return self.oauth_token def get_oauth_token_secret(self): return self.oauth_token_secret

TokenCredentialMixin需要实现get_oauth_token()与get_oauth_token_secret()两个方法(见 authlib/oauth1/rfc5849/models.py)。从 authlib/integrations/django_oauth1/authorization_server.py 的create_token_credential源码可以看到,兑换令牌时服务器会用temporary_credential.get_user_id()和temporary_credential.get_client_id()自动填充user_id与client_id字段,然后保存入库:

def create_token_credential(self, request): temporary_credential = request.credential token = self.token_generator() item = self.token_model( oauth_token=token["oauth_token"], oauth_token_secret=token["oauth_token_secret"], user_id=temporary_credential.get_user_id(), client_id=temporary_credential.get_client_id(), ) item.save() return item

服务端实现:三个视图函数

完成模型后,就可以创建授权与签发令牌的端点视图了。

1. 临时凭证端点

该端点供客户端获取临时凭证,仅接受 POST 请求:

from django.views.decorators.http import require_http_methods @require_http_methods(["POST"]) def initiate_temporary_credential(request): return server.create_temporary_credential_response(request)

在底层,authlib/oauth1/rfc5849/authorization_server.py 的validate_temporary_credentials_request会依次校验:HTTP 方法必须为 POST、必须携带oauth_consumer_key、必须携带合法oauth_callback(允许字面量"oob"或绝对 URI)、客户端必须存在,随后校验时间戳/nonce 与签名。通过后返回形如下面的响应体:

oauth_token=hh5s93j4hdidpola&oauth_token_secret=hdhd0244k9j7ao03&oauth_callback_confirmed=true

create_temporary_credential的具体落库逻辑由 Django 集成层实现(见下文"缓存与配置"小节)。

2. 资源所有者授权端点

客户端会将用户重定向到此授权页面,由资源所有者批准或拒绝请求:

from django.shortcuts import render def authorize(request): # 请确保用户已自行登录(此处未包含登录校验逻辑) if request.method == 'GET': try: req = server.check_authorization_request(request) context = {'req': req} return render(request, 'authorize.html', context) except OAuth1Error as error: context = {'error': error} return render(request, 'error.html', context) granted = request.POST.get('granted') if granted: grant_user = request.user else: grant_user = None try: return server.create_authorization_response(request, grant_user) except OAuth1Error as error: context = {'error': error} return render(request, 'error.html', context)

该视图的核心逻辑是:GET 请求时先check_authorization_request校验oauth_token是否对应一份有效的临时凭证(缺少oauth_token抛MissingRequiredParameterError,临时凭证不存在抛InvalidTokenError,见源码validate_authorization_request);POST 时根据表单中的granted字段决定grant_user,然后调用create_authorization_response。

从 authlib/oauth1/rfc5849/authorization_server.py 的create_authorization_response源码可以看到两种结果:

  • 用户拒绝(grant_user is None):以 302 重定向到回调地址,并附带access_denied错误参数;
  • 用户批准:生成一个 36 字符的oauth_verifier,将其与user_id一起写回缓存中的临时凭证(见create_authorization_verifier),然后以 302 重定向,回调地址带oauth_token与oauth_verifier两个参数:
http://printer.example.com/ready?oauth_token=hh5s93j4hdidpola&oauth_verifier=hfdp7dh39dks9884

当临时凭证没有回调地址或为"oob"时,服务器会回退到该客户端模型的get_default_redirect_uri()。

3. 令牌凭证端点

客户端携带临时凭证和资源所有者授权的oauth_verifier来兑换令牌凭证:

from django.views.decorators.http import require_http_methods @require_http_methods(["POST"]) def issue_token(request): return server.create_token_response(request)

validate_token_request的校验顺序(见源码)为:oauth_consumer_key必须存在 → 客户端必须存在 →oauth_token必须存在 → 临时凭证必须有效 →oauth_verifier必须与临时凭证匹配(token.check_verifier(verifier))→ 校验时间戳/nonce 与签名。全部通过后create_token_response生成令牌并删除临时凭证,返回:

oauth_token=nnch734d00sl2jdk&oauth_token_secret=pfkkdhi9sl3r4s00

注册 URL 路由

最后,把这几个视图注册到 URL 配置中。例如:

# urls.py from django.urls import path urlpatterns = [ path('oauth/initiate', initiate_temporary_credential), path('oauth/authorize', authorize), path('oauth/token', issue_token), ]

仓库测试(tests/django/test_oauth1/test_authorize.py)正是使用/oauth/initiate、/oauth/authorize、/oauth/token这三个路径来验证完整流程的。

缓存与配置:AUTHLIB_OAUTH1_PROVIDER 详解

CacheAuthorizationServer之所以"开箱即用",是因为临时凭证、nonce 都存放在 Django 缓存中(默认django.core.cache)。相关配置统一放在 Django 设置文件的AUTHLIB_OAUTH1_PROVIDER字典里(源码见 authlib/integrations/django_oauth1/authorization_server.py):

# settings.py AUTHLIB_OAUTH1_PROVIDER = { "nonce_expires_in": 86400, # nonce 缓存有效期(秒),默认 86400 "signature_methods": ["HMAC-SHA1", "PLAINTEXT"], # 支持的签名方法,默认 ["HMAC-SHA1"] "temporary_credential_expires_in": 86400, # 临时凭证缓存有效期(秒),默认 86400 "temporary_credential_key_prefix": "temporary_credential:", # 临时凭证缓存键前缀 }

各配置项的实际作用:

  • nonce_expires_in:nonce 防重放记录在缓存中的保留时长,默认 86400 秒。nonce的缓存键在 authlib/integrations/django_oauth1/nonce.py 中构造为nonce:{nonce}-{timestamp}-{client_id},若请求携带oauth_token还会追加-{token};
  • signature_methods:覆盖默认支持的签名方法列表。默认仅支持HMAC-SHA1(定义于 authlib/oauth1/rfc5849/base_server.py 的SUPPORTED_SIGNATURE_METHODS),可扩展为RSA-SHA1、PLAINTEXT;
  • temporary_credential_expires_in与temporary_credential_key_prefix:控制临时凭证在缓存中的过期时间与键前缀。

create_temporary_credential的缓存写入逻辑(源码)如下:以key_prefix + oauth_token为键,把client_id、oauth_callback与令牌一起写入缓存,并设置timeout:

def create_temporary_credential(self, request): key_prefix = self._temporary_credential_key_prefix token = self.token_generator() client_id = request.client_id redirect_uri = request.redirect_uri key = key_prefix + token["oauth_token"] token["client_id"] = client_id if redirect_uri: token["oauth_callback"] = redirect_uri cache.set(key, token, timeout=self._temporary_expires_in) return TemporaryCredential(token)

此外,token_generator默认生成 42 字符的oauth_token与 48 字符的oauth_token_secret;你也可以在构造服务器时传入自定义生成器:

authorization_server = CacheAuthorizationServer(Client, Token, token_generator=my_generator)

底层机制:时间戳、nonce 与签名校验

OAuth 1.0 的安全性依赖签名与防重放机制,这部分在 authlib/oauth1/rfc5849/base_server.py 中统一实现:

  • 时间戳校验:oauth_timestamp必须是正整数,且与当前时间的差值不能超过EXPIRY_TIME(默认 300 秒),超出即拒绝。使用PLAINTEXT签名方法时,时间戳和 nonce 可以省略;
  • nonce 防重放:同一 nonce 在同一时间戳、同一客户端(及同一令牌)组合下必须唯一,重复使用抛InvalidNonceError;
  • 签名校验:SUPPORTED_SIGNATURE_METHODS之外的签名方法抛UnsupportedSignatureMethodError,签名不匹配抛InvalidSignatureError。内置三种校验器(authlib/oauth1/rfc5849/signature.py):HMAC-SHA1、RSA-SHA1、PLAINTEXT,签名基串按 RFC 5849 3.4.1 节构造。

响应头也遵循 OAuth 规范:令牌类响应统一携带Content-Type: application/x-www-form-urlencoded、Cache-Control: no-store、Pragma: no-cache(见TOKEN_RESPONSE_HEADER)。

测试验证与资源服务器配套

仓库为这套 Django OAuth 1.0 集成提供了完整的测试覆盖(目录 tests/django/test_oauth1/):

  • conftest.py:构造CacheAuthorizationServer(Client, TokenCredential)测试服务器,并分别提供PLAINTEXT、RSA-SHA1签名方法的配置化 fixture;
  • test_authorize.py:验证授权端点——无效oauth_token抛InvalidTokenError,用户拒绝返回 302 且带access_denied,用户批准返回带oauth_verifier的 302;
  • test_token_credentials.py:验证令牌兑换端点的参数缺失、无效客户端、无效令牌、重复 OAuth 参数,以及三种签名方法的成功与失败路径(含 nonce 重用导致的invalid_nonce);
  • test_resource_protector.py:验证受保护资源端点的访问控制。

授权服务器签发令牌之后,还需要保护你的用户资源,这由同模块的ResourceProtector完成(详见 docs/oauth1/provider/django/resource-server.rst)。它作为装饰器使用,通过后会在request.oauth1_credential中注入 Token 实例:

from django.http import JsonResponse from authlib.integrations.django_oauth1 import ResourceProtector require_oauth = ResourceProtector(Client, TokenCredential) @require_oauth def user_api(request): user = request.oauth1_credential.user return JsonResponse(dict(username=user.username))

至此,一个完整的 Django OAuth 1.0 提供方(授权服务器 + 资源服务器)即搭建完成:客户端发起临时凭证请求 → 用户授权 → 兑换令牌 → 携带令牌访问受保护资源,全链路均由 Authlib 的缓存内置逻辑与签名校验保驾护航。

【免费下载链接】authlib

The ultimate Python library in building OAuth, OpenID Connect clients and servers. JWS, JWE, JWK, JWA, JWT included.

项目地址:https://gitcode.com/gh_mirrors/au/authlib
点击查看免费下载
上一篇:三步实现健康160自动化挂号:告别手动抢号的智能解决方案
下一篇:终极指南:3分钟搞定国家中小学智慧教育平台电子课本下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进