首页
/ Angular 路由守卫(Route Guards)完全指南:用 CanActivate、CanActivateChild、CanDeactivate、CanMatch 精细管控路由访问

Angular 路由守卫(Route Guards)完全指南:用 CanActivate、CanActivateChild、CanDeactivate、CanMatch 精细管控路由访问

2026-09-06 18:48:34作者:廉皓灿Ida

路由守卫是一组在路由导航前后执行的"检查点"函数,用来决定"用户能否进入/离开某个路由"以及"某个路由配置是否参与匹配"。本文以 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,
) => {
  // ...
};

从源码看,CanActivateFnCanActivateChildFnCanDeactivateFn<T>CanMatchFn 这四种函数类型均以 @publicApi 形式定义在 packages/router/src/models.ts,Router 在运行时支持"类式守卫"(实现 CanActivate 等接口)与"函数式守卫"两种写法:执行时若检测到类式守卫则调用其对应方法,否则在注入上下文中直接调用函数,见 check_guards.ts。对类式守卫,仓库还提供了 mapToCanActivatemapToCanMatchmapToCanActivateChildmapToCanDeactivate 等辅助函数,可将类守卫数组转换为等价的函数守卫数组,见 functional_guards.ts

守卫的返回值:所有类型统一约定的放行/拦截信号

无论哪一种守卫,都共享同一组可返回的类型。GuardResult 在源码中被定义为联合类型:

export type GuardResult = boolean | UrlTree | RedirectCommand;

packages/router/src/models.ts#L136。具体语义如下表:

返回类型 含义
boolean true 放行导航;false 阻止导航(注意 CanMatch 特例见下)
UrlTreeRedirectCommand 不直接放行也不彻底阻断,而是将用户重定向到另一条路由
Promise<T>Observable<T> Router 会异步等待,只取发出的第一个值作为判定结果,随后自动退订

关于异步返回值的实现依据:在守卫执行器中,无论守卫返回同步值、Promise 还是 Observable,都会被 wrapIntoObservable 包装成 Observable,并通过 .pipe(first()) 只取第一个发出值,见 check_guards.ts

重要提示(CanMatch 的特例):只有 CanMatch 在返回 false 时行为与其他守卫不同——它不会彻底阻断导航,而是跳过当前这条路由配置,继续尝试匹配其他路由

官方小贴士:如果需要把用户重定向到其他页面,应当在守卫中返回 UrlTreeRedirectCommand,而不是返回 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 定义routestate 两个参数,返回 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 定义中。

核心语法: 守卫以数组形式写在路由配置对象的 canActivatecanActivateChildcanDeactivatecanMatch 属性上。数组的意义在于允许对同一条路由叠加多个守卫,并且按数组顺序依次执行/判定

下面是一个把四种守卫全部用上的完整配置(摘自官方文档的示例并逐条注释):

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],未登录不允许进入;
  • /admincanActivate: [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):

  1. 收集所有 canDeactivate 检查(针对当前状态中将被停用的路由/组件)与所有 canActivate 检查(针对目标快照中将被激活的路由);
  2. 先运行离开守卫:任一离开守卫返回非 true,直接短路,不再进入激活检查;
  3. 离开守卫全部通过后,再运行激活守卫(其中还穿插触发 ChildActivationStartActivationStart 等路由生命周期事件);
  4. 全部守卫返回 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,才会采纳排在后面的守卫结果;一旦某位置守卫返回 falseUrlTreeRedirectCommand,立即采用该结果并终止。这一策略实现在 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 返回值之所以可行,是因为在 runCanActivaterunCanActivateChildrunCanDeactivaterunCanMatchGuards 中,返回值都会被 wrapIntoObservable(...).pipe(first()) 处理(例如 check_guards.ts#L168)。first() 保证:

  • 只取守卫发出的第一个值作为判定依据;
  • 判定完成后自动退订,不会因守卫流持续发出值而产生泄漏或后到结果覆盖先到结果。

因此,即使你的鉴权服务内部用的是长期存活的 Observable,守卫也只关心首个信号,这与官方文档"Router uses the first emitted value and then unsubscribes"的描述完全一致。

5. 重定向结果的内部处理

当守卫返回 UrlTreeRedirectCommand 时,Router 会取消当前导航并发起一条指向该目标的新导航。对 CanMatch/CanLoad 阶段产生的重定向,源码通过抛出 redirectingNavigationError 并在导航管道上游捕获来处理(相关实现见 navigation_canceling_error.ts 以及 check_guards.ts#L254-L263redirectIfUrlTree)。这也是为什么官方建议把重定向"交给返回值"而不是在守卫里手动 navigate——前者会被识别为导航结果的一部分并统一调度。

小结与最佳实践回顾

  1. 服务端才是真正的安全边界:守卫只改善用户体验,绝不能作为唯一的授权手段;
  2. 按场景选对守卫
    • 保护单个路由 → canActivate
    • 保护整组嵌套子路由 → 父级挂 canActivateChild
    • 拦截离开(未保存表单)→ canDeactivate
    • 功能开关 / 同路径分流 / 条件加载 → canMatchfalse 只跳过当前配置而非整段拦截);
  3. 需要重定向就返回 UrlTree/RedirectCommand,不要返回 false 后再手动 navigate()
  4. 多个守卫按数组顺序判定,把最先必须满足的条件放前面;全部通过才放行;
  5. 可以返回同步值,也可以返回 Promise/Observable,Router 只取第一个值并自动退订;
  6. 优先使用函数式守卫CanActivateFn 等)配合 inject(),它们与路由级 provider 及 Angular 现代注入上下文天然契合;如需兼容类式守卫,可用 mapToCanActivate/mapToCanMatch 等辅助函数转换(见 functional_guards.ts)。

延伸阅读:四种函数式守卫的类型签名与接口定义集中在 packages/router/src/models.ts(含 GuardResultPartialMatchRouteSnapshot 等关联类型),完整守卫执行流水线见 packages/router/src/operators/check_guards.ts,守卫组合优先级算法见 packages/router/src/operators/prioritized_guard_value.ts,丰富的行为契约测试见 packages/router/test/integration/guards.spec.ts。路由相关其他主题(重定向、导航、懒加载、生命周期事件、数据获取)可继续阅读 routing 指南目录

登录后查看全文
热门项目推荐
相关项目推荐