SSO简介
单点登录(Single Sign On)功能是一个非常常用的功能,尤其是我们在多个系统之间需要登录同步的时候,例如我们在登录QQ空间后,再去QQ的其他网站,都是默认登录的状态,这就是单点登录。
单点登录有很多种实现方法,这里介绍一个通过共享session的实现方法。实现共享session要做的就是要让多个不同应用共用同一个session,但是session默认的是每个应用一个独立的session和cookie的,所以这里要对session的存储进行配置。
除了默认的session存储,我也可以设置让session存储在文件、缓存或者数据库中。
如果我们让session存储在一个固定位置或者数据库中,然后我们设置各个应用cookie的domain为父域地址即可实现各个cookie的相同,从而时候各个cookie中存储的sessionID一致。
搭建测试环境
下面我们来创建两个空的Django项目来进行演示,SSO1和SSO2,这里采用pycharm直接创建两个Django项目,也可以在命令行中使用django-admin startproject sso
来创建,其中sso
是创建的项目名称。这里也可以使用两个完全相同的项目,在不同地址启动,但是为了演示效果,这里创建了2个。
创建好两个项目后,我们要给项目写一个模拟的登录,注销的功能。
在templates
文件夹下创建文件login.html
文件。这里直接使用之前写过的登录页面的代码,样式就不加了,在SSO1和SSO2中都加入login.html
,具体代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div class="login_content"> <div class="page-header" id="page_header"> <h1>登录<small>Login</small></h1> </div> <div id="login_form"> <form method="post"> <div class="form-group"> <label for="exampleInputEmail1">Email address</label> <input type="input" class="form-control" name="usr" id="exampleInputEmail1" placeholder="username"> </div> <div class="form-group"> <label for="exampleInputPassword1">密码</label> <input type="password" class="form-control" name="password" id="exampleInputPassword1" placeholder="密码"> </div> <div id="login_butt"> <button type="submit" class="btn btn-default">登录</button> <button type="button" class="btn btn-default" onclick="">注册</button> </div> </form> </div> </div> </body> </html>
创建文件后的文件目录为:(SSO2项目一样)
. ├── SSO1 │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ ├── view.py │ └── wsgi.py ├── manage.py ├── templates │ └── login.html └── venv ├── bin ├── include ├── lib └── pyvenv.cfg
插入一个小BUG
macbook运行环境,pycharm创建的Django应用有时候初始化有个bug,缺少os库,会报错:
Traceback (most recent call last): File "/Users/qiguan/Documents/develop_files/python_files/SSO1/manage.py", line 22, in <module> main() File "/Users/qiguan/Documents/develop_files/python_files/SSO1/manage.py", line 18, in main execute_from_command_line(sys.argv) File "/Users/qiguan/Documents/develop_files/python_files/SSO1/venv/lib/python3.7/site-packages/django/core/management/__init__.py", line 401, in execute_from_command_line utility.execute() File "/Users/qiguan/Documents/develop_files/python_files/SSO1/venv/lib/python3.7/site-packages/django/core/management/__init__.py", line 345, in execute settings.INSTALLED_APPS File "/Users/qiguan/Documents/develop_files/python_files/SSO1/venv/lib/python3.7/site-packages/django/conf/__init__.py", line 82, in __getattr__ self._setup(name) File "/Users/qiguan/Documents/develop_files/python_files/SSO1/venv/lib/python3.7/site-packages/django/conf/__init__.py", line 69, in _setup self._wrapped = Settings(settings_module) File "/Users/qiguan/Documents/develop_files/python_files/SSO1/venv/lib/python3.7/site-packages/django/conf/__init__.py", line 170, in __init__ mod = importlib.import_module(self.SETTINGS_MODULE) File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/importlib/__init__.py", line 127, in import_module return _bootstrap._gcd_import(name[level:], package, level) File "<frozen importlib._bootstrap>", line 1006, in _gcd_import File "<frozen importlib._bootstrap>", line 983, in _find_and_load File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 677, in _load_unlocked File "<frozen importlib._bootstrap_external>", line 728, in exec_module File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed File "/Users/qiguan/Documents/develop_files/python_files/SSO1/SSO1/settings.py", line 57, in <module> 'DIRS': [os.path.join(BASE_DIR, 'templates')] NameError: name 'os' is not defined
如果有这个报错的话,在setting.py
中导入os即可:import os
然后我们在两个项目的view.py中写入登录和注销函数:
from django.http import HttpResponse from django.shortcuts import render, redirect def login(request): if request.method == 'GET': if 'usr' in request.session: # 如果session中已有信息,则显示 usr = request.session['usr'] password = request.session['password'] return HttpResponse("usr:{},password:{},sessionid:{},cookie:{}".format(usr,password,request.session.session_key,request.COOKIES)) return render(request,'login.html') if request.method == 'POST': usr = request.POST['usr'] password = request.POST['password'] request.session['usr'] = usr request.session['password'] = password return HttpResponse( "usr:{},password:{},sessionid:{},cookie:{}".format(usr, password, request.session.session_key, request.COOKIES)) def logout(request): request.session.clear() return redirect('/login')
在url.py
中添加路由信息:
"""SSO1 URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/3.1/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin from django.urls import path from . import view urlpatterns = [ path('admin/', admin.site.urls), path('login/',view.login), path('logout/',view.logout), ]
Django默认配置了csrf,需要将它注释掉,在settings.py
文件中搜csrf,然后注释掉。
修改后的settings.py
文件为:
""" Django settings for SSO1 project. Generated by 'django-admin startproject' using Django 3.1.7. For more information on this file, see https://docs.djangoproject.com/en/3.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.1/ref/settings/ """ from pathlib import Path import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'o=blc^vzeb1&g*b!si(wtxe44_=i5cv(3jqm2*u2u&7vgj%&=%' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', # 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'SSO1.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')] , 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'SSO1.wsgi.application' # Database # https://docs.djangoproject.com/en/3.1/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } } # Password validation # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/3.1/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.1/howto/static-files/ STATIC_URL = '/static/'
然后分别为两个项目做数据库迁移,创建一些Django项目的基础库:
python3 manage.py migrate
两个项目都是同样的配置,这样我们目前两个测试的项目就搭建好了,然后我们分别启动他们在不同的端口。这里我们就直接手动启动了,分别启动在5000和6000端口。
python3 manage.py runserver 127.0.0.1:5000
python3 manage.py runserver 127.0.0.1:7000
启动两个项目:
现在我们分别在浏览器中打开http://127.0.0.1:5000/login/
和http://127.0.0.1:7000/login/
,显示的页面都是登录页面,显示如下:
这时我们在http://127.0.0.1:5000/login/
随意输入账户密码点击登录,显示:
usr:123,password:123,sessionid:None,cookie:{'csrftoken': '8YPzJbY03sHJUZH6kdFZzr9TkDtdVTKflgDDeIn0wgGC6cAeudcrkXLyIxXBEnzG'}
此时我们进入http://127.0.0.1:7000/login/
,发现这个应用中,显示的还是之前的页面,登录没有同步。下面我们来实现我们的SSO,这里的实现方法非常的简单,这里提供2中实现方法:
-
将session固定存储在同一个文件中,
-
将session存储在Redis中
将session存储在同一个文件中实现SSO
我们在SSO2文件下创建了一个session文件夹,这个文件夹位置任意,写绝对路径即可。
然后我们在两个项目的settings.py
中对cookie和session进行配置
# 设置cookie的domain为父域domain, # 如果是使用域名,以百度为例,主域名为`www.baidu.com`,旗下各个应用为:'asd.baidu.com' # 则这里设置为:`.baidu.com` SESSION_COOKIE_DOMAIN = '127.0.0.1' # 设置session存储在文件中 SESSION_ENGINE = 'django.contrib.sessions.backends.file' # 设置存储位置,这里设为绝对路径 SESSION_FILE_PATH = '/Users/qiguan/Documents/develop_files/python_files/SSO2/session'
注意一下,这里配置的都是一样的,但是如果两个项目名称不一样的话,是不能直接将完整的settings.py
直接复制到另一个的,因为里面有一些项目的配置,例如ROOT_URLCONF = 'SSO1.urls'
、WSGI_APPLICATION = 'SSO1.wsgi.application'
这些前面的都是项目名,需要主要区分。
此时我们在打开http://127.0.0.1:5000/login/
,输入账号密码,此页面显示:
usr:123,password:123,sessionid:2bs2nx2iq879epxu7au7o1zq63o095v7,cookie:{'sessionid': '2bs2nx2iq879epxu7au7o1zq63o095v7', 'csrftoken': '8YPzJbY03sHJUZH6kdFZzr9TkDtdVTKflgDDeIn0wgGC6cAeudcrkXLyIxXBEnzG'}
此时我们在打开http://127.0.0.1:7000/login/
,我们直接访问,而不用登录,发现显示同样的内容,即我们使用的是同样的内容,实现了SSO。
使用Redis实现SSO
使用文件系统上实现共享session在小并发系统上不会出现问题,但是并发量大的话,会出现一些问题,所以我们这里再介绍一下使用Redis的实现。
需要自行安装Redis,并且在两个项目使用的Python中安装Django-redis:pip3 install django-redis
在做好这些之后,修改settings.py文件,将使用文件存储session的配置注释掉,修改为:
# # 设置session存储在文件中 # SESSION_ENGINE = 'django.contrib.sessions.backends.file' # # 设置存储位置,这里设为绝对路径 # SESSION_FILE_PATH = '/Users/qiguan/Documents/develop_files/python_files/SSO2/session' # 使用Redis存储session CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "CONNECTION_POOL_KWARGS": {"max_connections": 100} # "PASSWORD": "123", } } } SESSION_ENGINE = 'django.contrib.sessions.backends.cache' SESSION_CACHE_ALIAS = 'default' SESSION_COOKIE_AGE = 60 * 5
此时我们再来测试一下两个应用,这时我们先访问一下logout,将session清空,然后访问:http://127.0.0.1:5000/login/
,输入账户密码后显示:
usr:123,password:123,sessionid:None,cookie:{'csrftoken': '8YPzJbY03sHJUZH6kdFZzr9TkDtdVTKflgDDeIn0wgGC6cAeudcrkXLyIxXBEnzG'}
此时我们访问http://127.0.0.1:7000/login/
(不登录),显示同样的usr和password信息。
此时我们的SSO也可以正常实现。
好了,本文就先到这里,大家如有需要,可以根据具体的业务进行实现,这里就不赘述了。等以后有空再写一些Django相关的开发博客。