蓝鲸社区版5.1 介入公司内部ldap认证
官方文档社区版: 蓝鲸登录接入企业内部登录中已经通过接入google登录的例子进行说明;但是公司内部只有ldap作为内部服务的统一认证,并不提供相关登录API。
以上恐怕也是很多中小企业的现状,这种情况下该如何进行接入ldap呢?
别急,我们先来看下源码是怎么实现的?
下面我们来分析下蓝鲸paas平台统一登录服务基本函数接口来看下登录流程,供我们参考。
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是一个表单。
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正是自定义接入企业登录模块要重写的函数,也就和 社区版: 蓝鲸登录接入企业内部登录
中的介绍对上了。
公司在没有登录API的情况下,其实我们可以通过重写AuthenticationForm表单的clean方法来进行本地认证。
下面我们就来实现下蓝鲸社区版5.1 接入ldap认证。
可能我们的蓝鲸已经在生产中使用了,为了避免影响使用,我们临时搭建蓝鲸paas平台的统一登录服务。可参考腾讯蓝鲸智云 / bk-PaaS。
蓝鲸paas平台有login(蓝鲸统一登录服务)、paas(蓝鲸开发者中心)、esb(蓝鲸API网关)、appengine(蓝鲸应用引擎)、paasagent(蓝鲸应用引擎Agent);其中开发环境只需搭建login即可,即蓝鲸智云下的所有服务依赖的统一登录服务, 包括作业平台/配置平台/PaaS平台/SaaS等。
部署过程可参考官方安装部署部分,我这简单介绍
# 创建数据库open_paas
CREATE DATABASE IF NOT EXISTS open_paas DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
# 虚拟环境, 自动进入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
主要修改下面两个即可
# paas
paas/conf/settings_development.py
# login
login/conf/settings_development.py
以上为开发环境搭建,用于前期的开发调试阶段。
以下为蓝鲸社区版5.1的正式接入。
1.普通用户登录先经ldap认证,若ldap中存在,蓝鲸中不存在,则创建新用户并将其设置为普通用户;
2.admin用户登录跳过ldap认证,直接走蓝鲸认证;
思考:
对于ldap无法连接或连接失败的状况,可以跳过ldap认证,走蓝鲸认证。这个功能在本次开发中没有完成,大家可自行实现。
ee_login/
├── enterprise_ldap ##自定义登录模块目录
│ ├── backends.py ##验证用户合法性
│ ├──_init_.py
│ ├── ldap.py ##接入ldap并获取用户信息
│ ├── utils.py ##自定义表单,集成AuthenticationForm,重写clean方法
│ ├── views.py ##登录处理逻辑函数
├── _init_.py
└── settings_login.py ##自定义登录配置文件
#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
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了,在本地就可实现。
登录后的跳转处理仍使用原来的处理。
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进行了对用户名、密码的验证。
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,则直接蓝鲸认证;
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”,否则此时也能正常过滤信息。
/data/install/bkcec stop paas login
/data/install/bkcec start paas login
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:
如果你对博文感兴趣,请关注我的公众号“木讷大叔爱运维”,与你分享运维路上的点滴。