测试基础 Django 接入 LDAP 用户认证

花菜 · 2023年10月22日 · 最后由 花菜 回复于 2023年10月23日 · 3787 次阅读

1、背景

在公司中,一般都会有 LDAP 认证,我们开发的效能平台如果单独设置一套用户认证体系,那样会很不方便。

因此,效能平台想要获得更好的用户体验,就需要在接入公司现在有的 LDAP 用户认证。

2、Django 接入 LDAP 认证

拿我的开源项目举例子,后端使用的框架是 Django,已经有现成的 LDAP 库,直接集成即可,不需要造轮子。

下面是具体的步骤

2.1 安装 django-ldap-auth

pip install django-auth-ldap==2.3.0

一定要指定版本,否则可能会出现不兼容的情况

我使用的 Django2.2,所以最多能支持的版本是 2.3.0

具体的版本兼容查看这里
https://django-auth-ldap.readthedocs.io/en/latest/changes.html#id4

2.2 Settings 配置 LDAP

在 Django 的配置文件最底部加上这个即可

# settings.py

# LDAP配置
import ldap
from django_auth_ldap.config import LDAPSearch

AUTHENTICATION_BACKENDS = (
    'django_auth_ldap.backend.LDAPBackend',
)

USE_LDAP = False # 如果需要开启LDAP认证,就设置位True
AUTH_LDAP_SERVER_URI = "ldap://localhost:389" # LDAP服务器地址,默认端口389

AUTH_LDAP_BIND_DN = "cn=admin,dc=myorg,dc=com" # LDAP管理员账号
AUTH_LDAP_BIND_PASSWORD = "admin" # LDAP管理员密码
AUTH_LDAP_USER_SEARCH = LDAPSearch(
    "ou=Tester,dc=myorg,dc=com",
    ldap.SCOPE_SUBTREE,
    "(uid=%(user)s)",
) # LDAP搜索账号,ou可以理解为组织单位或者部门,不填写也是ok,dc可以理解为域名

AUTH_LDAP_USER_ATTR_MAP = {
    "username": "uid",
    "first_name": "givenName",
    "last_name": "sn",
    "email": "mail",
} # 不配置也可以

2.3 登录接口中使用 LDAP 认证

整体逻辑是:
判断 settings 的 USE_LDAP 是否为真,为真就走 LDAP 认证
LDAP 认证成功,就会设置 local_user,然后返回 jwt token
LDAP 认证不成功,走本地认证,认证成功就返回 jwt token
最终两个都不成功,就会返回登录失败

# views.py
import logging
from typing import Optional

from django.conf import settings
from django.contrib.auth import authenticate, get_user_model
from django.contrib.auth.hashers import check_password, make_password
from drf_yasg.utils import swagger_auto_schema
from rest_framework.response import Response
from rest_framework.views import APIView



def ldap_auth(username: str, password: str) -> Optional[User]:
    ldap_user = authenticate(username=username, password=password)
    if ldap_user and ldap_user.backend == "django_auth_ldap.backend.LDAPBackend":
        logger.info(f"LDAP authentication successful for {username}")
        local_user: User = User.objects.filter(username=username).first()
        if local_user:
            local_user.password = make_password(password)
            local_user.save(update_fields=["password"])
            logger.info(f"ldap认证通过,更新本地用户密码: {username}")
        return local_user
    logger.info(f"LDAP authentication failed for {username}")
    return None


def local_auth(username: str, password: str) -> Optional[User]:
    local_user = User.objects.filter(username=username).first()
    if not local_user:
        logger.warning(f"Local user does not exist: {username}")
        return None
    if local_user.is_active == 0:
        logger.warning(f"Local user is blocked: {username}")
        return None
    if not check_password(password, local_user.password):
        logger.warning(f"Local authentication failed: {username}")
        return None
    return local_user


def generate_token_and_respond(local_user: User):
    jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
    jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
    payload = jwt_payload_handler(local_user)
    token = jwt_encode_handler(payload)
    response.LOGIN_SUCCESS["token"] = token
    response.LOGIN_SUCCESS["user"] = local_user.username
    response.LOGIN_SUCCESS["is_superuser"] = local_user.is_superuser
    response.LOGIN_SUCCESS["show_hosts"] = local_user.show_hosts
    return Response(response.LOGIN_SUCCESS)



class LoginView(APIView):
    """
    登陆视图,用户名与密码匹配返回token
    """

    authentication_classes = ()
    permission_classes = ()

    @swagger_auto_schema(request_body=UserLoginSerializer)
    def post(self, request):
        serializer = UserLoginSerializer(data=request.data)

        if serializer.is_valid():
            username: str = serializer.validated_data["username"]
            password: str = serializer.validated_data["password"]
            masked_password = f"{password[0]}{'*' * (len(password) - 2)}{password[-1]}"
            logger.info(f"Received login request for {username=}, password={masked_password}")

            local_user = None
            if settings.USE_LDAP:
                logger.info(f"Attempting LDAP authentication for {username=}")
                local_user = ldap_auth(username, password)

            if not local_user:
                logger.info(
                    f"LDAP authentication failed or not enabled, falling back to local authentication for {username=}")
                local_user = local_auth(username, password)

            if local_user:
                logger.info(f"Authentication successful for {username=}")
                return generate_token_and_respond(local_user)
            else:
                logger.info(f"Authentication failed for {username=}")
                return Response(response.LOGIN_FAILED)
        else:
            return Response(serializer.errors)

## 2.4 LDAP 认证测试
LDAP 服务端配置如下:

  • 有一个公司 myorg.com
  • 有一个部门 TestORg
  • TestOrg 部门有一个小组 AutoTest
  • AutoTest 下面有一个用户 ayo,密码是 123456

image.png
### 2.4.1 LDAP 认证成功

 2023-10-22 20:53:28,812 [e66bf977a7e146b68996c5aa164c55d6] fastuser.views INFO [pid:57614] [views.py->post:116] Received login request for username='ayo', password=1****6
2023-10-22 20:53:28,818 [e66bf977a7e146b68996c5aa164c55d6] fastuser.views INFO [pid:57614] [views.py->post:120] Attempting LDAP authentication for username='ayo'
2023-10-22 20:53:28,831 [e66bf977a7e146b68996c5aa164c55d6] django_auth_ldap WARNING [pid:57614] [backend.py->_populate_user_from_attributes:653] cn=ayo,cn=autotest,ou=testorg,dc=myorg,dc=com does not have a value for the attribute givenName
2023-10-22 20:53:28,833 [e66bf977a7e146b68996c5aa164c55d6] django_auth_ldap WARNING [pid:57614] [backend.py->_populate_user_from_attributes:653] cn=ayo,cn=autotest,ou=testorg,dc=myorg,dc=com does not have a value for the attribute mail
2023-10-22 20:53:28,836 [e66bf977a7e146b68996c5aa164c55d6] fastuser.views INFO [pid:57614] [views.py->ldap_auth:63] LDAP authentication successful for ayo
2023-10-22 20:53:28,930 [e66bf977a7e146b68996c5aa164c55d6] fastuser.views INFO [pid:57614] [views.py->ldap_auth:68] ldap认证通过,更新本地用户密码: ayo
2023-10-22 20:53:28,932 [e66bf977a7e146b68996c5aa164c55d6] fastuser.views INFO [pid:57614] [views.py->post:129] Authentication successful for username='ayo'
2023-10-22 20:53:28,942 [e66bf977a7e146b68996c5aa164c55d6] django.server INFO [pid:57614] [basehttp.py->log_message:154] "POST /api/user/login/ HTTP/1.1" 200 264

image.png
### 2.4.2 LDAP 认证失败,走本地认证成功

2023-10-22 20:50:17,208 [120eabbc698741198c2c068179e7294b] fastuser.views INFO [pid:54892] [views.py->post:116] Received login request for username='huacai', password=1****6
2023-10-22 20:50:17,216 [120eabbc698741198c2c068179e7294b] fastuser.views INFO [pid:54892] [views.py->post:120] Attempting LDAP authentication for username='huacai'
2023-10-22 20:50:17,224 [120eabbc698741198c2c068179e7294b] django_auth_ldap ERROR [pid:54892] [config.py->execute:152] search_s('ou=Tester,dc=myorg,dc=com', 2, '(uid=huacai)') raised NO_SUCH_OBJECT({'msgtype': 101, 'msgid': 2, 'result': 32, 'desc': 'No such object', 'ctrls': [], 'matched': 'dc=myorg,dc=com'})
2023-10-22 20:50:17,228 [120eabbc698741198c2c068179e7294b] fastuser.views INFO [pid:54892] [views.py->ldap_auth:70] LDAP authentication failed for huacai
2023-10-22 20:50:17,230 [120eabbc698741198c2c068179e7294b] fastuser.views INFO [pid:54892] [views.py->post:124] LDAP authentication failed or not enabled, falling back to local authentication for username='huacai'
2023-10-22 20:50:17,322 [120eabbc698741198c2c068179e7294b] fastuser.views INFO [pid:54892] [views.py->post:129] Authentication successful for username='huacai'
2023-10-22 20:50:17,339 [120eabbc698741198c2c068179e7294b] django.server INFO [pid:54892] [basehttp.py->log_message:154] "POST /api/user/login/ HTTP/1.1" 200 284

image.png

2.4.3 LDAP 和本地都认证失败

2023-10-22 21:02:09,705 [f65679ab9b75405daeeb06e0fd6cf7bb] fastuser.views INFO [pid:57614] [views.py->post:116] Received login request for username='huacai', password=1*******9
2023-10-22 21:02:09,711 [f65679ab9b75405daeeb06e0fd6cf7bb] fastuser.views INFO [pid:57614] [views.py->post:120] Attempting LDAP authentication for username='huacai'
2023-10-22 21:02:09,717 [f65679ab9b75405daeeb06e0fd6cf7bb] fastuser.views INFO [pid:57614] [views.py->ldap_auth:70] LDAP authentication failed for huacai
2023-10-22 21:02:09,719 [f65679ab9b75405daeeb06e0fd6cf7bb] fastuser.views INFO [pid:57614] [views.py->post:124] LDAP authentication failed or not enabled, falling back to local authentication for username='huacai'
2023-10-22 21:02:09,812 [f65679ab9b75405daeeb06e0fd6cf7bb] fastuser.views WARNING [pid:57614] [views.py->local_auth:83] Local authentication failed: huacai
2023-10-22 21:02:09,814 [f65679ab9b75405daeeb06e0fd6cf7bb] fastuser.views INFO [pid:57614] [views.py->post:132] Authentication failed for username='huacai'
2023-10-22 21:02:09,820 [f65679ab9b75405daeeb06e0fd6cf7bb] django.server INFO [pid:57614] [basehttp.py->log_message:154] "POST /api/user/login/ HTTP/1.1" 200 64

image.png

3、Docker 安装 python-ldap 失败

在本地验证通过之后,推送到 GitHub,但结果 action 构建失败了。

一看报错,大概是缺少了某个 lib(libldap2-dev libsasl2-dev)

把 log 粘贴过来给 gpt,一下子就够搞定,太爽啦!!!

image.png

4、总结

  • Django 接入 LDAP 很简单,只需加上对应配置后,再调用 LDAP 认证即可
  • 在 docker 中,安装 python-ldap 需要额外的依赖

文章全部代码在以下两个 Pull Request:

公众号原文链接

共收到 5 条回复 时间 点赞

如果你有更好的想法,欢迎留言和我交流哦

gpt4 高阶搜索。。

自己封装的,逻辑就是首次登录用 ladp,非首次登录可以用任意密码,ladp 密码记不住咋办毕竟一直再改

disable 回复

我这里是 LDAP 优先,因为之前公司是要求定期更新 LDAP 密码,并且多个系统用到 LDAP 认证

恒温 回复

有了 gpt4,日常使用搜索频率大幅度下降😂

6楼 已删除
花菜 一键搭建 LDAP 认证服务 中提及了此贴 10月27日 00:15
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册