Open edX的第三方登录是作为一个django app存在的,配置过程如下。
LMS配置过程
##安装CAS的Client、Mapper
进入edXapp环境,下载CAS的Client、Mapper
0 1 2 3 4 5 |
sudo su edxapp -s /bin/bash cd ~ source edxapp_env pip install git+https://github.com/kstateome/django-cas pip install git+https://github.com/eduStack/neusoft_cas_mapper |
CAS Client安装目录在下面的地址
0 1 |
/edx/app/edxapp/venvs/edxapp/lib/python2.7/site-packages/cas |
Mapper的安装目录在下面的地址
0 1 |
/edx/app/edxapp/venvs/edxapp/lib/python2.7/site-packages/mitx_cas_mapper |
开启edX特性支持CAS登陆
修改/edx/app/edxapp/lms.env.json
文件如下
0 1 2 3 4 5 6 7 |
"CAS_ATTRIBUTE_CALLBACK": { "module": "mitx_cas_mapper", "function": "populate_user" }, "CAS_EXTRA_LOGIN_PARAMS": "", "CAS_SERVER_URL": "https://wp.edustack.org/wp-cas/", "CAS_USER_DETAILS_RESOLVER": "mitx_cas_mapper.populate_user", |
修改/edx/app/edxapp/edx-platform/lms/envs/common.py
如下
0 1 2 3 4 |
# Even though external_auth is in common, shib assumes the LMS views / urls, so it should only be enabled # in LMS 'AUTH_USE_CAS': True, 'BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH': True, |
同时
0 1 2 3 4 5 6 7 8 9 |
######################## CAS authentication ########################### if FEATURES.get('AUTH_USE_CAS'): CAS_SERVER_URL = 'https://provide_your_cas_url_here' AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'cas.backends.CASBackend', ) #INSTALLED_APPS += ('cas',) MIDDLEWARE_CLASSES += ('cas.middleware.CASMiddleware',) |
修改/edx/app/edxapp/edx-platform/lms/envs/aws.py
如下
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# Django CAS external authentication settings CAS_EXTRA_LOGIN_PARAMS = ENV_TOKENS.get("CAS_EXTRA_LOGIN_PARAMS", None) if FEATURES.get('AUTH_USE_CAS'): CAS_SERVER_URL = ENV_TOKENS.get("CAS_SERVER_URL", None) AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'cas.backends.CASBackend', ) #INSTALLED_APPS += ('cas',) MIDDLEWARE_CLASSES += ('cas.middleware.CASMiddleware',) CAS_ATTRIBUTE_CALLBACK = ENV_TOKENS.get('CAS_ATTRIBUTE_CALLBACK', None) if CAS_ATTRIBUTE_CALLBACK: import importlib CAS_USER_DETAILS_RESOLVER = getattr( importlib.import_module(CAS_ATTRIBUTE_CALLBACK['module']), CAS_ATTRIBUTE_CALLBACK['function'] ) |
修改/edx/app/edxapp/edx-platform/lms/urls.py
如下
0 1 2 3 4 5 |
if settings.FEATURES.get('AUTH_USE_CAS'): urlpatterns += ( url(r'^cas-auth/login/$', 'external_auth.views.cas_login', name="cas-login"), url(r'^cas-auth/logout/$', 'cas.views.logout', {'next_page': '/'}, name="cas-logout"), ) |
增加一个nextpage属性,当用户通过CAS登陆后自动跳转到dashboard。
修改/edx/app/edxapp/edx-platform/common/djangoapps/external_auth/views.py
把django_cas.views
改为cas.view
0 1 2 |
if settings.FEATURES.get('AUTH_USE_CAS'): from cas.views import login as django_cas_login |
修改CAS Client配置文件
在cas目录中找到backends.py
。
默认的Client并没有返回附加的属性,寻找下面的函数
0 1 2 3 4 5 |
def _internal_verify_cas(ticket, service, suffix): """Verifies CAS 2.0 and 3.0 XML-based authentication ticket. Returns username on success and None on failure. """ |
在其返回值增加用户附加属性。也就是代码中的tree。修改如下
原版函数结尾:
0 1 2 3 4 5 6 7 8 9 |
except Exception as e: logger.error('Failed to verify CAS authentication: {message}'.format( message=e )) finally: page.close() return username |
改为下面这样:
0 1 2 3 4 5 6 7 8 9 |
except Exception as e: logger.error('Failed to verify CAS authentication: {message}'.format( message=e )) finally: page.close() return username,tree |
寻找下面的片段
0 1 2 3 4 |
class CASBackend(object): """ CAS authentication backend """ |
改为
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
from student.models import UserProfile from student.models import Registration _CAS_USER_DETAILS_RESOLVER = getattr(settings, 'CAS_USER_DETAILS_RESOLVER', None) class CASBackend(object): """ CAS authentication backend """ supports_object_permissions = False supports_inactive_user = False def authenticate(self, ticket, service): """ Verifies CAS ticket and gets or creates User object NB: Use of PT to identify proxy """ User = get_user_model() username, authentication_response = _verify(ticket, service) if not username: return None try: user = User.objects.get(username__iexact=username) if _CAS_USER_DETAILS_RESOLVER: _CAS_USER_DETAILS_RESOLVER(user, authentication_response) user.save() except User.DoesNotExist: # user will have an "unusable" password if settings.CAS_AUTO_CREATE_USER: user = User.objects.create_user(username, '') if _CAS_USER_DETAILS_RESOLVER: _CAS_USER_DETAILS_RESOLVER(user, authentication_response) user.save() registration = Registration() registration.register(user) profile = UserProfile(user=user) profile.name = username profile.save() else: user = None _CAS_USER_DETAILS_RESOLVER(user, authentication_response) return user |
分析:
username, authentication_response = _verify(ticket, service)
这一行是用户认证后返回的用户名和附加属性的XML树。
对接逻辑如下(try之后的部分):
尝试根据用户名获取用户的model。当用户不存在时候,如果设置了自动创建用户。那么就自动根据用户名创建一个用户的model,如果定义了属性解析器、那么根据CAS认证返回的XML树解析用户的属性,对应更改user的model。之后保存model并注册用户。
问题:
1. 是否应该让用户每次通过CAS登陆后都去解析用户的属性来保持edX中用户的资料和CAS系统提供的一致?
2. user和profile的关系是什么?
3. 用户属性的更改是通过更改user还是更改profile?
更改Mapper使其能正确解析CAS Server返回的XML树
修改/edx/app/edxapp/venvs/edxapp/lib/python2.7/site-packages/mitx_cas_mapper/__init__.py
打印源码中的attr结果如下
0 1 2 3 4 |
attrchild=[<Element '{http://www.yale.edu/tp/cas}first_name' at 0x7fd9ae637a90>, <Element '{http://www.yale.edu/tp/cas}last_name' at 0x7fd9ae637f10>, <Element '{http://www.yale.edu/tp/cas}display_name' at 0x7fd9ae637f50>, <Element '{http://www.yale.edu/tp/cas}user_email' at 0x7fd9ae637f90>] |
所以分析、CAS Server返回结果结构大致如下。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<{http://www.yale.edu/tp/cas}authenticationSuccess> <{http://www.yale.edu/tp/cas}attributes> <{http://www.yale.edu/tp/cas}first_name> firstName </{http://www.yale.edu/tp/cas}first_name> <{http://www.yale.edu/tp/cas}last_name> lastName </{http://www.yale.edu/tp/cas}last_name> <{http://www.yale.edu/tp/cas}display_name> testUser </{http://www.yale.edu/tp/cas}display_name> <{http://www.yale.edu/tp/cas}user_email> 123.com </{http://www.yale.edu/tp/cas}user_email> </{http://www.yale.edu/tp/cas}attributes> </{http://www.yale.edu/tp/cas}authenticationSuccess> |
修改源代码中原有的XPath、对应实际返回的XML Tag即可正常解析XML
CMS配置过程
开启edX特性支持CAS登陆
修改/edx/app/edxapp/cms.env.json
文件如下
0 1 2 3 4 5 6 7 |
"CAS_ATTRIBUTE_CALLBACK": { "module": "mitx_cas_mapper", "function": "populate_user" }, "CAS_EXTRA_LOGIN_PARAMS": "", "CAS_SERVER_URL": "https://wp.edustack.org/wp-cas/", "CAS_USER_DETAILS_RESOLVER": "mitx_cas_mapper.populate_user", |
修改/edx/app/edxapp/edx-platform/cms/envs/common.py
如下,增加一行
0 1 2 3 |
# Even though external_auth is in common, shib assumes the LMS views / urls, so it should only be enabled # in LMS 'AUTH_USE_CAS': True, |
修改/edx/app/edxapp/edx-platform/cms/envs/aws.py
如下
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# Django CAS external authentication settings CAS_EXTRA_LOGIN_PARAMS = ENV_TOKENS.get("CAS_EXTRA_LOGIN_PARAMS", None) if FEATURES.get('AUTH_USE_CAS'): CAS_SERVER_URL = ENV_TOKENS.get("CAS_SERVER_URL", None) AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'cas.backends.CASBackend', ) #INSTALLED_APPS += ('cas',) MIDDLEWARE_CLASSES += ('cas.middleware.CASMiddleware',) CAS_ATTRIBUTE_CALLBACK = ENV_TOKENS.get('CAS_ATTRIBUTE_CALLBACK', None) if CAS_ATTRIBUTE_CALLBACK: import importlib CAS_USER_DETAILS_RESOLVER = getattr( importlib.import_module(CAS_ATTRIBUTE_CALLBACK['module']), CAS_ATTRIBUTE_CALLBACK['function'] ) |
修改/edx/app/edxapp/edx-platform/cms/urls.py
如下
0 1 2 3 4 5 |
if settings.FEATURES.get('AUTH_USE_CAS'): urlpatterns += ( url(r'^cas-auth/login/$', 'external_auth.views.cas_login', name="cas-login"), url(r'^cas-auth/logout/$', 'cas.views.logout', {'next_page': '/'}, name="cas-logout"), ) |
增加一个nextpage属性,当用户通过CAS登陆后自动跳转到dashboard。
修改/edx/app/edxapp/edx-platform/common/djangoapps/external_auth/views.py
把django_cas.views
改为cas.view
0 1 2 |
if settings.FEATURES.get('AUTH_USE_CAS'): from cas.views import login as django_cas_login |
其他的说明
如果要对CAS返回的结果进行解析,需要更改mapper的代码。在mapper的安装目录下可以看到python脚本。在这里给出一个示例,本校CAS将会返回学号等登录信息,需要自动填写姓名、邮箱。解析脚本参考如下。
0 1 2 3 4 5 6 7 8 9 10 |
if attr.find(CAS + 'zjh', NSMAP) is not None: zjh = attr.find(CAS + 'zjh', NSMAP).text if attr.find(CAS + 'yhlb', NSMAP) is not None: yhlb = attr.find(CAS + 'yhlb', NSMAP).text logger.info('zjh={zjh},yhlb={yhlb}'.format(zjh=zjh,yhlb=yhlb)) if yhlb == '本科生代码' or yhlb == '研究生代码': user.email = '%s@mail.bistu.edu.cn' % zjh else: user.email = '%s@bistu.edu.cn' % zjh logger.info('user.email={email}'.format(email=user.email)) |
0 Comments