Django 借助 ldap3 自定义支持 LDAP 域账号的认证后端

一、项目初始化

  • pip install django ldap3
  • django-admin startproject auth_demo
  • cd auth_demo
  • django-admin startapp authldap
  • python manage.py migrate

二、编写自定义认证后端

Django 的认证系统支持的自定义插件,本质上是一个实现了 get_user(user_id)authenticate(request, **credentials) 方法的类。
其中 get_user 接收 user_id(可以是用户名、ID 等,但必须为 user 对象的主键)返回匹配的用户对象或 Noneauthenticate 接收 request 参数以及认证信息,根据最终的认证结果返回用户对象(认证通过)或 None(认证不通过)。

auth_demo/authldap/authbackends.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# auth_demo/authldap/authbackends.py
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.models import User
import ldap3

# 替换为实际的域控 IP
LDAP_HOST = 'xx.xx.xx.xx'


class LdapBackend(BaseBackend):
def authenticate(self, request, username=None, password=None):
if ldap_auth(username, password):
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
user = User(username=username)
if username.endswith('admin'):
user.is_staff = True
user.is_superuser = True
user.save()
return user
return None

def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None


# @example.com 改为自己域环境的域名
def ldap_auth(username, password):
username = username + '@example.com'\
if '@' not in username else username
server = ldap3.Server(LDAP_HOST, port=636, use_ssl=True)
conn = ldap3.Connection(server, username, password)
return conn.bind()

代码中的 ldap_auth 函数用于执行 LDAP 认证,将 Django 收到的认证信息传递给 LDAP 服务器,通过 Connection 对象的 bind() 方法确认用户名密码是否正确。正确返回 True,不正确则返回 False

LDAP 认证通过后,再检查 User 数据库表中是否已包含该用户。若该用户存在,则直接返回对应的 User 对象;若该用户不存在(第一次登录),则先在 User 中创建同名的新用户并添加权限等,保存后返回刚创建的 User 对象。

假设所有需要添加 Django 管理员权限的账号名字都以 admin 结尾。。。

配置认证后端

Django 认证时使用的插件列表由 settings.py 中的 AUTHENTICATION_BACKENDS 字段指定,默认值为 ['django.contrib.auth.backends.ModelBackend']
因此为了用上前面创建的自定义认证后端,需在 auth_demo/auth_demo/settings.py 配置文件中添加以下内容:

1
2
3
4
AUTHENTICATION_BACKENDS = [
'auth_ldap.authbackends.LdapBackend',
'django.contrib.auth.backends.ModelBackend'
]

三、测试

  • 运行 python manage.py runserver 0.0.0.0:8000 启动 Web 服务
  • 在域中创建 testaccounttestaccount-admin 测试账号
  • 访问 http://xx.xx.xx.xx:8000/admin 进入 Django 后台,用测试账号登录

效果如下:
no staff

staff & admin

staff & admin

四、初始化用户组

首先梳理下之前代码的逻辑流程:

  • Django 后台获取前端传入的认证信息,并转发给 LDAP 服务器进行认证
  • 认证成功且该用户不在数据库中(首次登录),则创建对应的用户并将其返回;该用户已存在则直接返回
  • 创建用户时,若用户名以 admin 结尾,则额外向其添加 staff 权限和管理员权限
  • 认证失败返回 None

假设在数据库中创建新用户时,需要根据一定的规则对用户的属组进行初始化(比如名称以 admin 结尾的用户自动添加到 admin 组中)。最终的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# auth_demo/authldap/authbackends.py
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.models import User, Group
import ldap3

# 替换为实际的域控 IP
LDAP_HOST = 'xx.xx.xx.xx'


class LdapBackend(BaseBackend):
def authenticate(self, request, username=None, password=None):
if ldap_auth(username, password):
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
user = User(username=username)
user.save()
if username.endswith('admin'):
user.is_staff = True
user.is_superuser = True
add_group(user)
user.save()
return user
return None

def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None


# @example.com 改为自己域环境的域名
def ldap_auth(username, password):
username = username + '@example.com'\
if '@' not in username else username
server = ldap3.Server(LDAP_HOST, port=636, use_ssl=True)
conn = ldap3.Connection(server, username, password)
return conn.bind()


def add_group(user, groupname='admin'):
try:
group = Group.objects.get(name=groupname)
except Group.DoesNotExist:
group = Group(name=groupname)
group.save()
group.user_set.add(user)
group.save()

删除上一步中数据库里新建的 testaccount-admin 账号,重新登录测试。则 LDAP 认证成功后,Django 后台会在数据库中新建 testaccount-admin 账号并将其添加至 admin 用户组中(如 admin 组不存在则新建该用户组)。即在新建账号的同时初始化其属组。
groups

参考资料

Customizing authentication in Django