Open edX Platform OAuth2 认证实战:获取、校验、刷新与使用 JWT 的完整代码示例
2026/9/16 18:24:21 网站建设 项目流程

Open edX Platform OAuth2 认证实战:获取、校验、刷新与使用 JWT 的完整代码示例

【免费下载链接】openedx-platformThe Open edX LMS & Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform

本文基于 Open edX Platform 官方参考文档 auth_code_samples.rst 整理,覆盖该平台 OAuth2 认证的完整生命周期:使用用户名密码获取 JWT(password 授权)、使用客户端凭证获取 JWT(client_credentials 授权)、校验 JWT 是否过期、用 refresh token 刷新令牌,以及携带 JWT 请求头调用 LMS REST API。读完后你可以直接用文档中的 Python 示例对接 Open edX 的http://<lms>/oauth2/access_token端点,并理解每个请求参数、响应字段(access_tokenrefresh_tokenscope等)与底层端点实现之间的对应关系。

安全警告:令牌即秘密

官方文档在开头就给出了明确警告:Access Token、Refresh Token 和 Client Secret 一般被视为秘密信息,不应出现在你的代码中。文档之所以把它们打印出来,只是为了让示例完整可用。在生产环境中,你不应当向任何不可信的系统或客户端暴露这些令牌。下文的示例沿用了文档原样,实际落地时请自行替换为环境变量或密钥管理方案注入的凭证。

端点与路由:/oauth2/access_token 从哪里来

在动手前,先确认示例中反复出现的 URLhttp://lms.example.com/oauth2/access_token在仓库中的真实来源:

  • LMS 主路由文件 lms/urls.py 将/oauth2/前缀挂载到openedx.core.djangoapps.oauth_dispatch应用,并附带注释提醒开发者应当使用这组包装过的路由以兼容现有客户端代码;
  • oauth_dispatch/urls.py 中定义了三个核心端点:authorize(授权页)、access_token(签发令牌,对应本文所有示例的 POST 目标)和revoke_token(吊销令牌);
  • 底层由 django-oauth-toolkit(oauth2_provider)驱动,应用模型由设置项OAUTH2_PROVIDER_APPLICATION_MODEL = 'oauth2_provider.Application'指定(见 openedx/envs/common.py)。

此外,针对历史上/oauth2/access_token/曾遭受攻击导致 LMS 宕机的情况,平台对该端点内置了速率限制:lms/envs/common.py 中RATELIMIT_ENABLE = TrueRATELIMIT_RATE = '120/m'表示默认每 IP 每分钟最多 120 次请求。批量获取令牌时需注意这一限制。

示例一:使用用户名和密码获取 JWT(password 授权)

这是面向交互式登录场景最直接的取令牌方式。请求向oauth2/access_token端点 POST 表单数据,其中grant_typepassword

import requests from pprint import pprint token_request = requests.post( f"http://lms.example.com/oauth2/access_token", data={ "client_id": "login-service-client-id", "grant_type": "password", "username": "test_user", "password": "test_password", "token_type": "JWT", }, ) pprint(token_request.json())

成功时返回如下结构(示例输出取自原文档):

{'access_token': 'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiAibG1zLWtleSIsICJleHAiOiAxNjkyMjExNjM4LCAiZ3JhbnRfdHlwZSI6ICJwYXNzd29yZCIsI...', 'expires_in': 3600, 'refresh_token': 'm8iXhVlGABu52xFxVFj5rAz8xSjsRq', 'scope': 'read write email profile', 'token_type': 'JWT'}

几个关键字段说明:

  • access_token:标准三段式 JWT(header.payload.signature),示例中 header 声明alg: HS256
  • expires_in: 3600:有效期 3600 秒,与 JWT payload 中expiat相差 1 小时一致;
  • refresh_token:仅在 password 授权(等用户级授权)时返回,是后续"刷新令牌"的凭证;
  • token_type: JWT:声明令牌格式为 JWT,调用 API 时请求头需写成Authorization: JWT <token>

原文档特别强调一条前置条件:该方式要求client_id对应的应用(Application)类型为public。在 Open edX 中,OAuth 客户端应用通过 Studio 的 API 应用管理界面或oauth2_provider.Application模型创建,confidential 类型客户端必须携带client_secret,而 public 客户端可以直接用client_id明文传参。

解码示例中的 JWT payload,可以看到平台在令牌中携带的用户级 claims:

Claim示例值含义
audlms-key受众,校验 JWT 时需以该值为 audience
isshttp://localhost:18000/oauth2签发方,即 LMS 的/oauth2前缀
sub用户 UUID资源所有者标识
preferred_usernamefeanil登录用户名
email/email_verifiedfeanil@axim.org/true邮箱及验证状态
nameFeanil Patel显示名
scopes["read", "write", "email", "profile"]令牌授予的权限范围
filters["user:me"]服务端 API 过滤提示(表示默认只能访问"自己"的数据)
is_restrictedfalse是否为受限令牌
administrator/superusertrue平台级管理员标志
grant_typepassword签发该令牌的授权类型

这些 claims 直接决定了令牌能访问哪些 API 以及访问范围,是理解 Open edX API 鉴权模型的关键。

示例二:使用 client_id 与 client_secret 获取 JWT(client_credentials 授权)

当调用方是服务本身(机器对机器)而非代表某个具体用户时,使用client_credentials授权。客户端凭证通过 HTTP Basic 认证头传递,即base64(client_id:client_secret)

import base64 import requests from pprint import pprint client_id = "ukbclQB8aPh7hgsy8ifPXkPf7fRqgUq1w21f2YZa" # 注意:client_secret 应当真正保密,不应硬编码在代码中, # 此处仅为示例演示 client_secret = "xkN0BJ19q9Jk8UPUppEtC1xe4764c81ioFtlegvokbmnAC7CFCT5gG1Og5nnFmCNc3NHNhUwWWDRVcBfnLSZ4xAlEmSePzfkFtLE06cwR1MuSc0gx9LUEjRrTs3j2vgK" credential = f"{client_id}:{client_secret}" encoded_credential = base64.b64encode(credential.encode("utf-8")).decode("utf-8") headers = {"Authorization": f"Basic {encoded_credential}", "Cache-Control": "no-cache"} data = {"grant_type": "client_credentials", "token_type": "jwt"} token_request = requests.post( "http://lms.example.com/oauth2/access_token", headers=headers, data=data ) pprint(token_request.json())

返回示例:

{'access_token': 'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiAibG1zLWtleSIsICJleHAiOiAxNjkyMjExNjM4LCAiZ3JhbnRfdHlwZSI6ICJjbGllbnQtY3JlZGVudGlhbHMiLCA...', 'expires_in': 3600, 'scope': 'read write email profile', 'token_type': 'JWT'}

与 password 授权的响应相比,有两点明显差异:

  1. 没有refresh_token字段。原文档对此有专门说明:使用client_credentials获取的 JWT 不会附带 refresh token,到期后预期你直接用客户端凭证重新发起一次取令牌请求;
  2. 解码后的 payload 中grant_typeclient_credentials,且filters为空列表——从该示例的 claims 结构看,此类令牌同样携带了用户维度信息(subpreferred_username等),具体权限边界取决于应用配置的 scope。

另外,令牌可用的 scope 并非无限制:oauth_dispatch应用通过自定义 scope 后端ApplicationModelScopes(见 scopes.py)取"该应用已配置 scope + 默认 scope"的交集来约束响应中的scope,这解释了为何示例中固定出现read write email profile

校验 JWT 是否过期

在把令牌送去调用 API 之前,可以在本地快速判断它是否已过期。使用pyjwt库、以签名密钥和受众解码即可:

import jwt # 参见上文示例获取 JWT token jwt_token = token_request.json()['access_token'] try: jwt.decode(jwt_token, "secret", audience="lms-key", algorithms=['HS256']) except jwt.ExpiredSignatureError: # 签名已过期 pass

要点:

  • audience="lms-key"必须与 payload 中的aud一致,解码时若不匹配会抛异常;
  • algorithms=['HS256']需与 header 声明的算法一致;
  • "secret"是 HS256 签名所用的服务端 JWT 密钥,仅适合在受控环境(如本地开发、脚本校验)中使用——再次呼应文档开头的安全警告。

使用 Refresh Token 刷新 JWT

password 授权返回的refresh_token用于在access_token过期后低成本地换发新令牌,而不需要用户再次输入密码。刷新请求仍然 POST 到同一个oauth2/access_token端点,把grant_type换成refresh_token并带上旧的 refresh token:

import requests from pprint import pprint # 参见"使用用户名和密码获取 JWT",响应中包含 refresh_token 属性 refresh_token = token_request.json()['refresh_token'] refreshed_token_request = requests.post( f"http://lms.example.com/oauth2/access_token", data={ "client_id": "login-service-client-id", "grant_type": "refresh_token", "refresh_token": token_request.json()['refresh_token'], "token_type": "JWT", }, ) pprint(refreshed_token_request.json())

返回示例(原文档原样保留):

{'access_token': 'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiAibG1zLWtleSIsICJleHAiOiAxNjkyMjE1MTgwLCAiZ3JhbnRfdHlwZSI6ICJwYXNzd29yZCIsICJpYXQiOiAxNjkyMjExNTgwLCA...', 'expires_in': 3600, 'token_type': 'JWT', 'scope': 'read write email profile', 'refresh_token': 'V5fbgDt2RPVnmI6Q3c6cJ3OjVriGii'}

注意两点:

  • 刷新响应中grant_typeclaims 仍标记为password(沿用原授权类型),但iat/exp已更新为新的时间窗,expires_in依旧 3600 秒;
  • 响应同时返回了一个新的refresh_token,意味着 refresh token 是会轮转的——应当保存并覆盖使用最新值,而非复用初始令牌。

携带 JWT 请求头调用 API

拿到有效access_token后,调用 LMS 的 REST API 只需在请求头中携带Authorization: JWT <token>(注意 scheme 是JWT而非常见的Bearer)。文档以查询当前用户课程注册为例:

# 参见上文示例获取 JWT token access_token = token_request.json()["access_token"] enrollment_request = requests.get( "http://lms.example.com/api/enrollment/v1/enrollment", headers={"Authorization": f"JWT {access_token}"}, ) pprint(enrollment_request.json())

示例响应是一条注册记录,字段完整体现了 JWT claims 的作用——user对应 payload 中的preferred_usernamefilters: ["user:me"]决定了该令牌只能查到该用户自己的注册:

[{'course_details': {'course_end': None, 'course_id': 'course-v1:TestX+Course+1', 'course_modes': [{'bulk_sku': None, 'currency': 'usd', 'description': None, 'expiration_datetime': None, 'min_price': 0, 'name': 'Audit', 'sku': None, 'slug': 'audit', 'suggested_prices': ''}], 'course_name': 'Open edX Test Course', 'course_start': '2022-04-09T00:00:00Z', 'enrollment_end': None, 'enrollment_start': None, 'invite_only': False, 'pacing_type': 'Instructor Paced'}, 'created': '2023-08-17T14:10:48.476967Z', 'is_active': True, 'mode': 'audit', 'user': 'test_user'}]

适用前提与实践小结

  • 所有示例假定 LMS 的 OAuth2 签发端点暴露在http://lms.example.com/oauth2/access_token,示例令牌中的isshttp://localhost:18000/oauth2,说明文档环境为本地开发栈(devstack);对接真实站点时请替换域名并改用 HTTPS。
  • 四种操作对应同一个端点、不同grant_typepassword(需 public 客户端、返回 refresh token)、client_credentials(Basic 认证传递凭证、无 refresh token)、refresh_token(轮转 refresh token);令牌过期则本地用pyjwtaudience="lms-key"HS256校验。
  • 调用 API 时请求头格式固定为Authorization: JWT <access_token>
  • 生产环境务必遵守文档开头的安全警告:令牌与 secret 不落代码;同时留意 lms/envs/common.py 所示的120/m每 IP 速率限制,避免脚本高频取令牌被限流。

【免费下载链接】openedx-platformThe Open edX LMS & Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询