[原創]django+ldap實現單點登錄(裝飾器和緩存)


前言

參考本系列之前的文章,我們已經搭建了ldap並且可以通過django來操作ldap了,剩下的就是下游系統的接入了,現在的應用場景,我是分了2個層次,第一層次是統一認證,保證各個系統通過ldap來維護統一的用戶名和密碼,第二層次就是sso單點登錄,即一個系統登錄,其他系統即是登錄狀態,一個系統登出,其他系統也自動登出,也就是我們登錄公司內部的N個系統,其實總共只需要登錄一次即可。
目前,django的下游系統可以接入單點,理論上,只要語言支持memcache客戶端,通過session維持登錄狀態,都可以接入,但類似jira這種商用產品,由於沒有二次開發的能力,所以只能支持到統一密碼的層面了。

代碼實現

首先在下游系統的settings.py需要進行一定的配置,其中,session要走我們的共享mc中,這樣才能獲取到sso系統傳入的session,以達到單點登錄的目的。

LOGIN_URL = "http://sa.ssotest.net/account/login/"  # 跳轉到sso系統的登錄上
PERM_DENY_URL = "http://sa.ssotest.net/403.html"  # 這個是自己加的一個配置,如果下游系統沒有sso用戶的權限,會跳轉到deny頁面上
#LOGIN_URL = "/account/login/"  # 之前走本地的配置

# ### session config BEGIN ### #
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_COOKIE_AGE = 86400  # 設置session有效期為一天,默認兩周
SESSION_COOKIE_DOMAIN = ".ssotest.net"
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': [
            'ldap1.prod.bj1.ssotest.net:11211',
            'ldap2.prod.bj1.ssotest.net:11211',
        ],
        'TIMEOUT': 60 * 15,  # 緩存默認過期時間
        'OPTIONS': {
            'MAX_ENTRIES': 3000  # 最大緩存個數
        }
    }
}
# ### session config END ### #

設定一個下游系統專用的驗證登錄狀態和登出的裝飾器,放在backend/decorators/login_auth.py以代替你可能在用的django自帶的login_required

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from SQLaudit import settings_prod as settings
from django.shortcuts import redirect
from web_sql import models
import json
from django.db import transaction
from django.core.cache import cache
from django.contrib import auth

# ########## 用於驗證用戶是否登陸的裝飾器 ##########
def login_sso(func):
    """ 如果用戶已經登陸,則執行相應的Views中的函數,否則,跳轉至 settings中設置的LOGIN_URL地址,即:  '/account/login/'"""
    def wrapper(request, *args, **kwargs):
        sso_dic = request.session.get('sso_dic', None) # 這個字典在sso系統中登錄成功后,由sso設定在request.session中
        if not sso_dic:
            login_url = '%s?back=%s' % (settings.LOGIN_URL, request.get_raw_uri())  # 把當前站點的url傳參給back,如果在sso中登錄成功會自動redirect到back
            return redirect(login_url)
        try:
            sso_dic = json.loads(sso_dic)
            # 用戶對象優先從cache緩存中取出,如沒有,從數據庫中取出,key: sso_uid_sqladmin.ssotest.net:8009_qudong
            cache_user = cache.get('sso_uid_%s_%s' % (request.get_host(), sso_dic.get('sso_username')))
            user_obj = cache_user if cache_user else \
                models.User.objects.filter(username=sso_dic.get('sso_username'),
                                           userprofile__name=sso_dic.get('sso_chinese_name'),
                                           email=sso_dic.get('sso_email')).first()
            if not user_obj:  # 如果上下游用戶信息不一致,創建或更新下游信息
                with transaction.atomic():
                    ''' 以下部分需要根據各下游系統,注冊用戶方法不同,而進行修改 '''
                    user_obj, status = models.User.objects.update_or_create(username=sso_dic.get('sso_username'),
                                                                            defaults={'email': sso_dic.get('sso_email')})
                    group_obj = models.Group.objects.get(name='開發人員')  # 新增用戶默認屬於開發人員組
                    user_obj.groups.add(group_obj)
                    models.UserProfile.objects.update_or_create(user=user_obj,
                                                                defaults={'name': sso_dic.get('sso_chinese_name')})
            if not user_obj.is_active:  # 用戶在下游系統被禁用
                raise Exception(u'用戶被禁用')
        except Exception:
            login_url = '%s?back=%s' % (settings.PERM_DENY_URL, request.get_raw_uri())
            return redirect(login_url)  # 下游系統在同步信息中出現問題,跳轉到deny頁面
        else:
            # 如用戶已同步,添加緩存,使用cache.add方法,如已有cache,不進行處理;cache.set方法會重新覆蓋cache.緩存失效時間5分鍾,期間,登錄用戶狀態不會被修改
            cache.add('sso_uid_%s_%s' % (request.get_host(), sso_dic.get('sso_username')), user_obj, 60 * 5)
            request.user = user_obj
        return func(request, *args, **kwargs)
    return wrapper


# ########## 用於用戶登出的裝飾器,主要用於session清除后,緩存的清除 ##########
def logout_sso(func):
    def wrapper(request):
        try:
            sso_dic = request.session.get('sso_dic', None)
            auth.logout(request)
            sso_dic = json.loads(sso_dic)
            cache.delete('sso_uid_%s_%s' % (request.get_host(), sso_dic.get('sso_username')))  # 除了auth.logout額外添加取出之前添加的cache動作
        except Exception:
            pass
        return func(request)
    return wrapper

順道提一下django的cache,我們的session和cache都走的settings.py的CACHES屬性,我們這里是緩存到memcache中,django cache部分的官網中文說明:http://python.usyiyi.cn/translate/django_182/topics/cache.html
django可以實現站點級的緩存,某個url的緩存,某個view的緩存,某個模板片段的緩存,某個數據的緩存(底層cache api),具體可以參考上邊的文檔,寫的很細致了,測試了一下,比如進行了view的緩存,它是把整個html包括靜態和動態的都緩存住,這樣如果我們的數據更新了,那就只能等到緩存過期了。我這里由於只是為了緩存一個數據對象(user_obj),所以只用了cache api,具體就是add,get,delete方法,注意add和set的區別,add如果已有cache將不處理,set是不管有沒有,都重新設置
如果要緩存某個view也很方便,直接在view上添加裝飾器cache_page(60 * 15) ,緩存15分鍾
調用的時候,將原來的驗證登錄的裝飾器替換成login_sso

@logout_sso
def sql_logout(request):
    return redirect(settings.LOGIN_URL)
    
@login_sso
def sql_base(request):
    user = request.user  # request.user通過login_sso已經定義,可以直接把當前登錄的user對象拿來用了
    return render(request, 'audit/sql_base.html', {})

結語

經過以上一系列的博客,大體上熟悉了整個sso+統一認證的流程,期間還是有一些不太理想的地方,比如非django系統如何更好的支持sso,因為我的方案里,sso的核心是共享session,而session存在django的request中,不知道其他語言的系統如何獲取這個request.session,另外整個單點系統只支持到同域的跨站級別,還沒考慮跨域的問題,歡迎牛人指導。

參考資料

http://python.usyiyi.cn/translate/django_182/topics/cache.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM