藍鯨社區版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/backends.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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章