小Cの已经记不起来的博客

docker版open-webui增加钉钉登录

之前公司使用的chatgpt-web开源项目已经正式宣布停止更新了, 现在准备使用open-webui进行代替...那么就需要进行一下二次开发以便支持钉钉登录

原理很简单, 获取钉钉登录后的用户信息,自动生成一个邮箱账号和密码,邮箱账号我直接使用的md5(钉钉昵称),密码直接使用的open_id+盐,这里只作为一个方法展示,不探讨密码安全性

其实非docker版应该操作方式大差不差

接下来是假设你已经安装并成功运行了open-webui

解决方案

先是对 /app/backend/open_webui/routers/auths.py 文件的修改,主要修改有两个地方,第一个是修改 signup 函数让其支持在关闭注册权限的情况下进行自动注册;第二个是增加 signin_dingding 函数以便支持钉钉自动注册、登录,修改如下:

修改 signup 函数,中文备注为修改位置,最好手动修改,不要直接覆盖,不保证后续版本的代码和下面一致!

# 接收dingding参数
async def signup(request: Request, response: Response, form_data: SignupForm, dingding=False):
    if WEBUI_AUTH:
        if (
                (not request.app.state.config.ENABLE_SIGNUP
            or not request.app.state.config.ENABLE_LOGIN_FORM) and not dingding # 增加钉钉判断,注意前面加了一对括号
        ):
            raise HTTPException(
                status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
            )
    else:
        if Users.get_num_users() != 0:
            raise HTTPException(
                status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
            )

    if not validate_email_format(form_data.email.lower()):
        raise HTTPException(
            status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
        )

    if Users.get_user_by_email(form_data.email.lower()):
        raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)

    try:
        role = (
            "admin"
            if Users.get_num_users() == 0
            else request.app.state.config.DEFAULT_USER_ROLE
        )

        if Users.get_num_users() == 0:
            # Disable signup after the first user is created
            request.app.state.config.ENABLE_SIGNUP = False

        hashed = get_password_hash(form_data.password)
        user = Auths.insert_new_auth(
            form_data.email.lower(),
            hashed,
            form_data.name,
            form_data.profile_image_url,
            role,
        )

        if user:
            expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN)
            expires_at = None
            if expires_delta:
                expires_at = int(time.time()) + int(expires_delta.total_seconds())

            token = create_token(
                data={"id": user.id},
                expires_delta=expires_delta,
            )

            datetime_expires_at = (
                datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc)
                if expires_at
                else None
            )

            # Set the cookie token
            response.set_cookie(
                key="token",
                value=token,
                expires=datetime_expires_at,
                httponly=True,  # Ensures the cookie is not accessible via JavaScript
                samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
                secure=WEBUI_AUTH_COOKIE_SECURE,
            )

            if request.app.state.config.WEBHOOK_URL:
                post_webhook(
                    request.app.state.config.WEBHOOK_URL,
                    WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
                    {
                        "action": "signup",
                        "message": WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
                        "user": user.model_dump_json(exclude_none=True),
                    },
                )

            user_permissions = get_permissions(
                user.id, request.app.state.config.USER_PERMISSIONS
            )

            return {
                "token": token,
                "token_type": "Bearer",
                "expires_at": expires_at,
                "id": user.id,
                "email": user.email,
                "name": user.name,
                "role": user.role,
                "profile_image_url": user.profile_image_url,
                "permissions": user_permissions,
            }
        else:
            raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
    except Exception as err:
        raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))

增加 signin_dingding 函数以便支持钉钉自动创建账号、登录

import hashlib
import json
import requests
from fastapi.responses import HTMLResponse
@router.get("/signin_dingding", response_class=HTMLResponse)
async def signin_dingding(request: Request, response: Response, code: str='', authCode: str=''):
    req = requests.post('https://api.dingtalk.com/v1.0/oauth2/userAccessToken', data=json.dumps({
        'clientSecret': 'clientSecret', # 自行替换
        'clientId': 'clientId', # 自行替换
        'code': code,
        'grantType': 'authorization_code'
    }), headers={
            'Content-Type': 'application/json'
        }
    )
    json_data = req.json()
    if json_data['corpId'] != 'xxx': # 自行替换, 用于防止其他企业的钉钉账号登录
        raise HTTPException(400, detail="错误的企业身份")

    req = requests.get('https://api.dingtalk.com/v1.0/contact/users/me', headers={
        'Content-Type': 'application/json',
        'x-acs-dingtalk-access-token': json_data['accessToken']
      }
    )
    json_data = req.json()
    print(json_data, flush=True)
    open_id = json_data['openId']
    nickname = json_data['nick']
    email = '%s@virtual.com' % hashlib.md5(nickname.encode('utf-8')).hexdigest()

    if not Users.get_user_by_email(email):
        await signup(
            request,
            response,
            SignupForm(
                email=email, password='%s_@password' % open_id, name=nickname
            ),
            dingding=True,
        )
    user = Auths.authenticate_user(email, '%s_@password' % open_id)

    if user:
        expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN)
        expires_at = None
        if expires_delta:
            expires_at = int(time.time()) + int(expires_delta.total_seconds())

        token = create_token(
            data={"id": user.id},
            expires_delta=expires_delta,
        )

        datetime_expires_at = (
            datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc)
            if expires_at
            else None
        )
        # Set the cookie token
        response.set_cookie(
            key="token",
            value=token,
            expires=datetime_expires_at,
            httponly=True,  # Ensures the cookie is not accessible via JavaScript
            samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
            secure=WEBUI_AUTH_COOKIE_SECURE,
        )

        user_permissions = get_permissions(
            user.id, request.app.state.config.USER_PERMISSIONS
        )

        return """登录成功, 正在跳转...
                    <script>
                        window.localStorage.setItem("token", "%s");
                        setTimeout(function(){
                            document.location.href = '/';
                        }, 1000);
                    </script>
                    """ % token
    else:
        raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)

好了。后端文件的修改就完成了,接下来是前端代码,本着能偷懒就不多干的原则,这里我就没有直接修改ui源码,然后进行编译,我是直接修改的 /app/build/index.html 文件,直接做了个循环,主打一个能用就行

/app/build/index.html 文件底部追加如下内容

<script>
t = setInterval(function(){
    if(location.href.indexOf('/auth') > -1 && document.querySelectorAll("#dingding_div").length == 0 && 
      document.querySelectorAll("button").length > 0){
        let dingdingDiv = document.createElement("div");
        dingdingDiv.id = 'dingding_div';
        dingdingDiv.innerHTML = "<a href='https://login.dingtalk.com/oauth2/challenge.htm?client_id=自行替换&response_type=code&scope=corpid&prompt=consent&redirect_uri=https://自行替换.com/api/v1/auths/signin_dingding'>使用钉钉登录</a>"
        document.querySelectorAll("button")[document.querySelectorAll("button").length - 1].parentElement.appendChild(dingdingDiv);
    }
}, 100);
</script>

显示效果如图所示,最后在钉钉后台设置回调地址为 https://xxxx.com/api/v1/auths/signin_dingding, 完成!

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放