简介
本技术文档聚焦 DouPHP 前台认证中间件,围绕 FrontendUserAuthMiddleware(即 front/middleware/UserAuthMiddleware)对 AbstractUserAuthMiddleware 的具体实现展开,系统说明:
- 前台会话管理、用户状态解析与重定向处理机制
- 登录状态验证流程:Cookie/session 检查、用户信息提取与身份注入
- 前台白名单配置:哪些页面允许匿名访问(注册、登录、商品浏览等)
- 未登录时的重定向逻辑:跳转登录页、返回 URL 保存与错误提示
- 配置选项与使用示例:自定义登录页、重定向规则、错误处理
- 前台与 API 端认证差异及拒绝响应方式
- 多端会话同步最佳实践:Web 端与小程序端一致性
项目结构
前台认证由“策略 + 基类 + 前端实现”三层构成:
- 策略层:UserAuthPolicy 根据路由段与配置表决定鉴权模式(public/optional/required)以及是否需要工作端身份。
- 基类层:AbstractUserAuthMiddleware 提供模板方法,统一处理请求解析、策略决策、三态分支、身份注入与工作端校验。
- 前端实现层:UserAuthMiddleware 指定前台 guard(auth('front'))、配置文件路径与拒绝响应(重定向或 JSON)。
graph TB
A["请求进入"] --> B["AbstractUserAuthMiddleware.handle()"]
B --> C["UserAuthPolicy.resolve()"]
C --> D{"mode: public/optional/required"}
D --> |public| E["直接放行"]
D --> |optional/required| F["resolveContext() -> auth('front')->resolveUserContext()"]
F --> G{"是否已登录"}
G --> |否 & required| H["rejectUnauthenticated() 重定向/JSON"]
G --> |是| I["inject() -> auth('front')->hydrate()"]
I --> J{"work_required?"}
J --> |是 且无工作端| K["rejectForbidden() 重定向到用户中心"]
J --> |否| L["放行到控制器"]
核心组件
- UserAuthMiddleware(前台实现)
- 指定配置文件路径为前台中间件配置
- 通过 auth('front') 解析上下文并注入身份缓存
- 未登录时:XHR from=js 返回 JSON 401 并附带 jump_url;普通页面重定向到登录页并携带 redirect 参数
- 无工作端权限时:重定向到用户中心
- AbstractUserAuthMiddleware(通用骨架)
- 加载前台鉴权配置(auth_modes、work_required)
- 基于路由段与策略表决策 mode 与 workRequired
- 三态分支:public 直达;optional 失败放行;required 失败拒绝
- 注入身份后按 work_required 决定是否要求工作端身份
- UserAuthPolicy(策略解析器)
- 构建候选键(精确 > 父段 > 模块根),从配置表中匹配最细粒度模式
- 默认模式 optional,确保新增模块默认尝试恢复登录态但不拦截
- Auth Guard(front/facade/Auth.php)
- resolveUserContext:优先从 Session 恢复,否则尝试 remember token
- verifySession/matchSession:校验 session shell 与数据库一致
- hydrate:将上下文写入当前实例的 userId、userProfile、workId 等
- touchSession:刷新会话心跳,过期则清理
- UserAuthService(会话与凭证)
- 前台登录成功:生成 shell、写入 user_id/shell/ontime/field,并生成静态 CSRF 令牌
- remember token:签发并设置 DOU_TOKEN Cookie,用于后续自动恢复
架构总览
下图展示一次受保护的前台请求在中间件链中的完整流转:
sequenceDiagram
participant Client as "浏览器"
participant MW as "UserAuthMiddleware"
participant Base as "AbstractUserAuthMiddleware"
participant Policy as "UserAuthPolicy"
participant Guard as "Auth(front)"
participant Service as "UserAuthService"
participant Ctrl as "控制器"
Client->>MW : 发起受保护请求
MW->>Base : handle(next)
Base->>Policy : resolve(module, action, sub, parent, auth_modes, work_required)
Policy-->>Base : {mode, workRequired}
alt mode == public
Base-->>Ctrl : 放行
else mode != public
Base->>MW : resolveContext()
MW->>Guard : resolveUserContext()
Guard->>Service : 读取/恢复 session 或 remember token
Service-->>Guard : 用户上下文
Guard-->>MW : {ok, userId, userProfile, workId...}
alt ok == false 且 mode == required
MW->>MW : rejectUnauthenticated()
MW-->>Client : 重定向登录页 / JSON 401
else ok == true
MW->>Guard : hydrate(context)
alt workRequired && !hasWorkIdentity
MW->>MW : rejectForbidden()
MW-->>Client : 重定向用户中心
else 通过
Base-->>Ctrl : 放行
end
end
end
详细组件分析
组件A:前台认证中间件(UserAuthMiddleware)
职责边界
- 指定前台配置文件路径
- 通过 auth('front') 解析上下文并注入身份缓存
- 未登录拒绝:XHR from=js 返回 JSON 401 并附带 jump_url;普通页面重定向到登录页并附加 redirect
- 无工作端权限:重定向到用户中心
关键流程
- resolveContext:调用 auth('front')->resolveUserContext() 获取用户上下文
- inject:调用 auth('front')->hydrate(context) 注入身份缓存
- hasWorkIdentity:判断当前用户是否具备有效工作端身份
- rejectUnauthenticated:区分 XHR 与普通请求,分别返回 JSON 或重定向
- rejectForbidden:无工作端身份时重定向到用户中心
flowchart TD
Start(["进入 rejectUnauthenticated"]) --> CheckXHR{"from === 'js' ?"}
CheckXHR --> |是| ThrowJson["抛出 ApiResponse 401<br/>包含 jump_url"]
CheckXHR --> |否| BuildRedirect["构造登录页URL<br/>拼接 redirect=当前URL"]
BuildRedirect --> Redirect["抛出 HttpResponseException(redirect)"]
ThrowJson --> End(["结束"])
Redirect --> End
组件B:通用鉴权骨架(AbstractUserAuthMiddleware)
职责边界
- 加载前台鉴权配置(auth_modes、work_required)
- 基于路由段与策略表决策 mode 与 workRequired
- 三态分支:public 直达;optional 失败放行;required 失败拒绝
- 注入身份后按 work_required 决定是否要求工作端身份
关键流程
- __construct:加载配置文件
- handle:解析路由段、调用策略、执行三态分支、注入身份、检查工作端、放行
classDiagram
class AbstractUserAuthMiddleware {
-array $authModes
-array $workRequired
-string|null $routeModeOverride
+setRouteParameters(params) void
+handle(next) mixed
#configFile() string
#resolveContext() array
#inject(context) void
#hasWorkIdentity() bool
#rejectUnauthenticated() void
#rejectForbidden() void
}
组件C:鉴权策略(UserAuthPolicy)
职责边界
- 根据 module/action/sub/parent 构建候选键列表
- 从配置表匹配最细粒度的模式(public/optional/required)
- 判定是否命中 work_required
关键流程
- buildCandidates:精确 > 父段 > 模块根
- resolve:遍历候选键,取首个合法模式;同时判定 workRequired
flowchart TD
S(["开始"]) --> C1["构建候选键列表"]
C1 --> M{"匹配到模式?"}
M --> |是| SetMode["设置 mode = 匹配值"]
M --> |否| DefaultMode["mode = optional"]
SetMode --> W{"命中 work_required?"}
DefaultMode --> W
W --> |是| WorkTrue["workRequired = true"]
W --> |否| WorkFalse["workRequired = false"]
WorkTrue --> E(["结束"])
WorkFalse --> E
组件D:前台 Guard(Auth)与会话管理
职责边界
- resolveUserContext:优先从 Session 恢复,否则尝试 remember token
- verifySession/matchSession:校验 session shell 与数据库一致
- hydrate:将上下文写入当前实例的 userId、userProfile、workId 等
- touchSession:刷新会话心跳,过期则清理
关键流程
- 登录成功:UserAuthService 写入 session(user_id/shell/ontime/field)并生成静态 CSRF 令牌
- 自动恢复:若 Session 缺失,尝试从 DOU_TOKEN Cookie 恢复
- 心跳刷新:每次访问更新 ontime,超过阈值则清理会话
sequenceDiagram
participant Guard as "Auth(front)"
participant Service as "UserAuthService"
participant DB as "数据库"
participant Session as "Session"
Note over Guard : 解析用户上下文
Guard->>Session : 读取 user_id/shell/ontime/field
alt 会话有效
Guard->>DB : matchSession(userId, shell)
DB-->>Guard : 用户行
Guard->>Session : touchSession()
else 会话无效
Guard->>Service : restoreFromToken()
Service-->>Guard : 是否恢复成功
alt 恢复成功
Guard->>Session : 重新写入会话
else 恢复失败
Guard-->>Guard : 返回空上下文
end
end
组件E:前台白名单与重定向规则
- 白名单配置位置:front/init/middleware.php
- 模式说明:
- public:匿名可访问,且不尝试 Session 登录态
- optional:尝试登录态,失败不拦截(默认兜底)
- required:必须登录
- 典型公开页面:
- 登录/注册/找回密码/验证码相关:user/login、user/register、user/password_reset、user/verification
- 预约公开页面:book/default、book/index、book/list、book/class、book/show、book/schedule、book/date、book/time
- 插件回调:plugin/notify、plugin/finish、plugin/status
- 公共浏览模块:article、brand、cases、product、service 等
- 重定向规则:
- 未登录且 required:普通页面重定向到登录页,并附加 redirect=当前URL;XHR from=js 返回 JSON 401 并附带 jump_url
- 无工作端权限:重定向到用户中心
依赖关系分析
- UserAuthMiddleware 依赖:
- AbstractUserAuthMiddleware:继承复用通用骨架
- UserAuthPolicy:策略决策
- Auth(front):会话解析与身份注入
- UserAuthService:会话写入与 remember token
- 配置依赖:
- front/init/middleware.php:auth_modes 与 work_required 定义
graph LR
UAM["UserAuthMiddleware"] --> AUAM["AbstractUserAuthMiddleware"]
UAM --> AP["UserAuthPolicy"]
UAM --> AG["Auth(front)"]
AG --> UA["UserAuthService"]
AUAM --> CFG["front/init/middleware.php"]
性能考虑
- 策略匹配开销低:仅字符串比较与数组查找,时间复杂度近似 O(1)
- 会话校验仅在 optional/required 分支执行,public 分支零开销
- remember token 恢复仅在 Session 缺失时触发,避免多余数据库查询
- 心跳刷新按需进行,避免频繁写 Session
- 建议:
- 合理划分 public/optional/required,减少不必要的登录态解析
- 对高频只读页面尽量设为 optional,降低强制登录带来的额外开销
- 注意会话超时阈值,避免频繁清理导致用户体验下降
故障排查指南
常见问题与定位要点
- 未登录仍被重定向:
- 检查路由是否在 auth_modes 中声明为 required
- 确认 XHR 是否携带 from=js 以期望 JSON 401
- 登录后仍显示未登录:
- 检查 Session 是否写入 user_id/shell/ontime/field
- 核对 shell 计算是否与 UserAuthService 一致
- 检查 remember token 是否过期或失效
- 工作端权限不足:
- 检查 work_required 配置是否命中该路由
- 确认 hasWorkIdentity 判断条件(workId > 0)
- 会话过期:
- 检查 touchSession 是否按时刷新 ontime
- 确认超时阈值与业务预期一致
结论
前台认证中间件通过策略化配置与模板方法实现了高内聚、低耦合的鉴权体系:
- 策略层清晰界定 public/optional/required,便于安全治理与扩展
- 基类统一处理三态分支与工作端校验,子类仅需关注前台特有行为
- Guard 与会话服务解耦,支持 remember token 与 Session 双通道恢复
- 拒绝响应区分 Web 与 XHR,提升前后端交互体验
- 通过白名单与重定向规则,既保障安全又兼顾可用性
附录
配置选项与使用示例
- 配置文件:front/init/middleware.php
- 主要选项:
- auth_modes:按 module/sub/action 维度声明 public/optional/required
- work_required:命中后需具备工作端身份
- 使用示例:
- 新增需登录模块:在 auth_modes 中登记为 required
- 新增公开页面:在 auth_modes 中登记为 public
- 需要工作端权限:在 work_required 中添加对应路由键
前台与 API 端认证差异
- 前台:基于 Session + remember token,拒绝时重定向或返回 JSON 401(XHR)
- API:无状态 token 鉴权,拒绝时返回 JSON 错误码
- 原因:
- 前台为浏览器环境,适合重定向与页面级跳转
- API 为客户端/小程序调用,需结构化 JSON 响应以便前端处理
多端会话同步最佳实践
- 统一凭据来源:前台与小程序均通过同一认证入口(如手机号+验证码)完成登录
- 会话存储:
- 前台:Session + remember token(DOU_TOKEN)
- 小程序:使用 API token(由 UserAuthService 签发)
- 同步策略:
- 登录成功后,前台写入 Session,小程序侧保存 API token
- 跨端操作通过 API 调用,保证状态一致性
- 注意事项:
- 避免在同一会话中混用不同凭据类型
- 登出时清除所有端凭据(Session、Cookie、API token)