client api

当时处理这部分的动机是将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-providerdjango-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": "[email protected]", "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": "[email protected]",

    "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'[email protected]',

 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

你可能感兴趣的:(client)