蓝鲸社区版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:
如果你对博文感兴趣,请关注我的公众号“木讷大叔爱运维”,与你分享运维路上的点滴。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值