導讀
使用者管理可以說是運維工作最基礎的部分,隨著企業的發展,我們面對的將不僅僅是一個或多個系統,使用者也可能成倍增長。此時統一使用者認證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平臺的統一登入服務的原始碼解析,不僅僅是功能上的實現,更重要的是參考大廠研發程式碼的佈局、流程、規範等,給我們自身帶來的啟發。