75. Django 集成 CAS 实现 SSO 单点登陆

2020-11-17 12:44:02 浏览数 (1)

需求

在公司平台的开发中,由于内部平台越来越多,本次要求我们开发的平台需要同步公司的 OA 账号。

那么怎么同步呢?简单来说就是采用 CAS 服务机制,实现 CAS 服务完成多应用单点登陆 功能。

Django 默认的 Session Cookie 的登陆机制

image-20200909110936463

在了解 CAS 单点登陆之前,先来回顾一下 Django 默认的 Session Cookie 的登陆机制:

  1. 浏览器发送登陆请求 至 Django 服务
  2. Django 服务接收到 浏览器发送过来的请求之后,则创建 CSRFToken 以及 相关用户信息,存储到 Session 中,并且返回浏览器 Set-Cookie 的信息,通知浏览器设置相关 Cookie
  3. 浏览器再次发送请求 至 Django 服务,则会携带前面设置的 Cookie 信息
  4. Django 服务接收到 浏览器发送过来的请求之后,发现携带了 CSRFToken 以及 记录用户信息的 sessionID,根据 sessionID 查询服务器上的 session 数据。

下面再来看看 CAS 的单点登陆机制

CAS 的 (Single Sign-On)SSO单点登陆机制

首先先不看 CAS 的一堆概念,我们直接上请求时序图,了解请求 CAS 对于服务登陆认证的过程先。

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 示例服务

image-20200909165844507

说明:本次示例服务代码分别创建一个 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

代码语言:javascript复制
$ pip install Django==2.1.7

因为目前线上运行的是 2.1.7 的版本,还没有改用 3.x 系列版本,所以本次使用 2.1.7 的版本进行演示。

创建Django项目

代码语言:javascript复制
$ django-admin startproject django_cas_server .

image-20200909173638238

测试启动Django项目

代码语言:javascript复制
$ python manage.py runserver

image-20200909173737679

访问页面如下:

image-20200909173752193

停止服务,开始安装 django-mama-cas 库。

安装 django-mama-cas

代码语言:javascript复制
$ pip install django-mama-cas

配置 settings,安装 mama-cas 应用

代码语言:javascript复制
INSTALLED_APPS = [
    'mama_cas', # 安装 mama_cas 应用
	...
]

image-20200909171302285

配置 url,设置访问 cas 服务的路由

代码语言:javascript复制
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),
]

image-20200909171604589

在settings 配置 CAS 回调:

官网示例配置:

代码语言:javascript复制
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',
    }
]

本次项目配置:

代码语言:javascript复制
# 配置CAS
MAMA_CAS_SERVICES = [
    {
        # 必填项,客户端允许访问的域名
        'SERVICE': 'http://127.0.0.1:8000',
        # 回调模式,具体参考官方文档
        'CALLBACKS': [
            'mama_cas.callbacks.user_model_attributes',
        ],
    },
]

image-20200909200755361

初始化表

代码语言:javascript复制
$ python manage.py migrate

启动服务

代码语言:javascript复制
$ python manage.py runserver 0.0.0.0:3000

在这里我不占用 8000 端口号,开启为 3000 端口号作为 cas 服务。

访问CAS登陆页面

访问 http://127.0.0.1:3000/cas/login

image-20200909201733073

那么账号、密码应该填写什么呢?

其实这个取决于Django的 User 表已经存储注册以及激活了的用户。在这里,我们就创建一个 admin 的 超级用户,作为 CAS 的用户。

创建超级用户

代码语言:javascript复制
$ 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服务

image-20200909202310620

提示已经登陆成功,要注意,这里没有其他配置,所以不会跳至其他的页面。只是在上面提示已经登陆成功!

CAS 的 测试用户:casuser01 密码:123456

如果登陆失败,则会提示如下:

image-20200914110114011

CAS 客户端项目

下面首先写一个项目,然后再接入 CAS 服务。

准备好客户端项目

首先准备好一个简单的客户端项目来进行演示,首先具备以下三个视图功能:

  • 注册:用来新增用户
  • 登陆:登陆项目新增的用户
  • 首页:用来演示登陆成功之后的视图页面。
注册页面

http://127.0.0.1:8000/register

image-20200914135239661

这个页面我只实现了最基础填写信息,然后点击注册按钮进行注册的功能,注册成功的话则自动跳转至登陆页面。

登陆页面

http://127.0.0.1:8000/login

image-20200914135857849

在登陆页面,我提供了填写用户、密码以及验证码,然后点击登录按钮的功能。

这里我自己注册的一个 测试用户为: testuser01 密码:123456

要注意:这个用户是在这个项目中注册的数据,后续对接 CAS ,要用的是 CAS 项目的用户。 ”

登陆成功之后,则跳转至 index 页面如下:

image-20200914135935427

安装 CAS 的 Client 库

在 python 中对于 cas 的 client 客户端功能有不少开源库。例如:

  • python-cas:https://github.com/python-cas/python-cas
  • django-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

image-20200914141205269

使用 pip 安装:

代码语言:javascript复制
pip install django-cas-ng

配置项目使用 CAS 的客户端

在项目的配置文件 settings.py 添加以下配置。

参考官网的配置文档:https://djangocas.dev/docs/latest/configuration.html

image-20200914141452552

配置INSTALLED_APPS, 安装CAS应用
代码语言:javascript复制
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客户端的中间件类
代码语言:javascript复制
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 ,指定认证授权的后端
代码语言:javascript复制
# 指定授权认证的后端
AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',
    'django_cas_ng.backends.CASBackend',
)
配置准备接入的 CAS 服务地址和版本,添加几个对应的配置:
代码语言:javascript复制
# 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

官网的示例配置:

代码语言:javascript复制
# 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 如下:

代码语言:javascript复制
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 的相关数据表

代码语言:javascript复制
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

启动客户端的服务

代码语言:javascript复制
$ python manage.py runserver

测试客户端访问 CAS 服务

  • 1.访问 http://127.0.0.1:8000/cas/login ,登陆用户

image-20200914162201532

自动重定向至 CAS 服务如下:

image-20200914162617249

登陆成功之后,返回客户端的服务如下:

image-20200914162641249

  • 2.访问 http://127.0.0.1:8000/cas/logout 退出登陆状态

访问之后,自动重定向至未登录状态:

image-20200914162842100

总结

  • 1.成功访问CAS服务,登陆用户之后,通过配置,可以自动将用户同步在客户端项目的用户数据中

通过在 settings.py 配置自动同步用户数据:

代码语言:javascript复制
# 存入所有 CAS 服务端返回的 User 数据。
CAS_APPLY_ATTRIBUTES_TO_USER = True

登陆成功之后,可以查询到登陆成功的用户数据,如下:

image-20200914163317309

  • 2.同步CAS的用户的其他字段根据默认值设置,例如:角色按照默认设置

首先确认一下,我定义用户模型类的角色字段默认值,如下:

image-20200914163639274

查询CAS同步用户 的 角色数据:

代码语言:javascript复制
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 访问的,并且都可以设置登陆的状态。

也就是说,我可以在一个页面中设置不同的登陆访问,如下:

image-20200914165552564

点击CAS登陆,显示如下:

image-20200914165617366

image-20200914165737365

  • 4.在项目的登陆视图,增加用户的登陆状态判断,如果已登陆,则直接重定向至首页

image-20200914170313767

代码语言:javascript复制
    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

0 人点赞