首頁>科技>

導讀

使用者管理可以說是運維工作最基礎的部分,隨著企業的發展,我們面對的將不僅僅是一個或多個系統,使用者也可能成倍增長。此時統一使用者認證LDAP或許是你的一種解決方案。

藍鯨作為騰訊互動娛樂事業群(Interactive Entertainment Group,簡稱 IEG)自研自用的一套用於構建企業研發運營一體化體系的 PaaS 開發框架,提供了 aPaaS(DevOps 流水線、執行環境託管、前後臺框架)和 iPaaS(持續整合、CMDB、作業平臺、容器管理、資料平臺、AI 等原子平臺)等模組,幫助企業技術人員快速構建基礎運營 PaaS。

當二者結合在一起會給你工作帶來意想不到的收穫!

藍鯨官方文件社群版: “藍鯨登入接入企業內部登入”中已經通過接入google登入的例子進行說明;但是公司內部只有ldap作為內部服務的統一認證,並不提供相關登入API,難道我們還要再自己搭建API?

以上恐怕也是很多中小企業的現狀,這種情況下該如何接入企業內部ldap呢?

原始碼分析

下面我們來分析下藍鯨paas平臺統一登入服務基本函式介面來看下登入流程,供我們參考

1.藍鯨統一登入提供的基本函式

from bkaccount.accounts import Account

從以上python的模組匯入來看,藍鯨的登入跳轉函式主要由Account類實現,其中登入頁面和登入動作的功能主要由login實現:

def login(self, request, template_name='login/login.html',              authentication_form=AuthenticationForm,              current_app=None, extra_context=None):        """        登入頁面和登入動作        """        redirect_field_name = self.REDIRECT_FIELD_NAME        redirect_to = request.POST.get(redirect_field_name,                                       request.GET.get(redirect_field_name, ''))        app_id = request.POST.get('app_id', request.GET.get('app_id', ''))        if request.method == 'POST':            form = authentication_form(request, data=request.POST)            if form.is_valid():                return self.login_success_response(request, form, redirect_to, app_id)        else:            form = authentication_form(request)        current_site = get_current_site(request)        context = {            'form': form,            redirect_field_name: redirect_to,            'site': current_site,            'site_name': current_site.name,            'app_id': app_id,        }        if extra_context is not None:            context.update(extra_context)        if current_app is not None:            request.current_app = current_app        response = TemplateResponse(request, template_name, context)        response = self.set_bk_token_invalid(request, response)        return response

其中當登入頁面輸入使用者名稱、密碼登入會發出POST請求,程式碼段如下:

if request.method == 'POST':            form = authentication_form(request, data=request.POST)            if form.is_valid():                return self.login_success_response(request, form, redirect_to, app_id)        else:            form = authentication_form(request)

authentication_form處理接收提交到使用者名稱和密碼,引用自:

authentication_form=AuthenticationForm

from django.contrib.auth.forms import AuthenticationForm

其中AuthenticationForm是一個表單。

2.登入表單認證

AuthenticationForm是一個表單,定義如下:

class AuthenticationForm(forms.Form):    """    Base class for authenticating users. Extend this to get a form that accepts    username/password logins.    """    username = forms.CharField(max_length=254)    password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)    error_messages = {        'invalid_login': _("Please enter a correct %(username)s and password. "                           "Note that both fields may be case-sensitive."),        'inactive': _("This account is inactive."),    }    def __init__(self, request=None, *args, **kwargs):        """        The 'request' parameter is set for custom auth use by subclasses.        The form data comes in via the standard 'data' kwarg.        """        self.request = request        self.user_cache = None        super(AuthenticationForm, self).__init__(*args, **kwargs)        # Set the label for the "username" field.        UserModel = get_user_model()        self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)        if self.fields['username'].label is None:            self.fields['username'].label = capfirst(self.username_field.verbose_name)    def clean(self):        username = self.cleaned_data.get('username')        password = self.cleaned_data.get('password')        if username and password:            self.user_cache = authenticate(username=username,                                           password=password)            if self.user_cache is None:                raise forms.ValidationError(                    self.error_messages['invalid_login'],                    code='invalid_login',                    params={'username': self.username_field.verbose_name},                )            else:                self.confirm_login_allowed(self.user_cache)        return self.cleaned_data    def confirm_login_allowed(self, user):        """        Controls whether the given User may log in. This is a policy setting,        independent of end-user authentication. This default behavior is to        allow login by active users, and reject login by inactive users.        If the given user cannot log in, this method should raise a        ``forms.ValidationError``.        If the given user may log in, this method should return None.        """        if not user.is_active:            raise forms.ValidationError(                self.error_messages['inactive'],                code='inactive',            )    def get_user_id(self):        if self.user_cache:            return self.user_cache.id        return None    def get_user(self):        return self.user_cache

django的表單功能我們可以知道,獲取到前端request.post的資料需要經表單進行clean驗證,最終返回cleaned_data字典,程式碼段如下:

def clean(self):        username = self.cleaned_data.get('username')        password = self.cleaned_data.get('password')        if username and password:            self.user_cache = authenticate(username=username,                                           password=password)            if self.user_cache is None:                raise forms.ValidationError(                    self.error_messages['invalid_login'],                    code='invalid_login',                    params={'username': self.username_field.verbose_name},                )            else:                self.confirm_login_allowed(self.user_cache)        return self.cleaned_data

從程式碼看出,如果使用者名稱、密碼不為空,呼叫authenticate進行驗證。

引用來自:

from django.contrib.auth import authenticate

而authenticate正是自定義接入企業登入模組需要重寫的函式,也就和“藍鯨登入接入企業內部登入”中的說明對上了。

3.登入總結

公司在沒有登入API的情況下,我們最終可以通過重寫AuthenticationForm表單的clean方法來進行自主本地認證。

企業接入

1.登入功能描述

1.普通使用者登入先經ldap認證

a.若ldap中存在,藍鯨中不存在,則建立新使用者並將其設定為普通使用者;

b.若ldap中不存在,則進入藍鯨預設的頁面跳轉動作;

2.admin使用者登入跳過ldap認證,直接走藍鯨認證;

思考:對於ldap無法連線或連線失敗的狀況,可以跳過ldap認證,走藍鯨認證。這個功能在本次開發中沒有完成,大家可自行實現。

2.目錄結構

ee_login/├── enterprise_ldap       ##自定義登入模組目錄│   ├── backends.py       ##驗證使用者合法性 │   ├── __init__.py│   ├── ldap.py           ##接入ldap並獲取使用者資訊│   ├── utils.py          ##自定義表單,整合AuthenticationForm,重寫clean方法│   ├── views.py          ##登入處理邏輯函式├── __init__.py└── settings_login.py     ##自定義登入配置檔案

3.建立模組目錄及配置檔案

#paas所在機器#安裝ldap模組workon open_paas-loginpip install ldap3#一定要是在open_paas-login這個虛擬環境下,否則ldap會找不到#中控機cd /data/bkce/open_paas/login/ee_login#建立自定義登入模組目錄#此目錄下的py檔案可使用以下程式碼部分直接建立即可mkdir enterprise_ldap#修改配置檔案vim settings_login.py# -*- coding: utf-8 -*-# 藍鯨登入方式:bk_login# 自定義登入方式:custom_login#LOGIN_TYPE = 'bk_login'LOGIN_TYPE = 'custom_login'# 預設bk_login,無需設定其他配置############################ 自定義登入 custom_login   ############################# 配置自定義登入請求和登入回撥的響應函式, 如:CUSTOM_LOGIN_VIEW = 'ee_official_login.oauth.google.views.login'CUSTOM_LOGIN_VIEW = 'ee_login.enterprise_ldap.views.login'# 配置自定義驗證是否登入的認證函式, 如:CUSTOM_AUTHENTICATION_BACKEND = 'ee_official_login.oauth.google.backends.OauthBackend'CUSTOM_AUTHENTICATION_BACKEND = 'ee_login.enterprise_ldap.backends.ldapbackend'

4.登入請求和登入回撥函式

vim enterprise_ldap/views.py# -*- coding: utf-8 -*-    from django.http.response import HttpResponsefrom bkaccount.accounts import Accountfrom django.contrib.sites.shortcuts import get_current_sitefrom django.template.response import TemplateResponsefrom .utils import CustomLoginForm def login(request, template_name='login/login.html',              authentication_form=CustomLoginForm,              current_app=None, extra_context=None):    """    登入處理    """    account = Account()        # 獲取使用者實際請求的 URL, 目前 account.REDIRECT_FIELD_NAME = 'c_url'    redirect_to = request.GET.get(account.REDIRECT_FIELD_NAME, '')    # 獲取使用者實際訪問的藍鯨應用    app_id = request.GET.get('app_id', '')    redirect_field_name = account.REDIRECT_FIELD_NAME        if request.method == 'POST':        #通過自定義表單CustomLoginForm實現登入驗證        form = authentication_form(request, data=request.POST)        if form.is_valid():            #驗證通過跳轉            return account.login_success_response(request, form, redirect_to, app_id)    else:        form = authentication_form(request)        current_site = get_current_site(request)    context = {        'form': form,        redirect_field_name: redirect_to,        'site': current_site,        'site_name': current_site.name,        'app_id': app_id,    }    if extra_context is not None:        context.update(extra_context)    if current_app is not None:        request.current_app = current_app    response = TemplateResponse(request, template_name, context)    response = account.set_bk_token_invalid(request, response)    return response

login函式是參照藍鯨自帶的login函式,它們之間的區別就是呼叫了不同的表單,在此我們呼叫的是重寫AuthenticationForm後的表單,引用於:

from .utils import CustomLoginForm

這樣login登入就不需要走API了,在本地就可實現。

登入後的跳轉處理仍使用原來的處理,通過account呼叫跳轉函式即可。

5.自定義表單

vim enterprise_ldap/utils.py# -*- coding: utf-8 -*-from django import formsfrom django.contrib.auth.forms import AuthenticationFormfrom django.contrib.auth import authenticatefrom common.log import loggerclass CustomLoginForm(AuthenticationForm):    """    重寫AuthenticationForm類,用於自定義登入custom_login    """    def clean(self):        username = self.cleaned_data.get('username')        password = self.cleaned_data.get('password')        if username and password:            self.user_cache = authenticate(username=username,                                           password=password)            if self.user_cache is None:                raise forms.ValidationError(                    self.error_messages['invalid_login'],                    code='invalid_login',                    params={'username': self.username_field.verbose_name},                )            else:                super(CustomLoginForm, self).confirm_login_allowed(self.user_cache)        return self.cleaned_data

重寫了父類AuthenticationForm中的clean方法,因為clean方法中呼叫了authenticate進行了對使用者名稱、密碼的驗證。

6.使用者認證功能實現

vim enterprise_ldap/backends.py# -*- coding: utf-8 -*-from django.contrib.auth.backends import ModelBackendfrom .ldap import SearchLdapfrom django.contrib.auth import get_user_modelfrom bkaccount.constants import RoleCodeEnumfrom common.log import loggerclass ldapbackend(ModelBackend):    def authenticate(self, **credentials):           username = credentials.get('username')        password = credentials.get('password')                      if username and password:            logger.info("username: %s,password: %s" % (username,password))            #當登入賬號為admin時,直接在藍鯨驗證,不走ldap認證            if username == 'admin':                logger.info(u'使用者為admin,直接藍鯨驗證')                return super(ldapbackend, self).authenticate(username=username, password=password)            else:                ldapinfo = SearchLdap()                resp = ldapinfo.get_user_info(username=username, password=password)                #如果ldap中存在此使用者                if resp["result"] == "success":                    # 獲取使用者類 Model(即對應使用者表)                    user_model = get_user_model()                    try:                        user = user_model.objects.get(username=username)                    except user_model.DoesNotExist:                        # 建立 User 物件                        user = user_model.objects.create_user(username)                        # 獲取使用者資訊,只在第一次建立時設定,已經存在不更新                        chname = resp['data']['chname']                        phone = resp['data']['mobile']                        email = resp['data']['email']                        user.chname = chname                        user.phone = phone                        user.email = email                        user.save()                        # 設定新增使用者角色為普通管理員                        logger.info(u'新建使用者:%s 許可權:%s' % (chname, u'普通使用者'))                        result, message = user_model.objects.modify_user_role(username, RoleCodeEnum.STAFF)                    return user                             else:                    return None        else:            return None

使用者認證主要通過authenticate函式實現:

1.登入ldap後過濾相應的使用者cn、mail、mobile欄位,並判斷是否在藍鯨資料庫中存在,不存在則新建使用者並授予普通管理員角色;

2.登入使用者為admin,則直接藍鯨認證;

7.LDAP獲取使用者資訊

vim enterprise_ldap/backends.py# -*- coding: utf-8 -*-from ldap3 import Connection, Server, SUBTREEfrom common.log import loggerclass SearchLdap:    host = '10.90.10.123'    port = 389    ldap_base = 'dc=test,dc=cn'    def get_user_info(self, **kwargs):                username = kwargs.get("username")        password = kwargs.get("password")        ldap_user = 'cn='+username+','+self.ldap_base        try:            #與ldap建立連線            s = Server(host=self.host, port=self.port, use_ssl=False, get_info='ALL', connect_timeout=5)            #bind開啟連線            c = Connection(s, user=ldap_user, password=password, auto_bind='NONE', version=3, authentication='SIMPLE', client_strategy='SYNC', auto_referrals=True, check_names=True, read_only=True, lazy=False, raise_exceptions=False)                c.bind()            logger.info(c.result)            #認證正確-success 不正確-invalidCredentials            if c.result['description'] == 'success':                res = c.search(search_base=self.ldap_base, search_filter = "(cn="+username+")", search_scope = SUBTREE, attributes = ['cn', 'mobile', 'mail'], paged_size = 5)                if res:                    attr_dict = c.response[0]["attributes"]                    chname = attr_dict['cn'][0]                    email = attr_dict['mail'][0]                    mobile = attr_dict['mobile'][0]                               data = {                        'username': "%s" % username,                        'password': "%s" % password,                        'chname': "%s" % chname,                        'email': "%s" % email,                        'mobile' : "%s" % mobile,                    }                    logger.info(u'ldap成功匹配使用者')                    result = {                        'result': "success",                        'message':'驗證成功',                        'data':data                    }                else:                    logger.info(u'ldap無此使用者資訊')                    result = {                        'result': "null",                        'message':'result is null'                    }                #關閉連線                c.unbind()            else:                logger.info(u"使用者認證失敗")                result = {                    'result': "auth_failure",                    'message': "user auth failure"                }                        except Exception as e:            logger.info(u'ldap連接出錯: %s' % e)            result = {                'result': 'conn_error',                'message': "connect error"            }                return result

登入驗證使用者是否存在,需注意:

1.ldap使用者名稱、密碼登入是否成功一定要通過c.result的description欄位是否為success來確認,否則即使認證不成功,也能連線並過濾到資訊。此時在藍鯨登入時會出現,只要是ldap中有的賬戶,即使密碼不正確也能成功登入;

2.ldap登入時的使用者名稱一定要是“cn=test,dc=test,dc=cn”(具體格式根據實際情況調整),否則登入是不成功的,但也能正常過濾資訊;

3.ldap中的使用者一定要有cn,mail,mobile等欄位,否則賬戶即使存在登入也會不成功;

8.重啟login服務並使配置生效

/data/install/bkcec stop paas login/data/install/bkcec start paas login

9.登入檢視訪問日誌

cd /data/bkce/logs/open_paas/login_uwsgi.log   login.log
總結

通過藍鯨paas平臺的統一登入服務的原始碼解析,不僅僅是功能上的實現,更重要的是參考大廠研發程式碼的佈局、流程、規範等,給我們自身帶來的啟發。

最新評論
  • 整治雙十一購物亂象,國家再次出手!該跟這些套路說再見了
  • 迅雷下載速度那麼快,我為啥還去折騰IDM?這個功能香透了