需求
在公司平台的开发中,由于内部平台越来越多,本次要求我们开发的平台需要同步公司的 OA 账号。
那么怎么同步呢?简单来说就是采用 CAS 服务机制,实现 CAS 服务完成多应用单点登陆 功能。
Django 默认的 Session + Cookie 的登陆机制
![699682b41f5cb91325e7278faad2cbea.png](https://i-blog.csdnimg.cn/blog_migrate/52e235ddf389e9a1f75b98469de9fcad.png)
在了解 CAS 单点登陆之前,先来回顾一下 Django 默认的 Session + Cookie 的登陆机制:
浏览器发送登陆请求 至 Django 服务
Django 服务接收到 浏览器发送过来的请求之后,则创建 CSRFToken 以及 相关用户信息,存储到 Session 中,并且返回浏览器 Set-Cookie 的信息,通知浏览器设置相关 Cookie
浏览器再次发送请求 至 Django 服务,则会携带前面设置的 Cookie 信息
Django 服务接收到 浏览器发送过来的请求之后,发现携带了 CSRFToken 以及 记录用户信息的 sessionID,根据 sessionID 查询服务器上的 session 数据。
下面再来看看 CAS 的单点登陆机制。
CAS 的 (Single Sign-On)SSO单点登陆机制
首先先不看 CAS 的一堆概念,我们直接上请求时序图,了解请求 CAS 对于服务登陆认证的过程先。
CAS 登陆服务请求时序图
从上面的时序图来看,可以清晰知道 CAS 服务就是用来统一管理 APP 服务登陆认证的 独立服务。在时序图我写了 16 个处理步骤,在这16 个处理步骤中,可以知道,APP 服务 与 CAS 服务验证登陆是否通过是基于 服务票据 ST 来确认的。
基本认证过程简略如下:
前端访问 APP 服务的一个页面, 此时未携带相关登陆参数。
后端发现该请求未登陆,则返回前端 302 ,并 重定向到 CAS 服务器的登录页面,并携带当前用户访问的网页链接
在CAS 服务器上,用户填写登录信息,浏览器发送请求到 CAS 服务器进行认证
CAS 服务 认证通过,将本次登录保存到会话,返回 服务票据 ST 并 重定向 浏览器至 APP 服务
APP服务接收前端重定向请求过来路径 以及 服务票据 ST ,APP服务 再将 服务票据 ST 请求至 CAS 服务,验证 ST。验证通过,则创建该用户给登陆成功的 session 数据;反之,返回 前端 302, 重定向至 CAS 登陆页面。
APP 服务验证 ST 通过之后,返回 前端 登陆页面的 页面内容。
在清楚了 CAS 登陆服务请求的机制之后,我们来开始搭设服务,搭设一个完整的 CAS 服务。
CAS 示例服务
![f378ee3659f876c3d97479f13c71ebc2.png](https://i-blog.csdnimg.cn/blog_migrate/8f555b8aac085c99536748d956317bbd.png)
说明:本次示例服务代码分别创建一个 CAS 服务端的 项目,再创建一个 CAS 客户端的 项目,通过两个项目来实现完整的 CAS 服务登陆机制。
相关使用库的 Github 地址
https://github.com/jbittel/django-mama-cas
https://django-mama-cas.readthedocs.io/en/latest/
https://github.com/django-cas-ng/django-cas-ng
CAS 服务端项目
安装Django
$ pip install Django==2.1.7
因为目前线上运行的是 2.1.7 的版本,还没有改用 3.x 系列版本,所以本次使用 2.1.7 的版本进行演示。
创建Django项目
$ django-admin startproject django_cas_server .
![cb3d2c2c4f6bdd0d40238cc255ed5e54.png](https://i-blog.csdnimg.cn/blog_migrate/829fbde8f267deba6f5ab64bfcfda8d2.png)
测试启动Django项目
$ python manage.py runserver
![f7b7c132871c8e291c4caffc54d49b23.png](https://i-blog.csdnimg.cn/blog_migrate/c9f0018acac0a3878402e72f526299a1.png)
访问页面如下:
![bd6edc21bdac7d649468f9bf6ca44524.png](https://i-blog.csdnimg.cn/blog_migrate/b4a48275d5ef41da0d60036f74aad1fa.png)
停止服务,开始安装 django-mama-cas 库。
安装 django-mama-cas
$ pip install django-mama-cas
配置 settings,安装 mama-cas 应用
INSTALLED_APPS = [
'mama_cas', # 安装 mama_cas 应用
...
]
![3e662da2dbef7dc35e43fd6b2a1065ca.png](https://i-blog.csdnimg.cn/blog_migrate/a99fdc953a9b8732969e64e857cce4ae.png)
配置 url,设置访问 cas 服务的路由
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('cas/', include('mama_cas.urls')), # 导入mama_cas应用的urls.py
path('admin/', admin.site.urls),
]
![6beaf4f24ef1175a0e469d06e40da63a.png](https://i-blog.csdnimg.cn/blog_migrate/cf3a3e0c021a86ec08f057d044afe775.png)
在settings 配置 CAS 回调:
官网示例配置:
MAMA_CAS_SERVICES = [
{
'SERVICE': '^https://[^\.]+\.example\.com',
'CALLBACKS': [
'mama_cas.callbacks.user_name_attributes',
],
'LOGOUT_ALLOW': True,
'LOGOUT_URL': 'https://www.example.com/logout',
'PROXY_ALLOW': True,
'PROXY_PATTERN': '^https://proxy\.example\.com',
}
]
本次项目配置:
# 配置CAS
MAMA_CAS_SERVICES = [
{
# 必填项,客户端允许访问的域名
'SERVICE': 'http://127.0.0.1:8000',
# 回调模式,具体参考官方文档
'CALLBACKS': [
'mama_cas.callbacks.user_model_attributes',
],
},
]
![9796b768ae5c3e89ecbe5d15f56990ac.png](https://i-blog.csdnimg.cn/blog_migrate/88593731315400b4c5dcadf219d0d3ac.png)
初始化表
$ python manage.py migrate
启动服务
$ python manage.py runserver 0.0.0.0:3000
在这里我不占用 8000 端口号,开启为 3000 端口号作为 cas 服务。
访问CAS登陆页面
访问 http://127.0.0.1:3000/cas/login
![a6d78ee0cf5556d6291aad1419076d6c.png](https://i-blog.csdnimg.cn/blog_migrate/5f253a7746e31cfa3fc897dcc2184f28.png)
那么账号、密码应该填写什么呢?
其实这个取决于Django的 User 表已经存储注册以及激活了的用户。在这里,我们就创建一个 admin 的 超级用户,作为 CAS 的用户。
创建超级用户
$ python manage.py createsuperuser
Username (leave blank to use 'lijw'): casuser01
Email address:
Password:
Password (again):
This password is too short. It must contain at least 8 characters.
This password is too common.
This password is entirely numeric.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
登陆CAS服务
![8d8f10f9b930ed88d354856a2d506589.png](https://i-blog.csdnimg.cn/blog_migrate/1ed0612077014995625c3fd6334258a0.png)
提示已经登陆成功,要注意,这里没有其他配置,所以不会跳至其他的页面。只是在上面提示已经登陆成功!
CAS 的 测试用户:casuser01 密码:123456
如果登陆失败,则会提示如下:
![9375aa1f3df39cbaecb45f1391aa2e97.png](https://i-blog.csdnimg.cn/blog_migrate/f6de9c9b08fb8cdae51608d263c7caa3.png)
CAS 客户端项目
下面首先写一个项目,然后再接入 CAS 服务。
准备好客户端项目
首先准备好一个简单的客户端项目来进行演示,首先具备以下三个视图功能:
- 注册:用来新增用户
- 登陆:登陆项目新增的用户
- 首页:用来演示登陆成功之后的视图页面。
注册页面
http://127.0.0.1:8000/register
![1e4cbc8b4caace50bfda4c84c3b11ab1.png](https://i-blog.csdnimg.cn/blog_migrate/7f53b6832e177e5a5910e93cfca5ebce.png)
这个页面我只实现了最基础填写信息,然后点击注册按钮进行注册的功能,注册成功的话则自动跳转至登陆页面。
登陆页面
http://127.0.0.1:8000/login
![f82466534169c7213661f5a552a1259a.png](https://i-blog.csdnimg.cn/blog_migrate/c85e16a393aa0a359405e96107d7ce43.png)
在登陆页面,我提供了填写用户、密码以及验证码,然后点击登录按钮的功能。
这里我自己注册的一个 测试用户为: testuser01 密码:123456
要注意:这个用户是在这个项目中注册的数据,后续对接 CAS ,要用的是 CAS 项目的用户。
”
登陆成功之后,则跳转至 index 页面如下:
![9fb323d1ab4ee08ffb1dfd2c3243a5be.png](https://i-blog.csdnimg.cn/blog_migrate/900f6108d2df9f5cb00d2c2be5af8954.png)
安装 CAS 的 Client 库
在 python 中对于 cas 的 client 客户端功能有不少开源库。例如:
python-cas
:https://github.com/python-cas/python-casdjango-cas-ng
: https://github.com/django-cas-ng/django-cas-ng
因为我的项目采用的是 django 框架,所以安装 django-cas-ng
即可。
django-cas-ng 的安装文档:https://djangocas.dev/docs/latest/install.html
![b9cb67d86bc14ebd7f901b30c2172265.png](https://i-blog.csdnimg.cn/blog_migrate/e13c3f61ee70a2bb2c429668125499e8.png)
使用 pip 安装:
pip install django-cas-ng
配置项目使用 CAS 的客户端
在项目的配置文件 settings.py
添加以下配置。
参考官网的配置文档:https://djangocas.dev/docs/latest/configuration.html
![e4c75deed72384c86056af7774db165b.png](https://i-blog.csdnimg.cn/blog_migrate/f445ef79735783bdedb8c1c1052d46cc.png)
配置INSTALLED_APPS
, 安装CAS应用
INSTALLED_APPS = [
'user.apps.UserConfig', # 注册user应用
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_cas_ng', # 安装cas客户端应用
]
配置 MIDDLEWARE_CLASSES
,设置CAS客户端的中间件类
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django_cas_ng.middleware.CASMiddleware', # 设置cas客户端的中间件类
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
配置AUTHENTICATION_BACKENDS
,指定认证授权的后端
# 指定授权认证的后端
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'django_cas_ng.backends.CASBackend',
)
配置准备接入的 CAS 服务地址和版本,添加几个对应的配置:
# CAS 服务的访问地址
CAS_SERVER_URL = 'http://127.0.0.1:3000/cas/'
# CAS 版本
CAS_VERSION = '3'
# 存入所有 CAS 服务端返回的 User 数据。
CAS_APPLY_ATTRIBUTES_TO_USER = True
配置 CAS客户端 访问 CAS服务的视图页面 URL
官网的示例配置:
# Django 2.0+
from django.urls import path
import django_cas_ng.views
urlpatterns = [
# ...
path('accounts/login', django_cas_ng.views.LoginView.as_view(), name='cas_ng_login'),
path('accounts/logout', django_cas_ng.views.LogoutView.as_view(), name='cas_ng_logout'),
]
配置项目的路由 urls.py
如下:
from django.contrib import admin
from django.urls import path, include
import django_cas_ng.views # 导入cas的登陆视图
urlpatterns = [
# path('user/', include('user.urls')), # 导入user应用的urls.py
path('', include('user.urls')), # 导入user应用的urls.py
path('cas/login', django_cas_ng.views.LoginView.as_view(), name='cas_ng_login'), # 访问cas服务的登陆
path('cas/logout', django_cas_ng.views.LogoutView.as_view(), name='cas_ng_logout'), # 访问cas服务的登出
path('admin/', admin.site.urls),
]
说明:也就是说配置了这两个路径之后,具体操作过程如下:
访问客户端服务:http://127.0.0.1:8000/cas/login 判断如果未登陆服务,则自动重定向至 后台配置的 CAS 服务 http://127.0.0.1:3000/cas/login ,然后在 cas 服务器上登陆成功之后,重新重定向回客户端服务。
访问客户端服务:http://127.0.0.1:8000/cas/logout,则自动重定向至 后台配置的 CAS 服务 http://127.0.0.1:3000/cas/logout,则注销退出用户。
初始化 django_cas_ng
的相关数据表
You have 1 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): django_cas_ng.
Run 'python manage.py migrate' to apply them.
$ python manage.py migrate
启动客户端的服务
$ python manage.py runserver
测试客户端访问 CAS 服务
- 1.访问 http://127.0.0.1:8000/cas/login ,登陆用户
![472a93b0f541d3b17eff4c44550eab45.png](https://i-blog.csdnimg.cn/blog_migrate/7756ef57defe6a76c0b90419ee75a68c.png)
自动重定向至 CAS 服务如下:
![ed67188959051f0c8e133749edb1cb4b.png](https://i-blog.csdnimg.cn/blog_migrate/9a4611ffa2a3ada66f4191f20414ae45.png)
登陆成功之后,返回客户端的服务如下:
![31a4b1776144f746ddae0bca2f98235d.png](https://i-blog.csdnimg.cn/blog_migrate/70e7f0be0b91f5c649603b67fdf8b50a.png)
- 2.访问 http://127.0.0.1:8000/cas/logout 退出登陆状态
访问之后,自动重定向至未登录状态:
![938d21e6f5fa88aa8d10cb822a77891b.png](https://i-blog.csdnimg.cn/blog_migrate/116d027cede8f7d74d3d4f5cdeebba35.png)
总结
- 1.成功访问CAS服务,登陆用户之后,通过配置,可以自动将用户同步在客户端项目的用户数据中
通过在 settings.py
配置自动同步用户数据:
# 存入所有 CAS 服务端返回的 User 数据。
CAS_APPLY_ATTRIBUTES_TO_USER = True
登陆成功之后,可以查询到登陆成功的用户数据,如下:
![5da52b02ecb835c932a041dc1c03de0f.png](https://i-blog.csdnimg.cn/blog_migrate/a2e8fd3ecad27ecc163cdb35322f9d39.png)
- 2.同步CAS的用户的其他字段根据默认值设置,例如:角色按照默认设置
首先确认一下,我定义用户模型类的角色字段默认值,如下:
![ccad16e161201bd73269fc92a0676389.png](https://i-blog.csdnimg.cn/blog_migrate/148b6313cdbbaa9a38e144691ca7b40e.png)
查询CAS同步用户 的 角色数据:
In [13]: User.objects.get(username="casuser01").role
Out[13]: 0
In [14]: User.objects.get(username="casuser01").get_role_display()
Out[14]: '组员'
- 3.可以保留两个登陆页面
因为 客户端项目的登陆 和 CAS服务的登陆 是通过不同的 url 访问的,并且都可以设置登陆的状态。
也就是说,我可以在一个页面中设置不同的登陆访问,如下:
![9df13d80125a709ae945f0519be435bd.png](https://i-blog.csdnimg.cn/blog_migrate/af25ab31bad6f296d7b06b28316986cf.png)
点击CAS登陆,显示如下:
![b311fde182c2324df3a87f558778c57d.png](https://i-blog.csdnimg.cn/blog_migrate/1d015622d0d474a5c27a6a535df51da0.png)
![234492792b2116c2cc487d6380110a2f.png](https://i-blog.csdnimg.cn/blog_migrate/02355698e076e10c052c1816cc4774fe.png)
- 4.在项目的登陆视图,增加用户的登陆状态判断,如果已登陆,则直接重定向至首页
![0f8fdc5a31a504381f056a1799b3ac6a.png](https://i-blog.csdnimg.cn/blog_migrate/0549344a075f78f8b65d557122cb4ad4.png)
def get(self, request):
# get请求返回登录页面
# 判断用户是否已登陆
# 获取当前的用户
user = request.user # 获取当前的用户
# 判断用户是否已登陆
if user.is_authenticated: # 用户已登陆, 则跳至首页
return redirect('user:index')
# 用户未登陆,则进入登陆页
return render(request, "user/login.html")
- 5.登陆、用户数据、RBAC的方案策略
从上面的尝试过程中,可以确认 客户端项目 是可以保留 两种登陆用户的 方式的,并且两种方式的用户数据都会保存在 客户端项目中。
而同步过来的用户则会采用默认的角色字段,所以在配置RBAC的时候,直接根据默认角色配置可以显示的菜单即可。
其实也就是在做 RBAC 功能开发 并不受 CAS 用户的影响,CAS 用户只是增加了一种登陆的方式而已。
- 6.客户端采用 http 服务,可以配置 https 的 CAS 服务
在一开始我还担心 http 的客户端服务能否 对接 https 的CAS 服务,其实是可以的。
演示项目的仓库地址
CAS 服务端演示项目:https://gitee.com/kubernete/django_cas_server
CAS 客户端演示项目:https://gitee.com/kubernete/django_cas_client