Angular 路由守卫(Route Guards)完全指南:用 CanActivate、CanActivateChild、CanDeactivate、CanMatch 精细管控路由访问
路由守卫是一组在路由导航前后执行的"检查点"函数,用来决定"用户能否进入/离开某个路由"以及"某个路由配置是否参与匹配"。本文以 Angular 官方路由文档(对应仓库文件 route-guards.md)为主体,结合
packages/router中 Router 的源码实现与测试用例,系统讲解守卫的创建方式、四种守卫类型的参数与典型场景、可在守卫中返回的取值,以及在路由配置中的挂载顺序与执行原理。读完本文,你将能在自己的 Angular 应用中用函数式守卫实现登录鉴权、角色授权、未保存表单拦截、功能开关路由分流等能力,并理解守卫在导航流水线中的真实执行机制。
关键安全提醒(原文首要警告):绝不应当把客户端守卫当作访问控制的唯一手段。任何运行在浏览器中的 JavaScript 都可能被用户修改。必须在服务端强制执行用户授权,客户端守卫只能作为交互层的补充体验,不能替代服务端校验。
守卫是什么:导航路线上的"关卡检查"
路由守卫(Route Guard)本质上是控制用户能否导航到某个路由、或能否离开当前路由的函数。它们就像是设置在路由图上的检查站:
- 用户试图进入受保护区域时,守卫判断是否放行;
- 用户试图离开一个正在进行关键操作的页面(例如表单还没保存)时,守卫判断是否拦截。
最常见的使用场景就是身份认证(authentication)与访问控制(authorization)。除此之外,守卫还可以承担功能开关、A/B 实验分流、条件加载等职责。Router 在每次导航过程的目标路由树确认之后、真正激活组件之前,会集中执行这些守卫;只有当所有需要执行的守卫都返回"允许"(true),本次导航才会继续,见 check_guards.ts 中的 checkGuards 流水线。
创建路由守卫的两种方式
方式一:使用 Angular CLI 生成
ng generate guard CUSTOM_NAME
执行后,CLI 会交互式地询问要创建哪种类型的路由守卫(对应下文四种类型),随后生成形如 CUSTOM_NAME.guard.ts 的守卫文件(文件名的具体后缀风格取决于 CLI 模板版本与项目配置)。
方式二:手动创建
你也可以在项目中手动新建一个独立的 TypeScript 文件来定义守卫。为了和其他文件区分,官方建议在文件名中加上 -guard 后缀,例如 auth.guard.ts。生成的内容是一个导出常量——函数式守卫,签名形如:
export const authGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
) => {
// ...
};
从源码看,CanActivateFn、CanActivateChildFn、CanDeactivateFn<T>、CanMatchFn 这四种函数类型均以 @publicApi 形式定义在 packages/router/src/models.ts,Router 在运行时支持"类式守卫"(实现 CanActivate 等接口)与"函数式守卫"两种写法:执行时若检测到类式守卫则调用其对应方法,否则在注入上下文中直接调用函数,见 check_guards.ts。对类式守卫,仓库还提供了 mapToCanActivate、mapToCanMatch、mapToCanActivateChild、mapToCanDeactivate 等辅助函数,可将类守卫数组转换为等价的函数守卫数组,见 functional_guards.ts。
守卫的返回值:所有类型统一约定的放行/拦截信号
无论哪一种守卫,都共享同一组可返回的类型。GuardResult 在源码中被定义为联合类型:
export type GuardResult = boolean | UrlTree | RedirectCommand;
见 packages/router/src/models.ts#L136。具体语义如下表:
| 返回类型 | 含义 |
|---|---|
boolean |
true 放行导航;false 阻止导航(注意 CanMatch 特例见下) |
UrlTree 或 RedirectCommand |
不直接放行也不彻底阻断,而是将用户重定向到另一条路由 |
Promise<T> 或 Observable<T> |
Router 会异步等待,只取发出的第一个值作为判定结果,随后自动退订 |
关于异步返回值的实现依据:在守卫执行器中,无论守卫返回同步值、Promise 还是 Observable,都会被 wrapIntoObservable 包装成 Observable,并通过 .pipe(first()) 只取第一个发出值,见 check_guards.ts。
重要提示(
CanMatch的特例):只有CanMatch在返回false时行为与其他守卫不同——它不会彻底阻断导航,而是跳过当前这条路由配置,继续尝试匹配其他路由。
官方小贴士:如果需要把用户重定向到其他页面,应当在守卫中返回
UrlTree或RedirectCommand,而不是返回false之后再在代码里手动调用navigate()。这样重定向逻辑才能作为导航判定的一部分被 Router 统一处理,而不是游离在守卫判定之外、容易产生竞态。
四种路由守卫逐一详解
Angular 提供四种路由守卫,服务于导航流程中的不同检查点。所有守卫都能访问路由级提供的服务(见下文"路由级依赖注入"),并通过各自的路由参数拿到"即将激活/离开的路由"相关信息。
守卫类型速览:
| 守卫 | 检查时机 | 典型用途 |
|---|---|---|
CanActivate |
激活某条路由前 | 登录校验、角色鉴权 |
CanActivateChild |
激活父路由下的任意子路由前 | 一次性保护整组嵌套路由 |
CanDeactivate |
离开当前路由前 | 拦截未保存表单等离开动作 |
CanMatch |
路由匹配阶段 | 功能开关、A/B 分流、同路径多组件选择、条件加载 |
CanActivate:控制"能否进入该路由"
CanActivate 判定用户能否访问某条路由,是认证与授权最常用的守卫。
默认参数:
route: ActivatedRouteSnapshot:包含正在被激活的路由信息;state: RouterStateSnapshot:包含 Router 的当前状态。
返回值遵循标准的守卫返回类型。典型实现(登录态检查):
export const authGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
) => {
const authService = inject(AuthService);
return authService.isAuthenticated();
};
源码中的函数类型定义见 models.ts 中 CanActivateFn 定义:route、state 两个参数,返回 MaybeAsync<GuardResult>(即守卫结果,或返回该结果的 Promise/Observable)。其对应接口式版本为 CanActivate(定义在 models.ts#L904),类式守卫实现 canActivate(route, state) 方法即可。
CanActivateChild:一次性保护整组嵌套子路由
CanActivateChild 判定用户能否访问某个父路由下的子路由。当你想保护一整片嵌套路由区域时,不必在每个子路由上重复挂 canActivate,直接在父路由上挂 canActivateChild 即可。
执行语义上的关键点(原文强调): canActivateChild 会为所有子级运行。如果子路由下面还有更深一层的子路由,canActivateChild 会为该层级的每个子节点各运行一次。
默认参数:
childRoute: ActivatedRouteSnapshot:包含"未来"快照(即 Router 正试图导航到的状态)中、正在被激活的子路由信息;state: RouterStateSnapshot:包含 Router 的当前状态。
典型实现(角色校验):
export const adminChildGuard: CanActivateChildFn = (
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
) => {
const authService = inject(AuthService);
return authService.hasRole('admin');
};
源码中,runCanActivateChild 会沿目标路由路径逐个节点收集 canActivateChild 守卫并执行,见 check_guards.ts#L174-L207。这解释了为什么"挂在父级一次、作用于全部后代"。
CanDeactivate:控制"能否离开该路由"
CanDeactivate 判定用户能否离开当前路由。最常见的使用场景是防止带未保存内容的表单被意外导航走(如在用户编辑到一半时点刷新、跳转或浏览器返回)。
默认参数(四种守卫中参数最多):
component: T:即将被停用(deactivate)的组件实例;currentRoute: ActivatedRouteSnapshot:当前路由的信息;currentState: RouterStateSnapshot:当前 Router 状态;nextState: RouterStateSnapshot:将要导航前往的下一个 Router 状态。
其中 T 是当前组件类型,因此在守卫内可以直接访问组件实例上的状态与方法。典型实现:
export const unsavedChangesGuard: CanDeactivateFn<Form> = (
component: Form,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState: RouterStateSnapshot,
) => {
return component.hasUnsavedChanges()
? confirm('You have unsaved changes. Are you sure you want to leave?')
: true;
};
component.hasUnsavedChanges() 返回 true 时弹出浏览器确认框,用户确认离开则返回 true,否则返回 false 阻断导航。函数类型定义见 models.ts#L1134-L1139,其泛型参数 T 正是被停用组件的类型。
CanMatch:在"路由匹配阶段"就参与决策
CanMatch 判定一条路由配置是否参与 URL 匹配。它和其他守卫最大的区别在于拒绝即降级:返回 false 时 Angular 会跳过这条配置去尝试其他仍可匹配的路由,而不是把整次导航拦死。这让它非常适合:
- 功能开关(feature flag):新功能未开放时自动落到旧页面或"敬请期待"页;
- A/B 测试分流:不同用户群体命中不同的落地组件;
- 条件化路由加载:结合按需加载决定是否加载某段路由。
默认参数:
route: Route:正在被评估的路由配置;segments: UrlSegment[]:尚未被前面父级路由匹配消费掉的 URL 片段;currentSnapshot: PartialMatchRouteSnapshot:截至目前匹配流程中的路由快照(之所以是"部分"快照,是因为解析器等后续阶段尚未执行,父子等关系也尚未完全确立,其类型定义见 models.ts#L1253)。
返回值遵循标准守卫返回类型,但 false 的特殊语义如前所述。典型实现(功能开关):
export const featureToggleGuard: CanMatchFn = (
route: Route,
segments: UrlSegment[],
currentSnapshot: PartialMatchRouteSnapshot,
) => {
const featureService = inject(FeatureService);
return featureService.isFeatureEnabled('newDashboard');
};
高级玩法:同一个 path 挂载多个组件,由 canMatch 决定命中谁。当用户访问 /dashboard 时,按数组顺序匹配,第一个守卫放行的配置胜出:
// 📄 routes.ts
const routes: Routes = [
{
path: 'dashboard',
component: AdminDashboard,
canMatch: [adminGuard],
},
{
path: 'dashboard',
component: UserDashboard,
canMatch: [userGuard],
},
];
运行 CanMatch 守卫并处理重定向的逻辑在 runCanMatchGuards 中实现,见 check_guards.ts#L265-L287。匹配期被拒绝后"继续尝试其他路由"的语义,可参见 models.ts 中 CanMatch 接口的文档注释:守卫返回 false 则该配置被跳过,其他配置继续被处理,例如最终落到 ** 通配的 NotFoundComponent。
把守卫挂到路由配置上:数组语法与组合示例
创建好守卫之后,需要把它们配置进 Routes 定义中。
核心语法: 守卫以数组形式写在路由配置对象的 canActivate、canActivateChild、canDeactivate、canMatch 属性上。数组的意义在于允许对同一条路由叠加多个守卫,并且按数组顺序依次执行/判定。
下面是一个把四种守卫全部用上的完整配置(摘自官方文档的示例并逐条注释):
import {Routes} from '@angular/router';
import {authGuard} from './guards/auth.guard';
import {adminGuard} from './guards/admin.guard';
import {canDeactivateGuard} from './guards/can-deactivate.guard';
import {featureToggleGuard} from './guards/feature-toggle.guard';
const routes: Routes = [
// Basic CanActivate - requires authentication
{
path: 'dashboard',
component: Dashboard,
canActivate: [authGuard],
},
// Multiple CanActivate guards - requires authentication AND admin role
{
path: 'admin',
component: Admin,
canActivate: [authGuard, adminGuard],
},
// CanActivate + CanDeactivate - protected route with unsaved changes check
{
path: 'profile',
component: Profile,
canActivate: [authGuard],
canDeactivate: [canDeactivateGuard],
},
// CanActivateChild - protects all child routes
{
path: 'users', // /users - NOT protected
canActivateChild: [authGuard],
children: [
// /users/list - PROTECTED
{path: 'list', component: UserList},
// /users/detail/:id - PROTECTED
{path: 'detail/:id', component: UserDetail},
],
},
// CanMatch - conditionally matches route based on feature flag
{
path: 'beta-feature',
component: BetaFeature,
canMatch: [featureToggleGuard],
},
// Fallback route if beta feature is disabled
{
path: 'beta-feature',
component: ComingSoon,
},
];
结合上例逐项说明:
/dashboard:挂canActivate: [authGuard],未登录不允许进入;/admin:canActivate: [authGuard, adminGuard]同时要求"已登录 且 是管理员",两个守卫按数组顺序判定,全部返回true才放行;/profile:同时使用canActivate(进入要登录)与canDeactivate(离开时检查未保存修改),这正是"受保护路由 + 离开拦截"的常见组合;/users及其子路由:注意父级/users本身没有canActivate(注释明确标注NOT protected),但在父级挂了canActivateChild: [authGuard],于是子路由/users/list、/users/detail/:id全部受保护(注释标注PROTECTED)——这是"保护整片嵌套路由"的标准姿势;/beta-feature定义了两条:第一条用canMatch: [featureToggleGuard]做功能开关,开关打开时命中BetaFeature;第二条不带守卫作为兜底路由,开关关闭时(第一条返回false)自动落到ComingSoon。这正是CanMatch"拒绝即降级"语义的实战体现。
深入源码:守卫在导航流水线中如何被执行
为了写出正确的守卫,值得理解 Router 内部究竟按什么顺序、以什么规则执行守卫。以下均来自当前仓库 packages/router 的实现。
1. 执行顺序:先"离场检查"、后"入场检查"
在导航真正生效前,Router 会按如下顺序处理守卫(见 check_guards.ts#L56-L78):
- 收集所有
canDeactivate检查(针对当前状态中将被停用的路由/组件)与所有canActivate检查(针对目标快照中将被激活的路由); - 先运行离开守卫:任一离开守卫返回非
true,直接短路,不再进入激活检查; - 离开守卫全部通过后,再运行激活守卫(其中还穿插触发
ChildActivationStart、ActivationStart等路由生命周期事件); - 全部守卫返回
true,导航才继续。
// 节选,见 check_guards.ts#L69-L77
return runCanDeactivateChecks(canDeactivateChecks, targetSnapshot!, currentSnapshot).pipe(
mergeMap((canDeactivate) => {
return canDeactivate && isBoolean(canDeactivate)
? runCanActivateChecks(targetSnapshot!, canActivateChecks, forwardEvent)
: of(canDeactivate);
}),
map((guardsResult) => ({...t, guardsResult})),
);
2. 多个守卫的合并策略:按数组位置优先级"串行裁决"
当一个守卫属性(如 canActivate)配了多个守卫时,Router 会并发地启动它们,但判定结果遵循数组顺序的优先级:只有排在前面的守卫全部返回 true,才会采纳排在后面的守卫结果;一旦某位置守卫返回 false、UrlTree 或 RedirectCommand,立即采用该结果并终止。这一策略实现在 prioritized_guard_value.ts#L18-L45:
for (const result of results) {
if (result === true) {
// 结果为 true,继续检查下一个
continue;
} else if (result === INITIAL_VALUE) {
// 前序守卫尚未完成,停止裁决
return INITIAL_VALUE;
} else if (result === false || isRedirect(result)) {
// 该守卫已出结果且非 true:采纳 false / UrlTree / RedirectCommand
return result;
}
}
return true; // 全部守卫都解析为 true
守卫数组的顺序因此很重要:把"必须最先满足"的条件(如登录)放在数组前面。packages/router/test/integration/guards.spec.ts 中有大量针对"多个守卫的优先级与重定向"的用例(例如 should redirect with UrlTree if higher priority guards have resolved),可以作为行为契约的参考。
3. 守卫中的依赖注入:与路由级 provider 天然配合
官方文档指出,所有守卫都能访问路由级提供的服务以及 route 参数携带的路由信息。之所以可行,是因为执行函数式守卫时 Router 使用了 runInInjectionContext(closestInjector, ...) 包裹调用,其中 closestInjector 取自目标路由节点最近的 EnvironmentInjector,见 check_guards.ts#L157-L167。这意味两件事:
- 守卫内部可以放心使用
inject(SomeService)(如示例里的inject(AuthService)、inject(FeatureService)); - 结合路由级 provider(用
providers在路由上单独提供、随路由按需创建的服务)可以写出高内聚的守卫逻辑。关于路由级依赖提供的更多写法,见 定义依赖提供者(defining-dependency-providers)。
4. 异步守卫:只消费第一个值并自动退订
Promise/Observable 返回值之所以可行,是因为在 runCanActivate、runCanActivateChild、runCanDeactivate、runCanMatchGuards 中,返回值都会被 wrapIntoObservable(...).pipe(first()) 处理(例如 check_guards.ts#L168)。first() 保证:
- 只取守卫发出的第一个值作为判定依据;
- 判定完成后自动退订,不会因守卫流持续发出值而产生泄漏或后到结果覆盖先到结果。
因此,即使你的鉴权服务内部用的是长期存活的 Observable,守卫也只关心首个信号,这与官方文档"Router uses the first emitted value and then unsubscribes"的描述完全一致。
5. 重定向结果的内部处理
当守卫返回 UrlTree 或 RedirectCommand 时,Router 会取消当前导航并发起一条指向该目标的新导航。对 CanMatch/CanLoad 阶段产生的重定向,源码通过抛出 redirectingNavigationError 并在导航管道上游捕获来处理(相关实现见 navigation_canceling_error.ts 以及 check_guards.ts#L254-L263 的 redirectIfUrlTree)。这也是为什么官方建议把重定向"交给返回值"而不是在守卫里手动 navigate——前者会被识别为导航结果的一部分并统一调度。
小结与最佳实践回顾
- 服务端才是真正的安全边界:守卫只改善用户体验,绝不能作为唯一的授权手段;
- 按场景选对守卫:
- 保护单个路由 →
canActivate; - 保护整组嵌套子路由 → 父级挂
canActivateChild; - 拦截离开(未保存表单)→
canDeactivate; - 功能开关 / 同路径分流 / 条件加载 →
canMatch(false只跳过当前配置而非整段拦截);
- 保护单个路由 →
- 需要重定向就返回
UrlTree/RedirectCommand,不要返回false后再手动navigate(); - 多个守卫按数组顺序判定,把最先必须满足的条件放前面;全部通过才放行;
- 可以返回同步值,也可以返回
Promise/Observable,Router 只取第一个值并自动退订; - 优先使用函数式守卫(
CanActivateFn等)配合inject(),它们与路由级 provider 及 Angular 现代注入上下文天然契合;如需兼容类式守卫,可用mapToCanActivate/mapToCanMatch等辅助函数转换(见 functional_guards.ts)。
延伸阅读:四种函数式守卫的类型签名与接口定义集中在 packages/router/src/models.ts(含 GuardResult、PartialMatchRouteSnapshot 等关联类型),完整守卫执行流水线见 packages/router/src/operators/check_guards.ts,守卫组合优先级算法见 packages/router/src/operators/prioritized_guard_value.ts,丰富的行为契约测试见 packages/router/test/integration/guards.spec.ts。路由相关其他主题(重定向、导航、懒加载、生命周期事件、数据获取)可继续阅读 routing 指南目录。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00