微信扫码
添加专属顾问
我要投稿
深入解析Dify账户系统的设计精髓,揭秘多租户架构与权限控制的实现逻辑。 核心内容: 1. Dify账户系统的核心实体模型与关系解析 2. 多租户架构下的角色权限划分与状态流转 3. 第三方登录集成与邀请机制的技术实现
之前写了篇10分钟搞定企业级登录!dify无缝集成LDAP实战指南,当时就是几个方法对接了LDAP,后来想了下对接规范吗?
本篇通过源码来释疑下。
dify的账户系统,直接看api/models/account.py
就行了,里面包含了相关的实体
111110..*0..*0..*0..*1Account+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 currentAccountIntegrate+String id+String account_id+String provider+String open_idInvitationCode+Integer id+String code+String status+String used_by_tenant_idTenantPluginPermission+String id+String tenant_id+String install_permission+String debug_permission
我们看下每个实体的作用:
看这些模型就知道了,Dify这个账户体系还是挺复杂的。
角色这块分了OWNER/ADMIN/EDITOR/NORMAL/DATASET_OPERATOR这些,DATASET_OPERATOR这个角色还挺有意思。账户状态有PENDING(待激活)ACTIVE(正常)BANNED(封号)这些。
OAuth第三方登录也支持,通过AccountIntegrate这个表。邀请机制用InvitationCode,应该就是邀请码功能把
我们看下用户状态和租户状态的流转关系:
注册激活初始化完成违规操作解封用户注销PENDINGUNINITIALIZEDACTIVEBANNEDCLOSEDTenant状态归档恢复NORMALARCHIVE
具体怎么流转的,得结合前端页面操作才能看明白。
账户管理这块,主要看这几个文件:
这几个文件中类的关系如下:
使用使用使用使用使用使用使用操作操作生成操作使用使用OAuthLogin+get(provider: str) : : redirectOAuthCallback+get(provider: str) : : redirectLoginApi+post() : : JSONLogoutApi+get() : : JSONResetPasswordSendEmailApi+post() : : JSONEmailCodeLoginApi+post() : : JSONAccountService+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) : : tokenAccount+id: StringUUID+name: String+email: String+status: AccountStatus+last_login_at: DateTime+get_status() : : AccountStatus+is_admin_or_owner() : : boolTenant+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机制,验证流程如下:
有效无效过期ACTIVEBANNEDPENDING是否用户请求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
多重防护:
# 来源: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.")
账户状态有这几种:
Token过期机制,Access Token短期有效。退出时清除Refresh Token。一个账户只能有一个有效的Refresh Token
5种用户角色:
权限检查用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
不同角色权限对比:
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费场景POC验证,效果验证后签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2025-05-29
2025-05-23
2025-05-07
2025-04-29
2025-05-07
2025-04-29
2025-05-07
2025-06-01
2025-06-07
2025-05-07