天天看點

藍鲸社群版5.1接入ldap認證

簡介

藍鲸社群版5.1 介入公司内部ldap認證

官方文檔社群版: 藍鲸登入接入企業内部登入中已經通過接入google登入的例子進行說明;但是公司内部隻有ldap作為内部服務的統一認證,并不提供相關登入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來自于login函數傳入的參數authentication_form=AuthenticationForm,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,也就是調用的clean方法,最終資料通過cleaned_data.get進行提取,代碼段如下:

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 進行驗證。從python子產品的導入來看:from django.contrib.auth import authenticate ,authenticate正是自定義接入企業登入子產品要重寫的函數,也就和 社群版: 藍鲸登入接入企業内部登入

中的介紹對上了。

3.登入總結

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

下面我們就來實作下藍鲸社群版5.1 接入ldap認證。

藍鲸社群版5.1 接入ldap認證

開發環境搭建

可能我們的藍鲸已經在生産中使用了,為了避免影響使用,我們臨時搭建藍鲸paas平台的統一登入服務。可參考騰訊藍鲸智雲 / bk-PaaS。

藍鲸paas平台有login(藍鲸統一登入服務)、paas(藍鲸開發者中心)、esb(藍鲸API網關)、appengine(藍鲸應用引擎)、paasagent(藍鲸應用引擎Agent);其中開發環境隻需搭建login即可,即藍鲸智雲下的所有服務依賴的統一登入服務, 包括作業平台/配置平台/PaaS平台/SaaS等。

部署過程可參考官方安裝部署部分,我這簡單介紹

1、建立資料庫

# 建立資料庫open_paas
CREATE DATABASE IF NOT EXISTS open_paas DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
           

2、部署web項目

# 虛拟環境, 自動進入paas virtualenv
$ virtualenv login

$ which python
$ cd paas-ce/paas/login/

# 安裝依賴
$ pip install -r requirements.txt

# 修改配置檔案, 配置資料庫,域名等; 注意如果是本地開發需要配置 LOGIN_DOMAIN
$ vim conf/settings_development.py

# 注意, login / paas 務必要執行migrate
# 執行migration, 其中 login / paas 兩個項目需要做 migration
python manage.py migrate

# 拉起服務, 可以使用其他的托管服務, 例如supervisor
$ python manage.py runserver 8003
           

3、配置檔案

主要修改下面兩個即可

# paas
paas/conf/settings_development.py

# login
login/conf/settings_development.py
           

以上為開發環境搭建,用于前期的開發調試階段。

以下為藍鲸社群版5.1的正式接入。

正式接入

1.登入功能描述

1.普通使用者登入先經ldap認證,若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-login
pip install ldap3
一定要是在open_paas-login這個虛拟環境下,否則ldap會找不到

#中控機
cd /data/bkce/open_paas/login/ee_login
#建立自定義登入子產品目錄
mkdir enterprise_ldap
#修改配置檔案
vim settings_login.py
# -*- coding: utf-8 -*-
"""
Tencent is pleased to support the open source community by making 藍鲸智雲PaaS平台社群版 (BlueKing PaaS Community Edition) available.
Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
""" # noqa

# 藍鲸登入方式: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'
           

配置檔案主要修改LOGIN_TYPE、CUSTOM_LOGIN_VIEW、CUSTOM_AUTHENTICATION_BACKEND。

其中:

LOGIN_TYPE 是 設定自定義登入的方式,custom_login就是自定義的方式

CUSTOM_LOGIN_VIEW 是登入頁面中處理登入跳轉的函數,在enterprise_ldap下的views中的login

CUSTOM_AUTHENTICATION_BACKEND 是驗證登入的函數,在enterprise_ldap下的backends中的ldapbackend

4.登入跳轉

vim enterprise_ldap/views.py
# -*- coding: utf-8 -*-
    
from django.http.response import HttpResponse
from bkaccount.accounts import Account
from django.contrib.sites.shortcuts import get_current_site
from django.template.response import TemplateResponse
from .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 ):CustomLoginForm,這樣login登入就不需要走API了,在本地就可實作。

登入後的跳轉處理仍使用原來的處理。

5.自定義表單

vim enterprise_ldap/utils.py
# -*- coding: utf-8 -*-
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import authenticate
from common.log import logger

class 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.authenticate實作

vim enterprise_ldap/backends.py
# -*- coding: utf-8 -*-

from django.contrib.auth.backends import ModelBackend
from .ldap import SearchLdap
from django.contrib.auth import get_user_model
from bkaccount.constants import RoleCodeEnum
from common.log import logger

class 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字段,并判斷是否在藍鲸資料庫中存在,不存在則建立使用者并授予普通管理者角色;

擷取ldap中的使用者資訊,通過enterprise_ldap/ldap.py實作。

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

7.ldap擷取使用者資訊

vim enterprise_ldap/ldap.py
# -*- coding: utf-8 -*-

from ldap3 import Connection, Server, SUBTREE
from common.log import logger

class SearchLdap:
    host = '10.90.10.123'
    port = 389
    ldap_base = 'ou=People,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,ou=People,dc=test,dc=cn”,否則此時也能正常過濾資訊。

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
           

網友回報:

1.由于我這面環境使用的是open-ldap,設定的使用者名和cn名稱保持一緻。

經網友回報,使用AD域的情況下,認證不成功,問題出在ldap連接配接方式上。

AD認證隻需要賬号密碼即可,如ldap_user=‘domain\’+username。

此網友接入後,出現登入後有跳出的現象,重新開機了整個paas後才正常。

2.目錄下需要有__init__.py空檔案,否則導緻子產品無法導入,如“enterprise_ldap.backends”。

其他,請根據環境實際情況進行調整。

PS:

如果你對博文感興趣,請關注我的公衆号“木讷大叔愛運維”,與你分享運維路上的點滴。

繼續閱讀