跳转至

Angular

Authany 帮助您为 Angular 应用添加用户登录功能,并提供预置的登录页面和用户设置页面,加速开发进程。

跟随此 🕐 15 分钟 教程,使用 Angular 与 Authany SDK 创建一个简单的应用。

说明

本教程的完整示例代码,请查看并克隆 GitHub 上的示例项目。

说明

本教程适用于 Angular 17 及以上版本,该版本使用独立组件(standalone components),无需 NgModule 即可启动应用。示例项目基于 Angular 22 构建(独立组件、无 Zone 变更检测)。如果您使用的是仍然依赖 NgModule 的旧版 Angular,请根据实际情况调整相应步骤。

目录

在 Authany 中设置应用

前往 https://manage.authany.com/ 注册账号,并创建一个项目。

然后,我们需要在项目管理台中创建一个应用。

在管理台中创建应用

  1. 在左侧菜单栏中进入 应用。
  2. 在顶部工具栏中点击 ⊕Add Application。
  3. 输入应用名称,例如 "MyAwesomeApp"。
  4. 选择 Single Page Application 作为应用类型
  5. 点击 "Save" 创建应用

配置授权重定向 URI

重定向 URI 是应用中的一个 URL,用户通过 Authany 登录后会被跳转到这里。在该路径下,需要发起一次 finish authentication 调用来完成登录流程。

在本教程中,请将 http://localhost:4000/auth-redirect 添加到 Authorize Redirect URIs 中。

配置登出后重定向 URI

登出后重定向 URI 是用户登出后会被跳转到的 URL,该 URL 必须加入白名单。

在本教程中,请将 http://localhost:4000/ 添加到 Post Logout Redirect URIs 中。

在进行下一步之前,请先 保存 配置。

配置授权重定向 URI 和登出后重定向 URI。

步骤 1:创建一个简单的 Angular 项目

以下是搭建 Angular 项目脚手架的推荐步骤。如果您是将 Authany 添加到已有项目中,可以跳过这部分,直接查看下一节的步骤 2:将 Authany SDK 安装到项目中。

安装 Angular CLI

要安装 Angular CLI,请打开终端窗口并运行以下命令:

npm install -g @angular/cli

说明

Windows 用户可参考 https://angular.dev/tools/cli/setup-local 了解安装 Angular CLI 的更多信息。

创建初始工作区

运行以下 CLI 命令,创建一个新的工作区,并生成一个启用了路由的初始应用 my-app。

# Create a workspace called my-app
ng new my-app --routing --defaults
# Move into the project directory
cd my-app

在 Angular 17+ 中,这会生成一个独立组件(standalone)应用。生成的重要文件包括:

  • src/main.ts — 使用 bootstrapApplication(AppComponent, appConfig) 启动应用
  • src/app/app.config.ts — 应用的 providers(我们将在这里配置 Authany 和路由)
  • src/app/app.routes.ts — 路由定义

说明

较新版本的 Angular CLI 生成组件和服务文件时不带 .component / .service 后缀(例如生成 app.ts 和类 App,而不是 app.component.ts 和类 AppComponent)。本教程及示例项目使用带后缀的命名方式。两种方式下代码逻辑完全一致——只需让文件名和类名与您的 CLI 生成结果保持一致即可。

编辑启动应用的脚本

在 package.json 文件中,编辑 script 部分的 start 脚本

# before
"start": "ng serve"

# after
"start": "ng serve --port 4000"

start 脚本会以开发模式在端口 4000(而非默认端口)上运行应用。

编辑 app.component.html 文件

默认情况下,Angular CLI 已经为我们生成了一个初始应用,但为了简化教程,我们建议对其中一些文件从头进行修改。

在 src/app/app.component.html 文件中,删除所有内容,并添加以下一行:

<div>Hello world</div>

运行初始应用

现在运行 npm start 来启动项目,您将在 http://localhost:4000 上看到 "Hello world"。

步骤 2:将 Authany SDK 安装到项目中

在 Angular 项目目录中运行以下命令,安装 Authany Web SDK

npm install --save-exact @authgear/web

必须在调用任何其他 SDK 函数(例如 finishAuthentication() 或 fetchUserInfo())之前完成 Authany 的配置,而配置过程是异步的。在 Angular 中确保这一点最干净的方式,是在应用初始化阶段运行 authgear.configure(),让应用只有在 Authany 准备就绪后才渲染。

在 src/app/app.config.ts 中,注册一个调用 configure 的应用初始化器(app initializer):

// src/app/app.config.ts
import {
  ApplicationConfig,
  provideAppInitializer,
  provideBrowserGlobalErrorListeners,
} from '@angular/core';
import { provideRouter } from '@angular/router';
import authgear from '@authgear/web';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes),
    // Configure Authgear before the app renders. configure() must complete
    // before any other SDK call, so blocking bootstrap here avoids a race in
    // which the redirect page or home page runs before the SDK is ready.
    provideAppInitializer(async () => {
      try {
        await authgear.configure({
          endpoint: '<your_app_endpoint>',
          clientID: '<your_client_id>',
          sessionType: 'refresh_token',
        });
      } catch (e) {
        // Don't block bootstrap if Authgear is unreachable; the app will
        // render in the logged-out state and login can be retried.
        console.error(e);
      }
    }),
  ],
};

Authany 容器实例接受 endpoint 和 clientID 作为参数,这两个值可以在在 Authany 中设置应用一节创建的应用页面中获取。

由于该初始化器是被 await 的,当任何路由开始渲染时,Authany 都已经可以使用。

说明

现在运行 npm start,如果 Authany SDK 配置成功,您应该会看到一个显示 "Hello World" 的页面,且控制台中没有错误信息

步骤 3:实现用户服务

由于我们希望在应用的任意位置引用登录状态,因此将该状态放入一个 service 中,即 /src/app/services/ 文件夹下的 user.service.ts。

在 user.service.ts 中会维护一个 isLoggedIn 状态。该状态通过 onSessionStateChange 回调自动更新,该回调保存在本地 SDK 容器的 delegate 中。我们将该状态以 Angular signal 的形式暴露出来,以便界面能够响应式更新(这在无 Zone 变更检测——较新版本 Angular 的默认方式——下同样适用)。

// src/app/services/user.service.ts
import { Injectable, signal } from '@angular/core';
import authgear from '@authgear/web';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  // By default the user is not logged in
  readonly isLoggedIn = signal(false);

  constructor() {
    // When the sessionState changed, logged in state will also be changed
    authgear.delegate = {
      onSessionStateChange: (container) => {
        // sessionState is now up to date
        // value of sessionState can be "NO_SESSION" or "AUTHENTICATED"
        const sessionState = container.sessionState;
        this.isLoggedIn.set(sessionState === 'AUTHENTICATED');
      },
    };
  }
}

onSessionStateChange delegate 会在 configure() 运行期间触发,因此 UserService 必须在 configure() 之前被实例化——否则会错过已登录用户的初始状态。请更新 src/app/app.config.ts 中的应用初始化器,先注入该服务:

// src/app/app.config.ts
import { inject, provideAppInitializer } from '@angular/core';
import authgear from '@authgear/web';
import { UserService } from './services/user.service';

// ...

    provideAppInitializer(async () => {
      // Instantiate UserService first so its onSessionStateChange delegate is
      // registered before configure() runs and can observe the initial state.
      inject(UserService);
      try {
        await authgear.configure({
          endpoint: '<your_app_endpoint>',
          clientID: '<your_client_id>',
          sessionType: 'refresh_token',
        });
      } catch (e) {
        console.error(e);
      }
    }),

步骤 4:实现身份验证跳转页面

接下来,我们将添加一个 "auth-redirect" 页面,用于在用户通过 Authany 完成身份验证后处理验证结果。

使用以下命令创建 auth-redirect 组件:

ng generate component auth-redirect

处理完跳转后,我们会注入路由(router)来进行导航。

在 Auth Redirect 组件中调用 Authany 的 finishAuthentication() 函数,将 token 发回 Authany 服务器以换取访问令牌和刷新令牌。不必担心这些技术细节,finishAuthentication() 会为您完成所有繁重的工作,并保存身份验证数据。

身份验证完成后,UserService 中的 isLoggedIn 状态会自动被设为 true。最后,导航回根路径(/),也就是我们的 Home 页面。

最终的 auth-redirect.component.ts 如下所示

// src/app/auth-redirect/auth-redirect.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { Router } from '@angular/router';
import authgear from '@authgear/web';

@Component({
  selector: 'app-auth-redirect',
  templateUrl: './auth-redirect.component.html',
  styleUrl: './auth-redirect.component.css',
})
export class AuthRedirectComponent implements OnInit {
  private readonly router = inject(Router);

  ngOnInit(): void {
    authgear
      .finishAuthentication()
      .catch((e) => console.error(e))
      .then(() => {
        this.router.navigate(['']);
      });
  }
}

步骤 5:为应用添加路由

接下来,我们将添加一个 "Home" 页面。使用以下命令创建 home 组件:

ng generate component home

然后将 HomeComponent 和 AuthRedirectComponent 作为路由导入。我们可以在工作区生成时创建的 app.routes.ts 文件中添加这些路由:

// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AuthRedirectComponent } from './auth-redirect/auth-redirect.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'auth-redirect', component: AuthRedirectComponent },
];

路由已经在 app.config.ts 中通过 provideRouter(routes)(在步骤 2 中添加)提供。请确保根组件通过导入 RouterOutlet 并在模板中使用它,来渲染路由对应的组件。

// src/app/app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css',
})
export class AppComponent {}

将 src/app/app.component.html 中的内容替换为以下代码:

<router-outlet></router-outlet>

此时的文件结构应如下所示

src
├── (...)
├── main.ts
└── app
    ├── app.config.ts
    ├── app.routes.ts
    ├── app.component.ts
    ├── app.component.html
    ├── (...)
    ├── auth-redirect
    │   ├── auth-redirect.component.ts
    │   ├── auth-redirect.component.html
    │   └── (...)
    ├── home
    │   ├── home.component.ts
    │   ├── home.component.html
    │   └── (...)
    └── services
        └── user.service.ts

步骤 6:添加登录按钮

首先,我们将在 home.component.ts 中导入 Authany 依赖并注入 UserService,然后添加 startLogin 方法,该方法会调用 startAuthentication(options),将用户跳转到登录页面。

// src/app/home/home.component.ts
import { Component, inject } from '@angular/core';
import { UserService } from '../services/user.service';
import authgear, { PromptOption } from '@authgear/web';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrl: './home.component.css',
})
export class HomeComponent {
  readonly user = inject(UserService);

  startLogin(): void {
    authgear
      .startAuthentication({
        redirectURI: 'http://localhost:4000/auth-redirect',
        prompt: PromptOption.Login,
      })
      .then(
        () => {
          // started authorization, user should be redirected to Authgear
        },
        (err) => {
          // failed to start authorization
          console.error(err);
        }
      );
  }
}

说明

在 @authgear/web v5 及以上版本中,prompt 接受一个 PromptOption 枚举值(例如 PromptOption.Login)。导入并使用该枚举可以让调用保持类型安全。

然后,您可以在 home.component.html 中添加一个按钮,用于触发 startLogin 方法:

<h1>Home Page</h1>
<button type="button" (click)="startLogin()">Login</button>

现在您可以运行 npm start,点击登录按钮后,会跳转到 Authany 登录页面。

点击登录按钮后,用户会被跳转到 Authany 登录页面

步骤 7:显示用户信息

Authany SDK 可以帮助您轻松获取已登录用户的信息。

在上一步中,用户已成功登录,接下来我们尝试在 Home 页面中打印该用户的用户 ID(sub)。

在 home 组件中,我们将添加一个简单的加载提示,以及一条打印 Sub ID 的问候信息。我们会添加两个条件元素,使其仅在用户登录后显示;同时也可以让登录按钮仅在用户未登录时显示。

利用 UserService 中的 isLoggedIn 来控制页面上的组件。通过 fetchUserInfo() 获取用户信息,并访问其 sub 属性。我们将加载状态和问候信息状态都存储为 signal,以保持模板的响应式更新。

// src/app/home/home.component.ts
import { Component, OnInit, inject, signal } from '@angular/core';
import { UserService } from '../services/user.service';
import authgear, { PromptOption } from '@authgear/web';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrl: './home.component.css',
})
export class HomeComponent implements OnInit {
  readonly user = inject(UserService);

  readonly isLoading = signal(false);
  readonly greetingMessage = signal('');

  async updateGreetingMessage() {
    this.isLoading.set(true);
    try {
      if (this.user.isLoggedIn()) {
        const userInfo = await authgear.fetchUserInfo();
        this.greetingMessage.set('The current User sub: ' + userInfo.sub);
      }
    } finally {
      this.isLoading.set(false);
    }
  }

  ngOnInit(): void {
    this.updateGreetingMessage().catch((e) => {
      console.error(e);
    });
  }

  startLogin(): void {
    authgear
      .startAuthentication({
        redirectURI: 'http://localhost:4000/auth-redirect',
        prompt: PromptOption.Login,
      })
      .then(
        () => {
          // started authorization, user should be redirected to Authgear
        },
        (err) => {
          // failed to start authorization
          console.error(err);
        }
      );
  }
}

在 home.component.html 中,使用新的 @if 控制流,并通过调用各个 signal 来读取其值:

<h1>Home Page</h1>
@if (isLoading()) {
  <span>Loading</span>
}
@if (greetingMessage()) {
  <span>{{ greetingMessage() }}</span>
}
@if (!user.isLoggedIn()) {
  <div>
    <button type="button" (click)="startLogin()">Login</button>
  </div>
}

重新运行应用,用户的用户 ID(sub)应该会打印在 Home 页面上。

步骤 8:添加登出按钮

最后,我们在用户登录后添加一个登出按钮。

在 home.component.html 中,我们将在模板里添加一个条件元素:

@if (user.isLoggedIn()) {
  <div>
    <button type="button" (click)="logout()">Logout</button>
  </div>
}

并添加 logout 方法:

logout(): void {
  authgear
    .logout({
      redirectURI: 'http://localhost:4000/',
    })
    .then(
      () => {
        this.greetingMessage.set('');
      },
      (err) => {
        console.error(err);
      }
    );
}

重新运行应用,现在点击登出按钮即可登出。

步骤 9:打开用户设置

Authany 提供了内置界面,供用户设置自己的属性并更改安全设置。

使用 open 函数打开位于 <your_app_endpoint>/settings 的设置页面

在 home.component.html 的登出按钮区域,追加一个条件链接。

@if (user.isLoggedIn()) {
  <div>
    <button type="button" (click)="logout()">Logout</button>
    <br />
    <a target="_blank" rel="noreferrer" (click)="userSetting($event)" href="#">
      User Setting
    </a>
  </div>
}

并添加 userSetting 方法(注意新增的 Page 导入):

import authgear, { Page, PromptOption } from '@authgear/web';

async userSetting(event: MouseEvent) {
  event.preventDefault();
  event.stopPropagation();
  await authgear.open(Page.Settings);
}

最终的 home.component.ts 如下:

// src/app/home/home.component.ts
import { Component, OnInit, inject, signal } from '@angular/core';
import { UserService } from '../services/user.service';
import authgear, { Page, PromptOption } from '@authgear/web';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrl: './home.component.css',
})
export class HomeComponent implements OnInit {
  readonly user = inject(UserService);

  readonly isLoading = signal(false);
  readonly greetingMessage = signal('');

  async updateGreetingMessage() {
    this.isLoading.set(true);
    try {
      if (this.user.isLoggedIn()) {
        const userInfo = await authgear.fetchUserInfo();
        this.greetingMessage.set('The current User sub: ' + userInfo.sub);
      }
    } finally {
      this.isLoading.set(false);
    }
  }

  ngOnInit(): void {
    this.updateGreetingMessage().catch((e) => {
      console.error(e);
    });
  }

  startLogin(): void {
    authgear
      .startAuthentication({
        redirectURI: 'http://localhost:4000/auth-redirect',
        prompt: PromptOption.Login,
      })
      .then(
        () => {
          // started authorization, user should be redirected to Authgear
        },
        (err) => {
          // failed to start authorization
          console.error(err);
        }
      );
  }

  logout(): void {
    authgear
      .logout({
        redirectURI: 'http://localhost:4000/',
      })
      .then(
        () => {
          this.greetingMessage.set('');
        },
        (err) => {
          console.error(err);
        }
      );
  }

  async userSetting(event: MouseEvent) {
    event.preventDefault();
    event.stopPropagation();
    await authgear.open(Page.Settings);
  }
}

最终的 home.component.html 如下:

<h1>Home Page</h1>
@if (isLoading()) {
  <span>Loading</span>
}
@if (greetingMessage()) {
  <span>{{ greetingMessage() }}</span>
}
@if (!user.isLoggedIn()) {
  <div>
    <button type="button" (click)="startLogin()">Login</button>
  </div>
}
@if (user.isLoggedIn()) {
  <div>
    <button type="button" (click)="logout()">Logout</button>
    <br />
    <a target="_blank" rel="noreferrer" (click)="userSetting($event)" href="#">
      User Setting
    </a>
  </div>
}

登录后显示用户 ID、指向用户设置的链接以及登出按钮

后续步骤:调用 API

要访问后端应用服务器上的受限资源,HTTP 请求需要在 Authorization 头中携带访问令牌。Web SDK 提供了 fetch 函数来自动处理这一点,您也可以通过 authgear.accessToken 获取令牌。

方式 1:使用 Authany SDK 提供的 fetch 函数

Authany SDK 提供了 fetch 函数,供您调用应用服务器。该 fetch 函数会自动在应用请求中携带 Authorization 头,并自动处理访问令牌的刷新。authgear.fetch 实现了 fetch 规范。

authgear
    .fetch("YOUR_SERVER_URL")
    .then(response => response.json())
    .then(data => console.log(data));

方式 2:将访问令牌添加到 HTTP 请求头

您可以通过 authgear.accessToken 获取访问令牌。每次使用访问令牌前,请调用 refreshAccessTokenIfNeeded,该函数会进行检查,只有在令牌过期时才会发起网络请求刷新令牌。请将访问令牌放入应用请求的 Authorization 头中。

authgear
    .refreshAccessTokenIfNeeded()
    .then(() => {
        // access token is ready to use
        // accessToken can be string or undefined
        // it will be empty if user is not logged in or session is invalid
        const accessToken = authgear.accessToken;

        // include Authorization header in your application request
        const headers = {
            Authorization: `Bearer ${accessToken}`
        };
    });