django python3 ldap_[原创]django+ldap实现统一认证部分二(python-ldap实践)

前言

python-ldap

我在sso项目的backend/lib/common/下添加一个ldaphelper.py文件,其中定义一个类

#!/usr/bin/env python

# -*- coding:utf-8 -*-

import ldap

import ldap.modlist as modlist

# 加载log配置

import logging

logger = logging.getLogger()

import sys

reload(sys)

sys.setdefaultencoding('utf8')

'''

实现LDAP用户登录验证,首先获取用户的dn,然后再验证用户名和密码

'''

from SSOadmin import settings

# 登陆 地址

LDAP_URI = settings.AUTH_LDAP_SERVER_URI

# 登陆 账户

LDAP_USER = settings.AUTH_LDAP_BIND_DN

# 登陆 密码

LDAP_PASS = settings.AUTH_LDAP_BIND_PASSWORD

# 默认 区域

BASE_DN = settings.base_dn

class LDAPTool(object):

def __init__(self,

ldap_uri=None,

base_dn=None,

user=None,

password=None):

"""

初始化

:param ldap_uri: ldap_uri

:param base_dn: 区域

:param user: 默认用户

:param password: 默认密码

:return:

"""

if not ldap_uri:

ldap_uri = LDAP_URI

if not base_dn:

self.base_dn = BASE_DN

if not user:

self.admin_user = LDAP_USER

if not password:

self.admin_password = LDAP_PASS

try:

self.ldapconn = ldap.initialize(ldap_uri) # 老版本使用open方法

self.ldapconn.simple_bind(self.admin_user, self.admin_password) # 绑定用户名、密码

except ldap.LDAPError, e:

logger.error('ldap conn失败,原因为: %s' % str(e))

def ldap_search_dn(self, value=None, value_type='uid'):

"""

# 根据表单提交的用户名,检索该用户的dn,一条dn就相当于数据库里的一条记录。

# 在ldap里类似cn=username,ou=users,dc=gccmx,dc=cn,验证用户密码,必须先检索出该DN

:param value: 用户 uid或 组cn

:param value_type: 用户 uid|cn

:return: search result

"""

obj = self.ldapconn

obj.protocal_version = ldap.VERSION3

searchScope = ldap.SCOPE_SUBTREE

retrieveAttributes = None

if value_type == 'cn':

searchFilter = "cn=" + value

else:

searchFilter = "uid=" + value

try:

ldap_result_id = obj.search(

base=self.base_dn,

scope=searchScope,

filterstr=searchFilter,

attrlist=retrieveAttributes

)

result_type, result_data = obj.result(ldap_result_id, 0)

if result_type == ldap.RES_SEARCH_ENTRY:

return result_data

else:

return None

except ldap.LDAPError, e:

logger.error('ldap search %s 失败,原因为: %s' % (value, str(e)))

def ldap_get_user(self, uid=None):

"""

通过查询用户uid,从ldap_search_dn进一步提取所需数据,search到的是全部信息

:param uid:

:return: {‘uid’:'zhangsan','mail':'zhangsan@xxx.com','cn':'张三'}

"""

result = None

try:

search = self.ldap_search_dn(value=uid, value_type=uid)

if search is None:

raise ldap.LDAPError('未查询到相应 id')

for user in search:

if user[1]['uid'][0] == uid:

result = {

'uid': uid,

'mail': user[1]['mail'][0],

'cn': user[1]['cn'][0],

}

except Exception, e:

logger.error('获取用户%s 失败,原因为: %s' % (uid, str(e)))

return result

def __ldap_getgid(self, cn="员工"):

"""

查询 组cn对应的gid

:param cn: 组cn

:return: 对应cn的gidNumber

"""

obj = self.ldapconn

obj.protocal_version = ldap.VERSION3

searchScope = ldap.SCOPE_SUBTREE

retrieveAttributes = None

searchFilter = "cn=" + cn

try:

ldap_result_id = obj.search(

base="ou=Group,%s" % self.base_dn,

scope=searchScope,

filterstr=searchFilter,

attrlist=retrieveAttributes

)

result_type, result_data = obj.result(ldap_result_id, 0)

if result_type == ldap.RES_SEARCH_ENTRY:

return result_data[0][1].get('gidNumber')[0]

else:

return None

except ldap.LDAPError, e:

logger.error('获取gid失败,原因为: %s' % str(e))

def __get_max_uidNumber(self):

"""

查询 当前最大的uid,这个是在添加用户时,用于自增uid

:param: None

:return: max uidNumber

"""

obj = self.ldapconn

obj.protocal_version = ldap.VERSION3

searchScope = ldap.SCOPE_SUBTREE

retrieveAttributes = ['uidNumber']

searchFilter = "uid=*"

try:

ldap_result = obj.search(

base="ou=People,%s" % self.base_dn,

scope=searchScope,

filterstr=searchFilter,

attrlist=retrieveAttributes

)

result_set = []

while True:

result_type, result_data = obj.result(ldap_result, 0)

if not result_data:

break

else:

if result_type == ldap.RES_SEARCH_ENTRY:

result_set.append(int(result_data[0][1].get('uidNumber')[0]))

return max(result_set) + 1

except ldap.LDAPError, e:

logger.error('获取最大uid失败,原因为: %s' % str(e))

def ldap_add_user(self, cn, mail, username, password):

"""

添加ldap用户

:param cn: 中文名, mail: 邮箱, username: 用户名, password: 密码

:return: True/None

"""

result = None

try:

obj = self.ldapconn

obj.protocal_version = ldap.VERSION3

addDN = "uid=%s,ou=People,%s" % (username, BASE_DN)

attrs = {}

attrs['objectclass'] = ['top', 'person', 'inetOrgPerson', 'posixAccount', 'organizationalPerson']

attrs['cn'] = str(cn)

attrs['homeDirectory'] = str('/home/%s' % username)

attrs['loginShell'] = '/bin/bash'

attrs['mail'] = str(mail)

attrs['sn'] = str(username)

attrs['uid'] = str(username)

attrs['userPassword'] = str(password)

attrs['uidNumber'] = str(self.__get_max_uidNumber())

attrs['gidNumber'] = self.__ldap_getgid(cn='员工')

ldif = ldap.modlist.addModlist(attrs)

obj.add_s(addDN, ldif)

obj.unbind_s()

result = True

except ldap.LDAPError, e:

logger.error("生成用户%s 失败,原因为: %s" % (username, str(e)))

return result

def check_user_belong_to_group(self, uid, group_cn='员工'):

"""

查询 用户 是否归属于某个组

:param uid: 用户uid , Ex: 'ssoadmin'

:param group_cn: 归属组cn , Ex: '黑名单'

:return: True|None

"""

result = None

try:

search = self.ldap_search_dn(value=group_cn, value_type='cn')

if search is None:

raise ldap.LDAPError('未查询到相应 id')

member_list = search[0][1].get('memberUid', [])

if uid in member_list:

result = True

except ldap.LDAPError, e:

logger.error('获取用户%s与组%s关系失败,原因为: %s' % (uid, group_cn, str(e)))

return result

def check_user_status(self, uid):

"""

验证用户状态

:param uid: 用户uid

:return: 200: 用户可用

404: 用户不存在

403: 用户被禁用

"""

result = 404

try:

target_cn = self.ldap_get_user(uid=uid)

if target_cn is None: # 如未查到用户,记录日志,但不算错误,后边有很多地方会验证用户是否存在

result = 404

logger.debug("%s uid未查询到" % uid)

else:

if self.check_user_belong_to_group(uid=uid, group_cn='黑名单'):

result = 403

else:

result = 200

except ldap.LDAPError, e:

logger.error("%s 检查用户状态失败,原因为: %s" % (uid, str(e)))

return result

def ldap_update_password(self, uid, new_password):

"""

更新密码

:param uid: 用户uid,新password

:return: True|None

"""

result = None

try:

obj = self.ldapconn

obj.protocal_version = ldap.VERSION3

modifyDN = "uid=%s,ou=People,%s" % (uid, BASE_DN)

# 因为是更新密码,如用passwd_s方法需要oldpassword,如果用下边方法,是增加一个新密码,而不是替换,而我们的需求是重置密码

# old_password = {'userPassword': ''}

# new_password = {'userPassword': new_password}

# ldif = modlist.modifyModlist(old_password, new_password)

# obj = modlist.modifyModlist(modifyDN, ldif)

# 以下方法实现密码替换的效果,第二个参数就是要替换的属性名,可以变更其他属性

obj.modify_s(modifyDN, [(ldap.MOD_REPLACE, 'userPassword', [str(new_password)])])

obj.unbind_s()

result = True

except ldap.LDAPError, e:

logger.error("%s 密码更新失败,原因为: %s" % (uid, str(e)))

return result

# for test

def main():

# print type(LDAPTool().get_max_uidNumber())

# print(LDAPTool().ldap_search_dn(value='qudong'))

# print(LDAPTool().check_user_belong_to_group('qudong', '黑名单'))

# print LDAPTool().ldap_get_user(uid='qudong')

# print(LDAPTool().check_user_and_email(uid='qudong', email='qudong@ssotest.com'))

# s=LDAPTool()

# print s.ldap_add_user('哈喽2','test222@ssotest.com','test222','test222')

# print(s._LDAPTool__ldap_getgid('黑名单'))

# print(LDAPTool().ldap_update_password('qudong','111111'))

pass

if __name__ == '__main__':

main()

view调用方面,就没什么太多说的了,就是把原先对本地数据库的user表的增删改查,移植到ldaphelper上即可,下边以我的修改密码的逻辑为例做以说明,其他的包括注册、登录都大同小异了。修改密码还涉及到一个验证码发送和验证的逻辑,可以用于参考

models.py,这里自定义了user表,参考Django admin定制化,User字段扩展[原创],自定义扩展user表,额外就多了一张验证码表

class MyUser(AbstractUser):

name = models.CharField(u'中文名', max_length=32, blank=False, null=False)

class Meta:

verbose_name = u'用户详情'

verbose_name_plural = u"用户详情"

class User_ex(models.Model):

"""User models ex"""

email = models.EmailField(unique=True, blank=False, null=False)

valid_code = models.CharField(max_length=24) #验证码

valid_time = models.DateTimeField(auto_now=True) #验证码有效时间

class Meta:

verbose_name = u'验证码'

verbose_name_plural = u"验证码"

def __unicode__(self):

return u'%s' % self.valid_code

获取验证码的view,生成验证码,入库,发送验证码邮件,发送邮件的方法send_mail在其他地方写好了

from web_sso.forms.account import LoginForm, RegisterForm, ForgetPwdForm

from web_sso import models

from backend.lib.common.sendemail import send_mail

from backend.lib.common.ldaphelper import LDAPTool

import logging

......

def get_email_code(request):

"""get email code"""

email = request.GET.get('email', '')

type = request.GET.get('type', '')

code = ''.join(random.sample(string.digits + string.letters, 4))

data = {}

data['success'] = False

data['message'] = ''

try:

# 检查邮箱

username = email.rsplit('@', 1)[0]

user_count = LDAPTool().check_user_status(uid=username)

# user_count = models.MyUser.objects.filter(email=email).count()

if type == "register":

if user_count != 404:

data['success'] = False

data['message'] = u'此用户%s已被注册过' % username

raise Exception(data['message'])

elif type == "forget_pwd":

if user_count == 404:

data['success'] = False

data['message'] = u'此邮箱未被注册过'

raise Exception(data['message'])

# 检查短时间内是否有生成过验证码

user_ex = models.User_ex.objects.filter(email=email)

if len(user_ex) > 0:

user_ex = user_ex[0]

# 两个datetime相减,得到datetime.timedelta类型

create_time = user_ex.valid_time

td = timezone.now() - create_time

if td.seconds < 60:

data['message'] = u'1分钟内发送过一次验证码'

raise Exception(data['message'])

# 发送邮件

subject = ""

if type == "register":

subject = u'[sa.ssotest.net]激活您的帐号'

elif type == "forget_pwd":

subject = u'[sa.ssotest.net]重置密码'

message = u"""

运维平台(sa.ssotest.net)

邮箱验证码
%s%s

请保管好您的验证,有效期10分钟

""" % (email, code)

mail_status = send_mail(subject, message, email, email)

if mail_status == 200:

# 邮件发送成功才入库验证码

models.User_ex.objects.filter(email=email).delete()

models.User_ex.objects.create(email=email, valid_code=code)

data['success'] = True

data['message'] = 'OK'

else:

data['success'] = False

data['message'] = "邮件发送失败,错误码:%s" % mail_status

raise Exception(data['message'])

except Exception:

pass

finally:

return HttpResponse(json.dumps(data), content_type="application/json")

验证码验证成功后,通过ldaphelper修改ldap中的对应用户密码,我这边注释的部分就是未使用ldap,使用数据库入库的方式,可作参考

def forget_pwd(request):

error = ''

forgetpwd_form = ForgetPwdForm(request.POST)

try:

if request.method == 'POST':

check = request.POST.get('checkcode', None).lower()

email = request.POST.get('email', None)

if not email:

error = '请输入邮箱.'

raise Exception('请输入邮箱.')

username = email.rsplit('@', 1)[0]

email_obj = models.User_ex.objects.get(email=email)

email_check = email_obj.valid_code.lower()

# 10分钟有效期

email_check_time = email_obj.valid_time

if (timezone.now() - email_check_time).seconds >= 600:

error = '验证码失效.'

elif check != email_check:

error = '验证码错误.'

else:

if not forgetpwd_form.is_valid():

error = '格式错误.'

else:

data = forgetpwd_form.clean()

ldap_obj = LDAPTool()

# if not models.MyUser.objects.filter(Q(username=username) | Q(email=email)):

if ldap_obj.check_user_status(uid=username) == 404:

error = '此邮箱未被注册过.'

else: # 重置密码成功

ldap_obj.ldap_update_password(uid=username, new_password=data.get('password', ''))

# user_obj = models.MyUser.objects.get(username=username,

# email=data.get('email', ''), )

# user_obj.set_password(data.get('password', ''))

# user_obj.save()

target = '%s?from=%s&username=%s' % ('/account/login/', 'forget_pwd', username)

return redirect(target)

except Exception, e:

logger.error(str(e))

return render(request, 'account/forget_pwd.html', {'model': forgetpwd_form, 'error': error})

综上,我们已经可以和ldap结合的比较好了,其中django-auth-ldap实现的功能,一般像一些成熟的软件,如csvn、jira、openvpn都可以通过配置来实现,python-ldap实现的部分是这个系统的核心所在,通过我们的平台对ldap进行操作,当然也可以使用ldap管理后台(我这边用的lam)来实现类似功能。

后边还有单点登录的实现部分,请看后续文章。

[原创]django+ldap实现单点登录(装饰器和缓存)

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值