一文吃透Dify账户系统:多租户 + 多登录方式 + 权限模型全揭底

 

之前写了篇10分钟搞定企业级登录!dify无缝集成LDAP实战指南,当时就是几个方法对接了LDAP,后来想了下对接规范吗?

本篇通过源码来释疑下。

实体关系模型

dify的账户系统,直接看api/models/account.py就行了,里面包含了相关的实体

1

1

1

1

1

0..*

0..*

0..*

0..*

1

Account

+String id

+String name

+String email

+String password

+String status

+DateTime last_login_at

+String last_login_ip

+is_admin_or_owner()

+is_editor()

+is_dataset_editor()

Tenant

+String id

+String name

+String status

+String plan

+get_accounts()

TenantAccountJoin

+String id

+String tenant_id

+String account_id

+String role

+Boolean current

AccountIntegrate

+String id

+String account_id

+String provider

+String open_id

InvitationCode

+Integer id

+String code

+String status

+String used_by_tenant_id

TenantPluginPermission

+String id

+String tenant_id

+String install_permission

+String debug_permission

我们看下每个实体的作用:

  • • Account:Dify的系统用户,包含基本信息和权限判断方法
  • • Tenant:在Dify中叫workspace(工作空间),支持多租户架构
  • • TenantAccountJoin:表示账户与租户的关联关系,定义角色权限
  • • AccountIntegrate:表示第三方登录集成,支持OAuth等外部认证
  • • InvitationCode:表示邀请加入租户的邀请码和邀请记录
  • • TenantPluginPermission:表示租户内插件的权限设置

看这些模型就知道了,Dify这个账户体系还是挺复杂的。

角色这块分了OWNER/ADMIN/EDITOR/NORMAL/DATASET_OPERATOR这些,DATASET_OPERATOR这个角色还挺有意思。账户状态有PENDING(待激活)ACTIVE(正常)BANNED(封号)这些。

OAuth第三方登录也支持,通过AccountIntegrate这个表。邀请机制用InvitationCode,应该就是邀请码功能把

我们看下用户状态和租户状态的流转关系:

注册

激活

初始化完成

违规操作

解封

用户注销

PENDING
UNINITIALIZED
ACTIVE
BANNED
CLOSED
Tenant状态

归档

恢复

NORMAL
ARCHIVE

具体怎么流转的,得结合前端页面操作才能看明白。

账户管理

账户管理这块,主要看这几个文件:

  • • oauth.py: OAuth第三方登录(GitHub/Google)那套
  • • login.py: 普通登录、登出、密码重置
  • • account.py: 账户模型和状态定义
  • • account_service.py: 账户业务逻辑实现

这几个文件中类的关系如下:

使用

使用

使用

使用

使用

使用

使用

操作

操作

生成

操作

使用

使用

OAuthLogin

+get(provider: str) : : redirect

OAuthCallback

+get(provider: str) : : redirect

LoginApi

+post() : : JSON

LogoutApi

+get() : : JSON

ResetPasswordSendEmailApi

+post() : : JSON

EmailCodeLoginApi

+post() : : JSON

AccountService

+authenticate(email, password) : : Account

+authenticate_ldap(email, password) : : Account

+login(account, ip_address) : : TokenPair

+logout(account)

+create_account(email, name, password) : : Account

+update_account_password(account, password, new_password)

+send_reset_password_email(account|email)

TenantService

+create_tenant(name) : : Tenant

+get_join_tenants(account) : : Tenant[]

+create_tenant_member(tenant, account, role)

RegisterService

+register(email, name, password) : : Account

+invite_new_member(tenant, email, role) : : token

Account

+id: StringUUID

+name: String

+email: String

+status: AccountStatus

+last_login_at: DateTime

+get_status() : : AccountStatus

+is_admin_or_owner() : : bool

Tenant

+id: StringUUID

+name: String

+status: TenantStatus

+get_accounts() : : Account[]

TokenPair

+access_token: String

+refresh_token: String

注册与登录

普通登录与注册

注册流程

dify这个注册,要考虑系统配置、工作空间创建这些:

用户注册
是否允许注册
注册失败
创建账户
发送邮件验证
用户激活
是否允许创建工作空间
自动创建工作空间
等待邀请加入
设置为Owner角色
等待管理员分配角色
注册完成

这个流程图看起来还行,基本逻辑是这样:

先检查系统让不让注册(FeatureService.get_system_features().is_allow_register这个方法),能注册就创建账户,有邮件功能的话会发验证邮件。接下来看系统配置决定要不要自动建工作空间,不让建的话用户就的等管理员邀请了

登录流程

登录这块涉及好几个组件,画了个时序图

Redis缓存数据库TenantServiceAccountService控制器用户Redis缓存数据库TenantServiceAccountService控制器用户普通登录流程POST /login (email, password)authenticate(email, password)查询用户账户返回账户信息验证密码get_join_tenants(account)查询用户租户返回租户列表生成JWT Token存储Refresh Token返回TokenPair登录成功,返回Token

先验证邮箱密码,验证通过了就获取用户工作空间列表,接着生成JWT token和refresh token,最后把refresh token存Redis里。

源码里支持好几种登录方式:


# 来源:controllers/console/auth/login.py LoginApi.post() 方法

def post(self):
    # 检查是否启用LDAP(这块代码是我加的)
    if is_ldap_enabled():
        account = AccountService.authenticate_ldap(args["email"], args["password"])
    elif invitation:
        # 邀请注册登录
        account = AccountService.authenticate(args["email"], args["password"], args["invite_token"])
    else:
        # 普通登录
        account = AccountService.authenticate(args["email"], args["password"])

邮箱验证码登录

还有个邮箱验证码登录,不用密码那种,对企业用户来说挺方便的:


# 来源:controllers/console/auth/login.py EmailCodeLoginSendEmailApi.post() 方法
# 发送验证码
token = AccountService.send_email_code_login_email(email=args["email"], language=language)

# 来源:controllers/console/auth/login.py EmailCodeLoginApi.post() 方法
# 验证码登录
token_data = AccountService.get_email_code_login_data(args["token"])
if token_data["code"] == args["code"]:
    # 登录成功,创建或获取账户
    account = AccountService.get_user_through_email(user_email)

第三方登录

OAuth第三方登录也支持,主要是GitHub和Google。流程图如下:

AccountServiceOAuth提供商Dify后端Dify前端用户AccountServiceOAuth提供商Dify后端Dify前端用户点击第三方登录GET /oauth/login/github重定向到GitHub授权页显示授权页面用户授权回调并返回授权码用exchange授权码获取access_token返回access_token使用token获取用户信息返回用户信息创建或关联本地账户返回账户信息重定向到前端并携带token登录成功

流程就是用户在GitHub那边授权,回调回来后用授权码换access_token,拿到用户信息就在本地创建或关联账户。

controllers/console/auth/oauth.py get_oauth_providers()里可以看到支持的提供商:

OAUTH_PROVIDERS = {
    "github": GitHubOAuth(
        client_id=dify_config.GITHUB_CLIENT_ID,
        client_secret=dify_config.GITHUB_CLIENT_SECRET,
        redirect_uri=dify_config.CONSOLE_API_URL + "/console/api/oauth/authorize/github",
    ),

    "google": GoogleOAuth(
        client_id=dify_config.GOOGLE_CLIENT_ID,
        client_secret=dify_config.GOOGLE_CLIENT_SECRET,
        redirect_uri=dify_config.CONSOLE_API_URL + "/console/api/oauth/authorize/google",
    )
}

后面要对接微信、钉钉啥的,都可以在oauth.py里按这个套路来

身份验证

JWT + Refresh Token双token机制,验证流程如下:

有效

无效

过期

ACTIVE

BANNED

PENDING

用户请求
Token验证
解析用户信息
返回401错误
尝试刷新Token
检查账户状态
允许访问
拒绝访问
要求激活
Refresh Token有效
生成新Token
要求重新登录
设置当前租户
加载用户角色
业务逻辑处理

看图知道,这个验证流程几个步骤:

先验证JWT签名和有效性,然后检查用户状态别是BANNED啥的,Access Token过期了就用Refresh Token刷新,最后设置当前租户和角色。

JWT Access Token默认30分钟有效,包含用户ID这些信息。Refresh Token有效期30天,存在Redis里专门用来刷新token

多租户身份切换

用户可以在多个工作空间间切换

# 来源:services/account_service.py TenantService.switch_tenant() 方法
@staticmethod
defswitch_tenant(account: Account, tenant_id: Optional[str] = None) -> None:
    """Switch the current workspace for the account"""
    # 验证用户是否有权限访问该租户
    tenant_account_join = (
        db.session.query(TenantAccountJoin)
        .join(Tenant, TenantAccountJoin.tenant_id == Tenant.id)
        .filter(
            TenantAccountJoin.account_id == account.id,
            TenantAccountJoin.tenant_id == tenant_id,
            Tenant.status == TenantStatus.NORMAL,
        )
        .first()
    )

    ifnot tenant_account_join:
        raise AccountNotLinkTenantError("Tenant not found or account is not a member of the tenant.")
    # 设置当前租户
    db.session.query(TenantAccountJoin).filter(
        TenantAccountJoin.account_id == account.id,
        TenantAccountJoin.tenant_id != tenant_id
    ).update({"current"False})
    tenant_account_join.current = True
    account.set_tenant_id(tenant_account_join.tenant_id)
    db.session.commit()

切换时得验证用户真的是那个租户的成员,还要确保租户状态正常(别是归档状态),最后更新用户当前租户设置

安全措施

密码安全措施

密码存储用的盐值+哈希,肯定不会明文存。每个密码都有自己的盐值,防彩虹表攻击。还有密码强度验证

# 来源:services/account_service.py AccountService.create_account() 方法
# 生成密码哈希
salt = secrets.token_bytes(16)
base64_salt = base64.b64encode(salt).decode()
password_hashed = hash_password(password, salt
base64_password_hashed = base64.b64encode(password_hashed).decode()
account.password = base64_password_hashed
account.password_salt = base64_salt

登录安全措施

多重防护:

  • • 登录错误5次就锁定
  • • 忘记密码也有次数限制,防止被滥用
  • • IP频率限制,防恶意攻击

# 来源:services/account_service.py AccountService类常量定义
LOGIN_MAX_ERROR_LIMITS = 5
FORGOT_PASSWORD_MAX_ERROR_LIMITS = 5
# 来源:services/account_service.py AccountService.is_login_error_rate_limit() 方法
@staticmethod
defis_login_error_rate_limit(email: str) -> bool:
    key = f"login_error_rate_limit:{email}"
    count = redis_client.get(key)
    if count isNone:
        returnFalse
    count = int(count)
    if count > AccountService.LOGIN_MAX_ERROR_LIMITS:
        returnTrue# 触发限制
    return False

邮件安全措施

邮件发送有频率限制,防止邮件轰炸。验证码有时间限制,用一次就失效。

账户状态检查

账户状态检查挺严格的,只有正常状态才能登录:

# 来源:services/account_service.py AccountService.authenticate() 方法
if account.status == AccountStatus.BANNED.value:
    raise AccountLoginError("Account is banned.")

# 来源:services/account_service.py AccountService.load_user() 方法
if account.status == AccountStatus.BANNED.value:
    raise Unauthorized("Account is banned.")

账户状态有这几种:

  • • PENDING:待激活,新注册用户
  • • ACTIVE:正常状态
  • • BANNED:被封号,不能登录
  • • CLOSED:用户注销了

Token安全措施

Token过期机制,Access Token短期有效。退出时清除Refresh Token。一个账户只能有一个有效的Refresh Token

角色权限体系

5种用户角色:

  • • OWNER:拥有者,啥都能干
  • • ADMIN:管理员,管理成员和应用
  • • EDITOR:编辑者,创建编辑应用
  • • NORMAL:普通用户,只能用应用
  • • DATASET_OPERATOR:数据集操作员,专门管数据集

权限检查用Account模型的属性方法:

# 来源:models/account.py Account类的属性方法
@property
defis_admin_or_owner(self):
    """检查是否为管理员或拥有者"""
    return TenantAccountRole.is_privileged_role(self.role)

@property
defis_editor(self):
    """检查是否为编辑者或更高权限"""
    return TenantAccountRole.is_editing_role(self.role)

@property
defis_dataset_editor(self):
    """检查是否可以编辑数据集"""
    return TenantAccountRole.is_dataset_edit_role(self.role)

@property
defis_dataset_operator(self):
    """检查是否为数据集操作员"""
    returnself.role == TenantAccountRole.DATASET_OPERATOR

不同角色权限对比:

功能
OWNER
ADMIN
EDITOR
NORMAL
DATASET_OPERATOR
管理成员
创建应用
编辑应用
使用应用
管理数据集

总结

  • • dify在架构设计上用了多租户,通过租户-账户关联表搞权限管理
  • • 开源版支持密码、邮箱验证码、OAuth这些,能满足不同的登录需求
  • • 通过5个角色控制了权限
  • • 回到最开始的问题,我对接LDAP的方式基本符合Dify规范,不过还能完善下,比如把LDAP用户信息记录到AccountIntegrate表就更完美了

系列文章

uv配置环境

dify相关

DeepSeek+dify 本地知识库:真的太香了
Deepseek+Dify本地知识库相关问题汇总
dify的sandbox机制,安全隔离限制
DeepSeek+dify 本地知识库:高级应用Agent+工作流
DeepSeek+dify知识库,查询数据库的两种方式(api+直连)
DeepSeek+dify 工作流应用,自然语言查询数据库信息并展示
聊聊dify权限验证的三种方案及实现
dify1.0.0版本升级及新功能预览
Dify 1.1.0史诗级更新!新增”灵魂功能”元数据,实测竟藏致命Bug?手把手教你避坑
【避坑血泪史】80次调试!我用Dify爬虫搭建个人知识库全记录
手撕Dify1.x插件报错!从配置到网络到Pip镜像,一条龙排雷实录
dify1.2.0升级,全新循环节点优化,长文写作案例
dify1.x无网环境安装插件
dify项目结构说明与win11本地部署
Dify 深度拆解(二):后端架构设计与启动流程全景图
dify应用:另类的关键词检索
10分钟搞定企业级登录!Dify无缝集成LDAP实战指南
Dify 1.5.0 上线:这次调试功能,真的省了我一半时间
Dify × MCP 实战(一):用插件一分钟搞定MCP Server(含时间踩坑实践)
Dify × MCP 实战(二):发布工作流为 AI 工具服务,全流程配置 + Cherry 调用实战
# Dify × MCP 实战(三):结果别再堆字了!用 AntV 插件打造图表可视化工具
Dify插件实战

ragflow相关

DeepSeek+ragflow构建企业知识库:突然觉的dify不香了(1)
DeepSeek+ragflow构建企业知识库之工作流,突然觉的dify又香了
DeepSeek+ragflow构建企业知识库:高级应用篇,越折腾越觉得ragflow好玩
RAGFlow爬虫组件使用及ragflow vs dify 组件设计对比
从8550秒到608秒!RAGFlow最新版本让知识图谱生成效率狂飙,终于不用通宵等结果了
以为发现的ragflow的宝藏接口,其实是一个天坑、Chrome/Selenium版本地狱
NLTK三重降噪内幕!RAGFlow检索强悍竟是靠这三板斧
从代码逆向RAGFlow架构:藏在18张表里的AI知识库设计哲学
解剖RAGFlow!全网最硬核源码架构解析
深度拆解RAGFlow分片引擎!3大阶段+视觉增强,全网最硬核架构解析
深度拆解RAGFlow分片引擎之切片实现
RAGFlow核心引擎DeepDoc之PDF解析大起底:黑客级PDF解析术与致命漏洞
RAGFlow 0.18.0 实战解读:从 MCP 支持到插件配置的全流程揭秘
ragflow 0.19.0 图文混排功能支持

mcp

上线3周:告警减少70%!AI巡检分级报告实战(一)
MCP不像想象的那么简单,MCP+数据库,rag之外的另一种解决方案
上线3周:告警减少85%!纯AI驱动巡检通知实战(二)无硬编码方案曝光

 


前沿技术新闻资讯

🧠 解码大语言模型的记忆力:上下文长度的前世今生

2025-7-15 16:30:54

前沿技术新闻资讯

2025年下半年,AI技术将何去何从?

2025-7-15 17:15:07

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
购物车
优惠劵
搜索