简介
本架构设计文档面向 DouPHP 社交登录系统,聚焦统一抽象层、OAuth2.0 授权码流程、令牌与用户信息同步、账号绑定与合并、插件化扩展与安全加固。通过 Provider 接口规范与 Service 层封装,将不同第三方平台(如 Google、QQ、微信)的差异化实现收敛到一致的接入面;通过 SnsLoginService 统一落地策略,完成“已关联直接登录 / 已登录用户绑定 / 未登录自动注册 / 引导绑定”的多分支处理;结合安全中间件与配置项,保障会话、CSRF、XSS 等基础安全能力。
项目结构
社交登录相关代码主要分布在以下位置:
- 插件层:各平台的 Provider 与 Service 实现(如 Google、QQ、微信),负责协议细节与外部交互。
- 核心服务层:SnsLoginService 提供统一的 SNS 身份解析与落地策略。
- 插件基础设施:ConnectPluginRegistry 扫描并发现 Connect 类插件,按约定加载 Provider。
- API 端:小程序微信登录控制器,处理 code 换 session、手机号解密与绑定。
- 安全与中间件:安全响应头、CSRF、API 认证等中间件与配置项。
graph TB
subgraph "插件层"
G["GoogleService"]
Q["QqService"]
WX["WxloginService"]
end
subgraph "核心服务"
SNS["SnsLoginService"]
end
subgraph "插件基础设施"
REG["ConnectPluginRegistry"]
end
subgraph "API端"
WXAPI["WeixinController"]
end
subgraph "安全与中间件"
SEC["SecurityHeadersMiddleware"]
CSRF["CsrfMiddleware"]
AUTH["UserAuthMiddleware"]
CFG["security.php"]
end
REG --> G
REG --> Q
REG --> WX
G --> SNS
Q --> SNS
WX --> SNS
WXAPI --> SNS
SEC --> CFG
CSRF --> CFG
AUTH --> CFG
核心组件
- 统一抽象层(Provider 接口)
- 所有社交登录插件需实现连接型 Provider 接口,暴露 pluginId、meta、start、finish 四个方法,使框架以统一方式发现与调用。
- 示例:微信登录 Provider 声明插件 ID、元数据与配置字段,并将 start/finish 委托给具体 Service。
- 平台服务层(Service)
- 每个平台一个 Service,负责 OAuth2.0 授权码流程:生成 state、构造授权 URL、回跳校验 state、code 换 token、获取用户信息、标准化为内部 sns 数据结构。
- 示例:GoogleService 使用 OpenID Connect scope,state 防 CSRF,token 与 userinfo 请求,最终调用 SnsLoginService 落地。
- 用户信息标准化与落地(SnsLoginService)
- 接收标准化后的 sns 数据(group/apptype/openid/unionid/nickname/avatar/sex),根据是否已关联、当前是否已登录、是否允许 nobind 等条件,决定直接登录、引导绑定或自动注册。
- 写入 user_sns 表建立三方标识与本地用户的关联。
- 插件注册与发现(ConnectPluginRegistry)
- 扫描插件目录下的 manifest.php,自动发现并缓存 Provider 实例,支持 has() 查询与 provider() 获取。
- API 端微信登录(WeixinController)
- 小程序端通过 code 换取 session_key 与 openid/unionid,进行绑定或自动注册,并维护 user_sns 关联。
架构总览
社交登录采用“插件 Provider + 平台 Service + 统一协调器”的分层架构:
- 前端/客户端触发登录,进入对应插件的 Provider.start,返回第三方授权页 URL。
- 用户在第三方平台授权后回调至 Provider.finish,Service 完成 state 校验、code 换 token、获取用户信息。
- 标准化后的 sns 数据交由 SnsLoginService.resolve 决策:已关联则登录;已登录用户则绑定;未登录且允许 nobind 则自动注册;否则跳转绑定页。
- 安全中间件在边界处提供 CSRF、安全头、限流与会话硬化等防护。
sequenceDiagram
participant U as "用户"
participant P as "插件Provider"
participant S as "平台Service"
participant T as "第三方平台"
participant C as "SnsLoginService"
participant A as "认证/会话"
U->>P : 发起登录
P->>S : start(ConnectStartRequest)
S-->>U : 返回授权页URL
U->>T : 授权
T-->>P : 回调 finish(ConnectCallbackPayload)
P->>S : finish(payload)
S->>T : 用code换access_token
T-->>S : 返回token
S->>T : 获取用户信息
T-->>S : 返回userinfo
S->>C : resolve(sns, userProfile, mode, nobind)
C-->>A : 已关联 -> 写登录态
C-->>A : 已登录 -> 绑定user_sns
C-->>A : 未登录+nobind -> 自动注册并登录
C-->>U : 未登录+非nobind -> 跳转绑定页
详细组件分析
统一抽象层:Connect 插件 Provider 接口
- 职责
- 声明插件 ID、元信息与配置项,供管理后台展示与编辑。
- 对外暴露 start/finish 两个入口,分别用于发起授权与处理回调。
- 关键点
- meta 中的 config 字段定义可配置项(如 appid/appsecret)。
- start/finish 通常委托给具体 Service,保持 Provider 薄壳化。
- 示例
- 微信登录 Provider 定义了开放平台与公众号两套 appid/appsecret 配置,并通过 service 委派逻辑。
classDiagram
class ConnectPluginProviderInterface {
+pluginId() string
+meta() array
+start(request) string
+finish(payload) string
}
class WxloginProvider {
-service : WxloginService
+pluginId() string
+meta() array
+start(request) string
+finish(payload) string
}
ConnectPluginProviderInterface <|.. WxloginProvider
平台服务层:Google OAuth2.0/OpenID Connect 实现
- 流程
- start:生成 state 存入 Session,构造授权 URL(含 scope 与 prompt)。
- finish:校验 state 防 CSRF;用 code 换 access_token;获取 userinfo;标准化为 sns;调用 SnsLoginService.resolve。
- 安全要点
- state 一致性校验,拒绝非法操作。
- 对昵称等用户输入进行非法字符过滤。
- 数据映射
- 将 sub 作为 openid,nickname/picture 映射为 nickname/avatar,性别默认 0。
flowchart TD
Start(["开始"]) --> GenState["生成state并存储"]
GenState --> BuildUrl["构造授权URL"]
BuildUrl --> Redirect["跳转到第三方授权页"]
Redirect --> Callback{"回调finish"}
Callback --> VerifyState{"state匹配?"}
VerifyState -- 否 --> Error["抛出异常并返回用户页"]
VerifyState -- 是 --> FetchToken["用code换access_token"]
FetchToken --> TokenOk{"token获取成功?"}
TokenOk -- 否 --> Error
TokenOk -- 是 --> FetchUserinfo["获取用户信息"]
FetchUserinfo --> UserOk{"用户信息有效?"}
UserOk -- 否 --> Error
UserOk -- 是 --> Normalize["标准化sns数据"]
Normalize --> Resolve["调用SnsLoginService.resolve"]
Resolve --> End(["结束"])
用户信息标准化与账号绑定:SnsLoginService
- 输入
- sns:包含 group/apptype/openid/unionid/nickname/avatar/sex。
- userProfile:当前已登录用户(可为空)。
- loginIdMode:openid 或 unionid。
- nobind:是否允许未登录用户直接注册。
- 决策流程
- 若 openid/unionid 已关联本地用户:写登录态并跳转用户中心。
- 若当前已登录但未绑定:写入或更新 user_sns 记录,跳转用户 SNS 管理页。
- 若未登录且 nobind=true:自动注册新会员并登录。
- 否则:将 sns 数据写入 Session,跳转绑定页。
- 数据持久化
- 写入 user_sns 表,包含 user_id/group/openid/unionid/apptype/created_at。
flowchart TD
In(["resolve入口"]) --> Normalize["补齐sns默认值"]
Normalize --> Lookup{"查找已关联用户"}
Lookup --> |找到| Login["写登录态并跳转用户中心"]
Lookup --> |未找到| CheckProfile{"当前已登录?"}
CheckProfile --> |是| BindCheck{"是否已绑定该sns?"}
BindCheck --> |是| Update["更新user_sns记录"]
BindCheck --> |否| Insert["插入user_sns记录"]
Update --> ToSnsPage["跳转用户SNS页面"]
Insert --> ToSnsPage
CheckProfile --> |否| Nobind{"nobind=true?"}
Nobind --> |是| AutoReg["自动注册并登录"]
Nobind --> |否| SessionSet["sns写入Session"]
SessionSet --> ToLink["跳转绑定页"]
AutoReg --> End(["结束"])
ToSnsPage --> End
ToLink --> End
API 端微信小程序登录:WeixinController
- 流程
- 校验 IP 限流;从配置读取小程序 appid/appsecret;用 code 换取 openid/unionid/session_key。
- 若 unionid 缺失且模式要求 unionid,则报错。
- 根据 openid/unionid 查找已关联用户;若存在且未锁定,则登录;否则走绑定或自动注册。
- 写入 user_sns 并更新登录次数。
- 与 PC 端差异
- 小程序端使用 jscode2session 流程,不等同于 PC 端插件 OAuth 流程。
sequenceDiagram
participant M as "小程序"
participant C as "WeixinController"
participant W as "微信API"
participant DB as "数据库"
participant SNS as "SnsLoginService"
M->>C : 提交code/rawData/iv/encryptedData
C->>W : jscode2session(code,appid,secret)
W-->>C : 返回openid/unionid/session_key
C->>DB : 查询user_sns关联
alt 已关联
C->>C : 检查账户锁定状态
C->>C : 登录并返回结果
else 未关联
C->>SNS : 绑定或自动注册
SNS-->>C : 返回结果
C->>DB : 写入user_sns并更新登录次数
end
C-->>M : 返回bind/用户信息
插件注册与发现:ConnectPluginRegistry
- 功能
- 扫描 PLUGIN_PATH 下各插件目录的 manifest.php,构建 Provider 类名映射。
- 提供 has(pluginId) 判断是否支持某登录插件;provider(pluginId) 获取实例。
- 通过容器创建 Provider 实例,并缓存以避免重复实例化。
- 意义
- 解耦平台实现与框架调度,新增平台只需遵循接口与清单即可被自动发现。
依赖关系分析
- 松耦合
- Provider 仅依赖接口与 Service,不感知具体业务细节。
- Service 依赖 SnsLoginService 进行统一落地,避免在各平台重复实现绑定逻辑。
- 关键依赖链
- ConnectPluginRegistry -> Provider -> Service -> SnsLoginService -> 认证/会话/数据库。
- API 端 WeixinController -> 微信API -> 数据库 -> SnsLoginService。
- 潜在风险
- 若 SnsLoginService 变更,需评估对多平台的影响。
- 第三方 API 超时或失败时,应做好重试与降级策略。
graph LR
REG["ConnectPluginRegistry"] --> PROV["Provider"]
PROV --> SVC["Platform Service"]
SVC --> SNS["SnsLoginService"]
SNS --> AUTH["认证/会话"]
SNS --> DB["数据库(user/user_sns)"]
API["WeixinController"] --> WXAPI["微信API"]
API --> DB
API --> SNS
性能考虑
- 网络请求优化
- 第三方 API(token、userinfo)建议使用连接池与超时控制,避免阻塞主线程。
- 缓存策略
- 可将用户信息短期缓存(如基于 openid/unionid 的只读缓存),减少重复请求。
- 数据库访问
- 批量写入 user_sns 时注意事务与索引,避免锁竞争。
- 限流与退避
- 对频繁登录尝试进行限流(IP 级),防止暴力破解与资源耗尽。
故障排查指南
- 常见错误与定位
- state 不匹配:检查 Session 中保存的 state 与回调参数是否一致,确认跨域与 Cookie 设置。
- code 无效或过期:确认授权页 redirect_uri 与第三方平台配置一致,检查网络连通性。
- 无法获取用户信息:检查 access_token 权限范围(scope)与有效期。
- 绑定失败:核对 user_sns 唯一约束与 group/apptype 维度,确保无冲突。
- 安全相关
- CSRF 防护:后台表单提交需携带 CSRF 令牌,由 CsrfMiddleware 校验。
- 安全头:通过 SecurityHeadersMiddleware 下发 X-Frame-Options、Referrer-Policy 等。
- 会话硬化:依据 security.php 配置 httponly、secure、samesite、use_strict_mode。
- 日志与审计
- 登录失败与异常路径建议记录审计日志,便于追踪问题根因。
结论
DouPHP 社交登录系统通过 Provider 接口与 Service 分层实现了多平台的统一接入;SnsLoginService 提供了清晰、可扩展的账号绑定与落地策略;ConnectPluginRegistry 使得新平台接入仅需遵循约定即可被自动发现;安全中间件与配置项保障了会话、CSRF、XSS 等基础安全能力。整体架构具备高内聚、低耦合、易扩展的特点,适合持续集成更多第三方平台与自定义用户字段、权限映射等扩展需求。
附录
- 扩展机制建议
- 新平台接入:实现 ConnectPluginProviderInterface,编写 manifest.php 与 Service,复用 SnsLoginService 流程。
- 自定义用户字段:在 autoRegister 或绑定流程中扩展用户资料映射,注意字段合法性校验。
- 权限映射:可在登录后根据第三方角色/组映射到本地权限体系,建议在认证完成后注入。
- 参考实现路径
- 插件 Provider:WxloginProvider.php
- 平台 Service:GoogleService.php、QqProvider.php
- 统一落地:SnsLoginService.php
- API 端微信登录:WeixinController.php
- 安全配置与中间件:security.php、SecurityHeadersMiddleware.php、CsrfMiddleware.php、UserAuthMiddleware.php