当时处理这部分的动机是将edx与微信对接
如果你在处理与edx API相关的工作,这篇文章可能对你也有帮助。好比你在编译edx移动端(android和iOS), 这部分工作应该也是最主要的工作之一
思路
我们首先简要做一下任务陈述:允许edx用户通过微信公众平台访问edX,登录以及请求相关的数据
这里假设读者们已经基本了解了OAuth2,包括它的一些基本概念和通信流程,如果还不了解,请先阅读OAuth2相关的材料。
在我们的任务中,我们先识别出OAuth中的参与实体,RO(resource owner),RS (resource server)和Client,至于AS(authorization server)在edx中和RS可以认为一体。
很显然我们的任务中,edx平台作为RS,而edx user是RO,而我们自己写的微信公众号后台便是Client。
由于微信后端和平台拥有者是相同的,所以我就不采用redirect的方式了。而假设Client是受信任的。
那么通信的流程是这样的,edx user在微信给微信公众号中给Client发送账号和密码,而后Client携带用户账号和密码去换取授权令牌(Access Token),且存下授权令牌,如此一来,概念上,用户在微信中便已经保持登录edX的状态了。
而后Client根据用户请求,携带Access Token去服务器请求资源返回给微信用户。
这里不应当混淆的是,使用微信账户登录edx
,和在微信中以edx user身份访问edx
,是两个完全不同的过程,使用微信账户登录edx
本质上是个第三方社交账号登录edx的问题,RS是微信,而edx user在微信中访问edx,RS是edX。
好了,思路基本清晰了。
先前的经验
之前写过一篇博客:让edx为手机端提供接口
本打算按照之前的经验,却发现,采用TokenAuthentication的解决方案除了侵入性太强,不够优雅之外,安全性也得不到保证
EdX API Authentication中有一句话,
OAuth 2.0 is an open standard used by many systems that require secure user authentication
我开始以为,secure只是个建议,稍后我们会发现,这是个强制要求。
无论是OAuth2Authentication, SessionAuthentication还是TokenAuthentication,本质都是个认证问题,而认证过程在django中间件里实现,对关注业务逻辑的开发者而言是透明的,而edx的api使用的统一是OAuth2Authentication和SessionAuthentication。
可选的路线只有一条,开始折腾OAuth2.
目标定位
经过一番跟踪和分析,我们发现了edx/edx-oauth2-provider和django-oauth2-provider与OAuth关系最大
而他们的关系是edx/edx-oauth2-provider依赖于edx/django-oauth2-provider
而edx/django-oauth2-providerfork自caffeinehit/django-oauth2-provider
caffeinehit/django-oauth2-provider的文档对我们很有助益,
实验
定位到这两个关键库,其实接下来的工作就轻松多了。
首先做些试探性的实验。
先去/edx/app/edxapp/lms.env.json
,在FEATURES里加上"ENABLE_OAUTH2_PROVIDER": true,
以及"ENABLE_VIDEO_ABSTRACTION_LAYER_API":true,
,而后去admin里获取一个受信任的Client和Access Token,对应的地址分别是是/admin/oauth2_provider/trustedclient/
和/admin/oauth2/accesstoken/add/
,过期时间(Expires)可以设得远些,使其不易生效,你也通过设置OAUTH_ID_TOKEN_EXPIRATION
来控制失效时间,这个数值衡量的是用户两次登录的时间间隔,好比你要求用户每七天需要登录一次。
那么激动人心的时刻来啦,我们开始请求接口
curl -k -H "Authorization: Bearer Your_Access_Token” http://example.com/api/user/v1/accounts/wwj
{"username": "wwj", "bio": null, "requires_parental_consent": true, "name": "wwj", "country": null, "is_active": true, "profile_image": {"image_url_full": "http://example.com/static/images/default-theme/default-profile_500.de2c6854f1eb.png", "image_url_large": "http://example.com/static/images/default-theme/default-profile_120.33ad4f755071.png", "image_url_medium": "http://example.com/static/images/default-theme/default-profile_50.5fb006f96a15.png", "image_url_small": "http://example.com/static/images/default-theme/default-profile_30.ae6a9ca9b390.png", "has_image": false}, "year_of_birth": null, "level_of_education": null, "goals": null, "language_proficiencies": [], "gender": null, "mailing_address": null, "email": "wwj@example.com", "date_joined": "2015-05-13T09:42:45Z"}
如果你使用httpie(推荐),那么返回的内容将以更易于阅读的形式(缩进高亮),返回给你.之后我们都只要httpie
http http://example.com/api/user/v1/accounts/wwj "Authorization: Bearer 1a17079824f66bfa5116bd8780b5a119e603a79c"
(实际上是header参数)
{
"bio": null,
"country": null,
"date_joined": "2015-05-13T09:42:45Z",
"email": "wwj@qq.com",
"gender": null,
"goals": null,
"is_active": true,
"language_proficiencies": [],
"level_of_education": null,
"mailing_address": null,
"name": "wwj",
"profile_image": {
"has_image": false,
"image_url_full": "http://example.com/static/images/default-theme/default-profile_500.de2c6854f1eb.png",
"image_url_large": "http://example.com/static/images/default-theme/default-profile_120.33ad4f755071.png",
"image_url_medium": "http://example.com/static/images/default-theme/default-profile_50.5fb006f96a15.png",
"image_url_small": "http://example.com/static/images/default-theme/default-profile_30.ae6a9ca9b390.png"
},
"requires_parental_consent": true,
"username": "wwj",
"year_of_birth": null
}
再演示一个使用requests的做法
import requests
headers = {"Authorization": "bearer 1a17079824f66bfa5116bd8780b5a119e603a79c", "User-Agent": "ChangeMeClient/0.1 by YourUsername"}
response = requests.get("http://127.0.0.1/api/user/v1/accounts/wwj", headers=headers)
response.json()
得到
{u'bio': None,
u'country': None,
u'date_joined': u'2015-05-13T09:42:45Z',
u'email': u'wwj@qq.com',
u'gender': None,
u'goals': None,
u'is_active': True,
u'language_proficiencies': [],
u'level_of_education': None,
u'mailing_address': None,
u'name': u'wwj',
u'profile_image': {u'has_image': False,
u'image_url_full': u'http://127.0.0.1/static/images/default-theme/default-profile_500.de2c6854f1eb.png',
u'image_url_large': u'http://127.0.0.1/static/images/default-theme/default-profile_120.33ad4f755071.png',
u'image_url_medium': u'http://127.0.0.1/static/images/default-theme/default-profile_50.5fb006f96a15.png',
u'image_url_small': u'http://127.0.0.1/static/images/default-theme/default-profile_30.ae6a9ca9b390.png'},
u'requires_parental_consent': True,
u'username': u'wwj',
u'year_of_birth': None}
下边演示请求Access Token的过程
使用requests
import requests
import requests.auth
client_auth = requests.auth.HTTPBasicAuth('dc107056a5335b3a7c74', '4e3f1fad6e0583fc80d78541f2ca6cfad8a93bed')
post_data = {"grant_type": "password", "username": "wwj", "password": "wwjtest"}
response = requests.post("http://127.0.0.1/oauth2/access_token", auth=client_auth, data=post_data)
response.json()
得到{u'error': u'invalid_request', u'error_description': u'A secure connection is required.'}
网站需要使用https,nmap查看443端口是close状态。
配置nginx。
启用https
Remember that you should always use HTTPS for all your OAuth 2 requests otherwise you won’t be secured.
OAuth2要求使用https。所以我们为edx做https支持
生成证书
cd /edx/app/nginx/
mkdir conf
chown -R 777 conf #好像不大好
cd conf
#创建服务器私钥,命令会让你输入一个口令
openssl genrsa -des3 -out server.key 1024
#创建签名请求的证书(CSR)
openssl req -new -key server.key -out server.csr
#在加载SSL支持的Nginx并使用上述私钥时除去必须的口令:
cp server.key server.key.org
openssl rsa -in server.key.org -out server.key
配置nginx
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
在/edx/app/nginx/sites-enabled
里,将lms复制为lms_https
sudo diff lms lms_https
1,3c1
< upstream lms-backend {
< server 127.0.0.1:8000 fail_timeout=0;
< }server {
---
> server {
12,13c10,13
<
< listen 80 default;
---
> listen 443;
> ssl on;
> ssl_certificate /edx/app/nginx/conf/server.crt;
> ssl_certificate_key /edx/app/nginx/conf/server.key;
在/edx/app/nginx/sites-enabled/lms
的server结尾里加上
# Forward to HTTPS if we're an HTTP request...
if ($http_x_forwarded_proto = "http") {
set $do_redirect "true";
}
# Run our actual redirect...
if ($do_redirect = "true") {
rewrite ^ https://$host$request_uri? permanent;
}
重启nginx,https方面的设置就好了,你可以访问,https://example.com 啦
https下请求Access Token
import requests
import requests.auth
client_auth = requests.auth.HTTPBasicAuth('dc107056a5335b3a7c74', '4e3f1fad6e0583fc80d78541f2ca6cfad8a93bed')
post_data = {"grant_type": "password", "username": "wwj", "password": "wwjtest"}
response = requests.post("https://127.0.0.1/oauth2/access_token", auth=client_auth, data=post_data, verify=False)
response.json()
``
ok
{u'access_token': u'e751c317435986b2a00425ed7a93a789fbcbeccd',
u'expires_in': 2591999,
u'scope': u'',
u'token_type': u'Bearer'}
微信后端
暂不方便公开源码
todo
- 将mobile api相关的请求全部redirect倒https
- https证书相关
2015.07.15更新
开发群里有小伙伴提到在用android客户端去访问服务器时,会出现这样的错误。javax.net.ssl.SSLPeerUnverifiedException: No peer certificate (文后评论中也有人提到)
这是ssl证书的问题,我此前的做法是不验证。这只是绕过了问题,而没有解决它,在此正面解决它,分以下步骤:
- 申请ssl证书,我用的是免费的startssl。可参考www.startssl.com
- 将申请来的证书加入到lms_https里:
ssl on;
ssl_certificate /etc/nginx/conf/your-ssl-unified.crt;
ssl_certificate_key /etc/nginx/conf/your-ssl.key;
- sudo killall -HUP nginx