鸿蒙原生 ArkTS 路由拦截实战:页面跳转前的权限校验

HarmonyOS NEXT 5.0 | API Version 24 | Stage Model | ArkTS


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、引言:为什么需要路由拦截?

在移动应用开发中,权限校验是一个无可回避的核心需求。用户的每一次页面跳转,背后都可能涉及身份验证、角色授权、数据隔离等安全考量。如果每次都在目标页面的 aboutToAppear 中写一遍检查逻辑,不仅导致代码大量重复,还容易出现遗漏——未经授权的用户可能在快速操作下绕过校验,直接访问敏感页面

路由拦截(Route Interception) 正是为了解决这一问题而生。它在页面导航的执行路径上插入一道「守卫」,在 router.pushUrl() 真正发生之前进行统一的权限校验。只有校验通过的请求才被放行,未通过的则被重定向到登录页或其他认证流程。

本文将以一个完整的 HarmonyOS NEXT 示例项目为载体,从零到一拆解路由拦截的实现原理、代码架构和最佳实践。无论你是刚接触鸿蒙开发的新手,还是正在迁移 Android/iOS 经验的老手,这篇文章都能帮你建立起清晰的 ArkTS 路由守卫认知。


二、项目概览与技术选型

2.1 应用场景描述

我们构建的应用包含三个页面:

页面 角色 是否需要认证
Index(主页) 导航入口,展示认证状态 否(公开)
LoginPage(登录页) 表单登录,获取认证凭证 否(公开)
TargetPage(目标页) 展示敏感数据,受保护内容

交互流程如下:

[主页] 点击「受保护页面」
    → AuthManager.checkAuth()  ← 路由拦截在此发生
        ├─ 已登录 → router.pushUrl(TargetPage)  ← 放行
        └─ 未登录 → 弹窗确认 → LoginPage → 登录成功 → 自动续航到 TargetPage

2.2 技术栈

  • 开发框架:HarmonyOS NEXT 5.0(API 24)
  • 编程语言:ArkTS(基于 TypeScript 的方舟语言)
  • 应用模型:Stage Model
  • 路由 API@kit.ArkUI 中的 router 模块
  • 弹窗 API@kit.ArkUI 中的 promptAction 模块
  • 构建工具:DevEco Studio + hvigor

2.3 为什么选择 Stage Model?

HarmonyOS NEXT 全面采用 Stage Model(而非 FA Model),其核心优势在于:

  • 组件化生命周期:Ability 与 Page 分离,职责清晰
  • 基于上下文(Context)的授权模型:所有敏感操作都需要显式传入 context
  • 路由独立管理:页面栈由系统统一调度,开发者专注于业务逻辑

我们的路由拦截架构天然契合 Stage Model——AuthManager 作为独立单例不依赖 Ability 生命周期,可以在任意页面被调用。


三、核心架构:AuthManager 认证状态管理器

3.1 单例模式与状态持有

路由拦截的核心是一个全局认证状态管理器。我们选择单例模式,确保整个应用进程中只有一份认证状态,避免多页面间的状态不同步。

// AuthManager.ets — 认证状态管理器(单例)
import { router, promptAction } from '@kit.ArkUI';

type AuthCallback = () => void;

class AuthManager {
  private static instance: AuthManager;
  private _isLoggedIn: boolean = false;
  private pendingCallbacks: AuthCallback[] = [];

  public static getInstance(): AuthManager {
    if (!AuthManager.instance) {
      AuthManager.instance = new AuthManager();
    }
    return AuthManager.instance;
  }

  public get isLoggedIn(): boolean {
    return this._isLoggedIn;
  }
  // ...
}

export const authManager = AuthManager.getInstance();

设计要点

  1. _isLoggedIn — 布尔状态标志,通过 getter 只读暴露
  2. pendingCallbacks — 回调队列,存储被拦截的导航意图
  3. getInstance() — 懒加载单例,首次访问时创建
  4. 导出实例 — 文件底部直接导出 authManager,其他页面 import { authManager } 即可使用

3.2 checkAuth:路由拦截的核心方法

这是整个架构的灵魂所在。每次导航发生前,调用方将目标 URL 和成功回调传入 checkAuth

public checkAuth(targetUrl: string, onSuccess: () => void): void {
  if (this._isLoggedIn) {
    // ── 已登录:直接放行 ──
    onSuccess();
    return;
  }

  // ── 未登录:拦截并引导登录 ──
  this.pendingCallbacks.push(onSuccess);
  this.showLoginPrompt(targetUrl);
}

算法逻辑

  • 已登录 → 立即执行 onSuccess(),等价于放行导航
  • 未登录 → 将 onSuccess 压入回调队列,弹出确认对话框

这个设计的精妙之处在于将「是否放行」的判断与「如何导航」的实现完全解耦。调用方只需提供 “我想去哪”(targetUrl)和 “到了做什么”(onSuccess),拦截器全权负责认证决策。

3.3 弹窗提示与重定向

当用户未登录时,我们通过 promptAction.showDialog() 弹窗询问:

private showLoginPrompt(targetUrl: string): void {
  promptAction.showDialog({
    title: '需要身份验证',
    message: `访问「${this.getPageName(targetUrl)}」需要登录,是否前往登录?`,
    buttons: [
      { text: '取消', color: '#999999' },
      { text: '前往登录', color: '#007DFF' }
    ]
  }).then((result: promptAction.ShowDialogSuccessResponse) => {
    if (result.index === 1) {
      router.pushUrl({
        url: 'pages/LoginPage',
        params: { returnUrl: targetUrl }
      });
    } else {
      // 用户取消 → 清除本次待执行的回调
      this.pendingCallbacks.pop();
    }
  });
}

细节处理

  1. 按钮索引result.index === 1 对应「前往登录」,0 对应「取消」
  2. 参数传递 — 将 returnUrl 传给 LoginPage,便于登录后知道要返回哪里
  3. 队列清理 — 用户取消时需 pop() 掉已压入的回调,防止内存泄漏
  4. 页面名提取getPageName() 工具方法从 URL 路径中提取可读页面名

3.4 login/logout:状态变更与回调执行

登录成功后,AuthManager 不仅更新状态,还负责「补发」所有被拦截的导航:

public login(): void {
  this._isLoggedIn = true;
  // 依次执行所有被拦截时积压的导航回调
  while (this.pendingCallbacks.length > 0) {
    const cb = this.pendingCallbacks.shift();
    cb?.();
  }
}

public logout(): void {
  this._isLoggedIn = false;
  this.pendingCallbacks = [];
}

续航机制login() 遍历 pendingCallbacks 队列,逐个执行之前在 checkAuth 中压入的回调。由于每个回调内部都包含 router.pushUrl(),效果就是登录后自动完成之前被拦截的导航——用户感受不到中断,仿佛从未被拦截过。


四、主页设计:Index 页面的多场景演示

4.1 页面结构

Index 页面作为演示入口,承载了三个功能按钮和一个认证状态卡片:

┌─────────────────────────────┐
│        🛡️                   │
│  路由拦截 · 权限校验          │
│  页面跳转前的身份验证守卫       │
├─────────────────────────────┤
│  🟢 已登录(认证通过) [退出]  │ ← 认证状态卡片
├─────────────────────────────┤
│  🎯 路由拦截工作原理           │ ← 说明卡片
├─────────────────────────────┤
│  🔒 访问受保护页面-A  🛡需认证 │ ← 按钮 1
│  🔐 访问受保护页面-B  🛡需认证 │ ← 按钮 2
│  📄 访问公开页面      🚀放行  │ ← 按钮 3
├─────────────────────────────┤
│  💡 操作提示                  │ ← 底部说明
└─────────────────────────────┘

4.2 使用 @Builder 构建按钮

由于三个按钮结构相同、参数不同,我们使用 @Builder 封装复用:

// 先定义接口,避免 ArkTS 的对象字面量类型限制
interface NavButtonParams {
  icon: string;
  title: string;
  subtitle: string;
  targetUrl: string;
  needAuth: boolean;
}

@Builder
NavButton(params: NavButtonParams) {
  Column() { /* ... UI 布局 ... */ }
    .onClick(() => {
      this.handleNavigation(params.targetUrl, params.needAuth);
    })
}

ArkTS 注意事项:在 API 24 中,ArkTS 编译器禁止将对象字面量直接用作类型声明(arkts-no-obj-literals-as-types)。必须预先定义显式的 interfaceclass,然后传入构造函数的参数类型标注中。

4.3 导航分发逻辑

每个按钮点击后,调用 handleNavigation 统一分发:

private handleNavigation(targetUrl: string, needAuth: boolean): void {
  if (needAuth) {
    // ── 需要认证 → 走路由拦截 ──
    authManager.checkAuth(targetUrl, () => {
      router.pushUrl({ url: targetUrl });
    });
  } else {
    // ── 无需认证 → 直接跳转 ──
    router.pushUrl({ url: targetUrl });
  }
}

关键设计needAuth 布尔标志控制了是否启用路由拦截。对于公开页面,直接调用 router.pushUrl 完全绕过拦截器;对于受保护页面,则必须经过 checkAuth 校验。

4.4 登录/登出切换

为了让演示者可重复体验拦截流程,页面顶部放置了登录/登出切换按钮:

private toggleAuth(): void {
  if (this.isLoggedIn) {
    authManager.logout();
  } else {
    authManager.login();
  }
  this.isLoggedIn = authManager.isLoggedIn;
}

注意 isLoggedIn 使用 @State 装饰,更新后会触发 UI 重新渲染,状态指示灯和按钮文字自动刷新。


五、登录页面:LoginPage 的认证实现

5.1 获取路由参数

aboutToAppear 生命周期中,获取由 AuthManager 传入的 returnUrl

aboutToAppear(): void {
  const params = router.getParams() as Record<string, Object>;
  if (params?.['returnUrl']) {
    this.returnUrl = params['returnUrl'] as string;
  }
}

returnUrl 用于记录拦截发生时用户原本想要访问的目标页面。登录成功后虽然不需要显式使用它(AuthManager 的回调队列自动处理),但保留此信息便于调试和日志追踪。

5.2 表单与登录流程

登录页面包含用户名和密码两个输入框,以及登录按钮。核心逻辑在 handleLogin 中:

private handleLogin(): void {
  this.isLoggingIn = true;

  setTimeout(() => {
    // 1. 更新认证状态(同时触发所有积压导航回调)
    authManager.login();

    // 2. 弹窗提示登录成功
    promptAction.showToast({
      message: '✅ 登录成功!正在跳转...',
      duration: 1500
    });

    // 3. 返回上一页
    router.back();
    this.isLoggingIn = false;
  }, 1200);
}

三步曲

  1. authManager.login() — 这是最关键的一步。它设置 _isLoggedIn = true,然后遍历执行 pendingCallbacks 中的所有导航回调。这些回调内部都包含了 router.pushUrl(targetUrl)
  2. promptAction.showToast() — 给用户即时反馈,告知登录成功。
  3. router.back() — 返回 Index 页面。由于第 1 步已经触发了导航回调,用户会看到页面自动跳转到 TargetPage,形成无缝续航的体验。

六、受保护页面:TargetPage 的路线验证

6.1 页面内容

TargetPage 展示了路由拦截的最终成果——只有经过认证的用户才能看到的敏感数据:

  • 认证状态指示器:绿色「🟢 已认证 · 访问授权通过」标签
  • 访问时间戳:记录页面被成功加载的时刻
  • 敏感数据内容:模拟的机密信息
  • 路由拦截流程图:用 ASCII 风格展示完整的拦截链路

6.2 滚动容器注意事项

在 API 24 中,Column 组件不再支持 .scrollable() 属性。如果需要纵向滚动,必须用 Scroll 包裹:

build() {
  Scroll() {      // ← 外层 Scroll 容器
    Column() {    // ← 内层 Column 内容
      // 所有页面内容...
    }
    .width('100%')
    .alignItems(HorizontalAlign.Center)
  }
  .scrollable(ScrollDirection.Vertical)   // ← scrollable 在 Scroll 上调用
  .width('100%')
  .height('100%')
  .backgroundColor('#F5F7FA')
}

这是 ArkTS 与传统 CSS/RN 布局的一个重要区别:滚动行为由容器组件提供,而不是内容组件自身


七、路由拦截完整流程图

┌──────────────────────────────────────────────────────────────────┐
│                      路由拦截完整执行链路                          │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  [用户点击「受保护页面」按钮]                                      │
│         │                                                        │
│         ▼                                                        │
│  Index.handleNavigation(url, needAuth=true)                      │
│         │                                                        │
│         ▼                                                        │
│  AuthManager.checkAuth(url, onSuccess)  ←── 拦截点               │
│         │                                                        │
│         ├── isLoggedIn === true?                                  │
│         │     │                                                   │
│         │     └── 是 → 执行 onSuccess()                           │
│         │              │                                          │
│         │              ▼                                          │
│         │         router.pushUrl(url) → TargetPage  ✓ 放行       │
│         │                                                        │
│         └── 否 → 弹出确认对话框                                   │
│                    │                                              │
│                    ├── 用户点「取消」 → pendingCallbacks.pop()    │
│                    │                  流程终止 ✗                  │
│                    │                                              │
│                    └── 用户点「前往登录」                          │
│                           │                                      │
│                           ▼                                      │
│                      router.pushUrl → LoginPage                  │
│                           │                                      │
│                           ▼                                      │
│                      用户输入 → 点击「登录」                       │
│                           │                                      │
│                           ▼                                      │
│                      authManager.login()                         │
│                           │                                      │
│                           ├── _isLoggedIn = true                 │
│                           │                                      │
│                           └── 遍历 pendingCallbacks              │
│                                │                                 │
│                                ▼                                 │
│                          执行 onSuccess()                         │
│                                │                                 │
│                                ▼                                 │
│                          router.pushUrl(url)                     │
│                                │                                 │
│                                ▼                                 │
│                          TargetPage  ✓ 登录后自动续航             │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

八、API 24 下的 ArkTS 编码规范总结

在编写本示例的过程中,我们遇到并解决了一些 ArkTS 在 API 24 下的编译限制。下面做系统性总结:

8.1 对象字面量类型限制

// ❌ 错误:Object literals cannot be used as type declarations
.then((result: { index: number }) => { ... })

// ✅ 正确:使用标准 API 类型或预定义接口
.then((result: promptAction.ShowDialogSuccessResponse) => { ... })
// ❌ 错误:Object literal must correspond to some explicitly declared class or interface
this.NavButton({ icon: '🔒', title: '...', ... })

// ✅ 正确:定义 interface 并作为参数类型
interface NavButtonParams { icon: string; title: string; ... }
@Builder NavButton(params: NavButtonParams) { ... }

规则:任何匿名对象字面量都必须有显式的类型声明对应,不能像 JavaScript 那样随意构造。

8.2 滚动容器分离

// ❌ 错误:Column 没有 scrollable 属性
Column() { ... }.scrollable(ScrollDirection.Vertical)

// ✅ 正确:Scroll 容器提供滚动能力
Scroll() { Column() { ... } }
  .scrollable(ScrollDirection.Vertical)

8.3 TextInput 初始值设置

// ❌ 错误:.value() 不是 TextInput 的链式属性
TextInput({ placeholder: '...' }).value(this.username)

// ✅ 正确:通过构造参数 text 传入初始值
TextInput({ placeholder: '...', text: this.username })

8.4 alignSelf 使用 ItemAlign

// ❌ 错误:HorizontalAlign 不能赋值给 ItemAlign
Text('...').alignSelf(HorizontalAlign.Start)

// ✅ 正确:使用 ItemAlign 枚举
Text('...').alignSelf(ItemAlign.Start)

8.5 import 路径与模块

// ✅ API 24 推荐的导入方式
import { router, promptAction } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';

// ❌ 不推荐:动态运行时获取
const promptAction = requireNapi('ohos.promptAction');

九、生产环境扩展方案

本文示例在演示路由拦截原理时做了适度简化。实际生产环境需要在此基础上做以下扩展:

9.1 认证凭证持久化

SharedPreferences / UserPreferences
├── accessToken: string    ← JWT 或自定义 token
├── refreshToken: string   ← 刷新凭证(可选)
├── expiresAt: number      ← 过期时间戳
└── userInfo: object       ← 用户基本信息(缓存)

AuthManager 在初始化时从持久化存储读取 token;login() 时写入;logout() 时清除。

9.2 Token 自动刷新

checkAuth 流程扩展:
1. 检查 accessToken 是否过期
2. 已过期 → 用 refreshToken 静默刷新
3. 刷新成功 → 放行导航
4. 刷新失败(refreshToken 也过期)→ 跳转登录页

9.3 角色(Role)与权限(Permission)分级

interface Permission {
  role: 'admin' | 'user' | 'guest';
  scope: string[];  // 可访问的页面路由列表
}

class AuthManager {
  // 增强 checkAuth 签名
  public checkAuth(
    targetUrl: string,
    requiredRoles: string[],
    onSuccess: () => void
  ): void { ... }
}

9.4 路由守卫装饰器模式

对于复杂项目,可以将拦截逻辑封装为装饰器或高阶函数:

function AuthGuard(requiredRoles: string[]) {
  return (targetUrl: string, onSuccess: () => void) => {
    authManager.checkAuth(targetUrl, requiredRoles, onSuccess);
  };
}

// 使用
const guardedNavigate = AuthGuard(['admin']);
guardedNavigate('pages/Dashboard', () => router.pushUrl({ url: 'pages/Dashboard' }));

十、常见问题与调试技巧

10.1 导航不生效

现象router.pushUrl() 调用了但页面没有跳转

排查

  1. 确认目标页面已在 main_pages.json 中注册
  2. 检查 URL 路径是否正确(以 pages/ 开头,不含 .ets 后缀)
  3. 查看日志中是否有路由跳转限制(如页面栈超过 32 层)
  4. 确认 onSuccess 回调确实被执行了(在回调内加 console.log)

10.2 登录后没有自动续航

// 常见错误:在 LoginPage 中直接 router.pushUrl 而不是 router.back
authManager.login();
router.pushUrl({ url: 'pages/TargetPage' }); // ❌ 错误的续航方式
router.back(); // ✅ 正确:返回后 AuthManager 自动执行导航回调

原因:如果登录后直接 pushUrl 到 TargetPage,会新建一个页面实例,而原本 Index 页面栈中积压的回调没有被执行。正确做法是 back() 返回,让 AuthManager 在 login() 中自动遍历执行回调。

10.3 弹窗不显示

现象promptAction.showDialog() 被调用但没有任何弹窗

排查

  1. 确认 page 权限已声明(弹窗在 API 24 中不需要额外权限)
  2. 检查是否在非 UI 线程调用(弹窗必须在主线程调用)
  3. 确认没有其他模态组件遮挡(如半透明的覆盖层)

十一、总结

本文通过一个完整的 HarmonyOS NEXT 示例应用,系统性地讲解了路由拦截在 ArkTS 中的实现方案。核心要点如下:

  1. 路由拦截的本质:在 router.pushUrl() 之前插入一道可编程的守卫函数,统一处理认证、授权等横切关注点。

  2. 单例 AuthManager:全局持有认证状态和待执行回调队列,确保多页面间的状态一致性。checkAuth() 完成拦截决策,login() 触发续航回调。

  3. 回调队列续航:被拦截的导航意图以回调函数的形式暂存在队列中,登录后按顺序执行,实现「拦截 → 登录 → 自动完成」的无缝体验。

  4. ArkTS 编码约束:API 24 对对象字面量、滚动容器、组件属性等有严格限制,需要在编码时遵守语言规范。

  5. 可扩展架构:从简单的布尔认证到基于 token 的持久化方案,从单角色到多权限分级,路由拦截模式可以平滑演进。

路由拦截模式将安全的关注点从各个页面中抽离出来,集中到单一的拦截器中管理。这不仅减少了重复代码,更重要的是防止了人为遗漏——每一次受保护页面的导航都必须经过守卫,没有任何例外路径。

在 HarmonyOS NEXT 的生态建设中,ArkTS 正在变得越来越成熟。掌握路由拦截这样的架构模式,将帮助你在构建复杂应用时事半功倍。


十二、参考资料


本文配套完整示例代码位于 entry/src/main/ets/ 目录下,包含 pages/Index.etspages/LoginPage.etspages/TargetPage.etsutils/AuthManager.ets 四个核心源文件。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐