跳到主要内容

为你的 Angular 应用添加认证 (Authentication)

本指南将向你展示如何将 Logto Angular SDK v2 集成到你的应用中。

提示:
  • 本指南使用官方的 @logto/angular v2 SDK,该 SDK 支持 Angular 20,并提供依赖注入和 Signals。
  • 示例项目可在我们的 SDK 仓库 中获取。

前置条件​

  • 一个 Logto Cloud 账户或 自托管 Logto。
  • 在 Logto 控制台中创建的单页应用程序(SPA)。
  • 一个 Angular 20 项目。

安装​

通过你喜欢的包管理器安装 Logto SDK:

npm i @logto/angular

集成​

初始化 Logto provider​

在你的 Angular 项目中,在 app.config.ts 文件中注册 provideLogto 和你的应用路由:

app/app.config.ts
import { type ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideLogto } from '@logto/angular';

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

export const appConfig: ApplicationConfig = {
providers: [
provideLogto({
endpoint: '<your-logto-endpoint>',
appId: '<your-app-id>',
}),
provideRouter(routes),
// ...other providers
],
};

provideLogto 会在浏览器首次渲染后自动恢复认证 (Authentication) 状态。你无需在组件中调用初始化方法。

备注:

当使用服务端渲染(SSR)时,认证 (Authentication) 状态和令牌仅在浏览器中可用。使用 isLoading() 在初始化完成前显示加载状态。如果你需要在服务端渲染期间获取已认证的数据,请使用服务端或 BFF SDK。

配置重定向 URI​

在我们深入细节之前,下面是终端用户体验的快速概览。登录流程可以简化为如下:

  1. 你的应用调用登录方法。
  2. 用户被重定向到 Logto 登录页面。对于原生应用,会打开系统浏览器。
  3. 用户完成登录后被重定向回你的应用(配置为重定向 URI)。

关于基于重定向的登录​

  1. 此认证 (Authentication) 过程遵循 OpenID Connect (OIDC) 协议,Logto 强制执行严格的安全措施以保护用户登录。
  2. 如果你有多个应用程序,可以使用相同的身份提供商 (IdP)(日志 (Logto))。一旦用户登录到一个应用程序,当用户访问另一个应用程序时,Logto 将自动完成登录过程。

要了解有关基于重定向的登录的原理和好处的更多信息,请参阅 Logto 登录体验解释。


备注:

在以下代码片段中,我们假设你的应用程序运行在 http://localhost:3000/。

配置重定向 URI​

切换到 Logto Console 的应用详情页面。添加一个重定向 URI http://localhost:3000/callback。

Logto Console 中的重定向 URI

就像登录一样,用户应该被重定向到 Logto 以注销共享会话。完成后,最好将用户重定向回你的网站。例如,添加 http://localhost:3000/ 作为注销后重定向 URI 部分。

然后点击“保存”以保存更改。

处理重定向​

创建一个回调组件,在 Logto 将用户重定向回你的应用后完成登录。使用 afterNextRender,确保回调处理仅在浏览器中运行:

app/callback.component.ts
import { afterNextRender, Component, inject } from '@angular/core';
import { LogtoService } from '@logto/angular';

@Component({
selector: 'app-callback',
standalone: true,
template: `
@if (logto.error(); as error) {
<p role="alert">{{ error.message }}</p>
} @else {
<p>正在完成登录...</p>
}
`,
})
export class CallbackComponent {
readonly logto = inject(LogtoService);

constructor() {
afterNextRender(() => {
void (async () => {
const callbackUri = window.location.href;

if (!(await this.logto.isSignInRedirected(callbackUri))) {
window.location.replace(window.location.origin);
return;
}

await this.logto.handleSignInCallback(callbackUri);
})().catch(() => {
// SDK 会通过 logto.error() 向模板暴露回调错误。
});
});
}
}

isSignInRedirected() 用于检查当前 URL 是否匹配一个活跃的登录会话。如果有人在没有登录会话的情况下访问回调路由,本示例会将其重定向回应用首页,而不是尝试完成登录。

在 app.routes.ts 中注册回调路由。它必须与重定向 URI 的路径一致,并且不能要求认证 (Authentication)。例如,重定向 URI 以 /callback 结尾时,使用 callback:

app/app.routes.ts
import { type Routes } from '@angular/router';

import { CallbackComponent } from './callback.component';

export const routes: Routes = [
{ path: 'callback', component: CallbackComponent },
// ...other routes
];

根组件需要一个 <router-outlet /> 来渲染该路由,具体见下一步。

实现登录与登出​

注入 LogtoService 以启动登录和登出。将已注册的重定向 URI 传递给这些方法。postRedirectUri 告诉 SDK 在成功处理登录回调后跳转到哪里:

备注:

在调用 signIn() 之前,请确保你已在管理控制台中正确配置了重定向 URI。

app/app.component.ts
import { Component, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { LogtoService } from '@logto/angular';

@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.component.html',
})
export class AppComponent {
readonly logto = inject(LogtoService);

async signIn() {
await this.logto.signIn({
redirectUri: 'http://localhost:3000/callback',
postRedirectUri: window.location.origin,
});
}

async signOut() {
await this.logto.signOut('http://localhost:3000/');
}
}

在模板中直接读取 isLoading()、isAuthenticated() 和 error() 信号:

app/app.component.html
@if (logto.error(); as error) {
<p role="alert">{{ error.message }}</p>
} @if (logto.isLoading()) {
<p>加载中...</p>
} @else if (logto.isAuthenticated()) {
<button type="button" (click)="signOut()">登出</button>
} @else {
<button type="button" (click)="signIn()">登录</button>
}

<router-outlet />

请将 <router-outlet /> 放在认证 (Authentication) 条件之外,这样回调可以在用户登录前渲染。

调用 .signOut() 将清除内存和 localStorage 中所有的 Logto 数据(如果存在)。

检查点:测试你的应用程序​

现在,你可以测试你的应用程序:

  1. 运行你的应用程序,你将看到登录按钮。
  2. 点击登录按钮,SDK 将初始化登录过程并将你重定向到 Logto 登录页面。
  3. 登录后,你将被重定向回你的应用程序,并看到登出按钮。
  4. 点击登出按钮以清除令牌存储并登出。

获取用户信息​

显示用户信息​

要显示用户的信息,可以使用 getIdTokenClaims() 从 ID 令牌 (ID token) 中读取声明 (Claims),无需额外的网络请求。在你的 AppComponent 中添加一个 effect,当 isAuthenticated() 变为 true 时加载声明 (Claims),包括恢复现有会话时。导入 JsonPipe 以显示结果:

app/app.component.ts
import { JsonPipe } from '@angular/common';
import { Component, effect, inject, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { LogtoService, type IdTokenClaims } from '@logto/angular';

@Component({
selector: 'app-root',
standalone: true,
imports: [JsonPipe, RouterOutlet],
templateUrl: './app.component.html',
})
export class AppComponent {
readonly logto = inject(LogtoService);
readonly user = signal<IdTokenClaims | undefined>(undefined);

constructor() {
effect(() => {
if (!this.logto.isAuthenticated()) {
this.user.set(undefined);
return;
}

void this.logto
.getIdTokenClaims()
.then((claims) => {
this.user.set(claims);
})
.catch(() => {
// SDK 会通过 logto.error() 向模板暴露错误。
});
});
}

// ...保留上一步的 signIn() 和 signOut() 方法
}

在你的模板的 logto.isAuthenticated() 分支内添加以下内容:

app/app.component.html
@if (user(); as claims) {
<pre>{{ claims | json }}</pre>
}

请求额外声明 (Claims)​

你可能会发现从 getIdTokenClaims() 返回的对象中缺少一些用户信息。这是因为 OAuth 2.0 和 OpenID Connect (OIDC) 的设计遵循最小权限原则 (PoLP),而 Logto 是基于这些标准构建的。

默认情况下,返回的声明(Claim)是有限的。如果你需要更多信息,可以请求额外的权限(Scope)以访问更多的声明(Claim)。

信息:

“声明(Claim)”是关于主体的断言;“权限(Scope)”是一组声明。在当前情况下,声明是关于用户的一条信息。

以下是权限(Scope)与声明(Claim)关系的非规范性示例:

提示:

“sub” 声明(Claim)表示“主体(Subject)”,即用户的唯一标识符(例如用户 ID)。

Logto SDK 将始终请求三个权限(Scope):openid、profile 和 offline_access。

在你的 provideLogto 配置中添加 scopes:

app/app.config.ts
import { type ApplicationConfig } from '@angular/core';
import { provideLogto, UserScope } from '@logto/angular';

export const appConfig: ApplicationConfig = {
providers: [
provideLogto({
endpoint: '<your-logto-endpoint>',
appId: '<your-app-id>',
scopes: [
UserScope.Email,
UserScope.Phone,
UserScope.CustomData,
UserScope.Identities,
UserScope.Organizations,
],
}),
// ...其他 providers
],
};

更改 scopes 后请重新登录。额外的 ID 令牌 (ID token) 声明 (Claims),如 email 和 phone_number,将可通过 getIdTokenClaims() 获取,并由上面的示例显示。

需要网络请求的声明​

为了防止 ID 令牌 (ID token) 过大,一些声明需要通过网络请求来获取。例如,即使在权限中请求了 custom_data 声明,它也不会包含在用户对象中。要访问这些声明,你可以使用 fetchUserInfo() 方法:

app/app.component.ts
// 将此方法添加到 AppComponent 并在登录后调用。
async loadUserInfo() {
const userInfo = await this.logto.fetchUserInfo();
// 现在你可以访问 userInfo.custom_data、userInfo.identities 等。
return userInfo;
}
该方法将通过请求 userinfo 端点来获取用户信息。要了解更多可用的权限和声明,请参阅 权限和声明部分。

fetchUserInfo() 可以与 API 资源 (API resource) 访问令牌 (Access token) 一起使用。配置 resources 并不会阻止 SDK 请求用户信息。

权限 (Scopes) 与声明 (Claims)​

Logto 使用 OIDC 权限 (Scopes) 和声明 (Claims) 约定 来定义用于从 ID 令牌 (ID token) 和 OIDC userinfo 端点 获取用户信息的权限 (Scopes) 和声明 (Claims)。"scope" 和 "claim" 都是 OAuth 2.0 和 OpenID Connect (OIDC) 规范中的术语。

对于标准 OIDC 声明 (Claims),其在 ID 令牌 (ID token) 中的包含严格由所请求的权限 (Scopes) 决定。扩展声明 (Claims)(如 custom_data 和 organizations)可以通过 自定义 ID 令牌 (Custom ID token) 设置额外配置到 ID 令牌 (ID token) 中。

以下是支持的权限 (Scopes) 及其对应的声明 (Claims) 列表:

标准 OIDC 权限 (Scopes)​

openid(默认)

Claim nameTypeDescription
substring用户的唯一标识符

profile(默认)

Claim nameTypeDescription
namestring用户的全名
usernamestring用户名
picturestring终端用户头像的 URL。该 URL 必须指向一个图片文件(例如 PNG、JPEG 或 GIF 图片文件),而不是包含图片的网页。请注意,该 URL 应专门指向适合在描述终端用户时显示的头像,而不是终端用户拍摄的任意照片。
created_atnumber终端用户创建的时间。该时间以自 Unix 纪元(1970-01-01T00:00:00Z)以来的毫秒数表示。
updated_atnumber终端用户信息最后更新时间。该时间以自 Unix 纪元(1970-01-01T00:00:00Z)以来的毫秒数表示。

其他 标准声明 (Claims) 包括 family_name、given_name、middle_name、nickname、preferred_username、profile、website、gender、birthdate、zoneinfo 和 locale 也会包含在 profile 权限 (Scope) 中,无需请求 userinfo 端点。与上表声明 (Claims) 不同的是,这些声明 (Claims) 仅在其值不为空时返回,而上表声明 (Claims) 的值为空时会返回 null。

备注:

与标准声明 (Claims) 不同,created_at 和 updated_at 声明 (Claims) 使用的是毫秒而不是秒。

email

Claim nameTypeDescription
emailstring用户的电子邮件地址
email_verifiedboolean电子邮件地址是否已被验证

phone

Claim nameTypeDescription
phone_numberstring用户的电话号码
phone_number_verifiedboolean电话号码是否已被验证

address

关于 address 声明 (Claim) 的详细信息,请参阅 OpenID Connect Core 1.0。

信息:

带有 (默认) 标记的权限 (Scopes) 总是由 Logto SDK 请求。当请求相应权限 (Scope) 时,标准 OIDC 权限 (Scopes) 下的声明 (Claims) 总是包含在 ID 令牌 (ID token) 中——无法关闭。

扩展权限 (Scopes)​

以下权限 (Scopes) 由 Logto 扩展,并将通过 userinfo 端点 返回声明 (Claims)。这些声明 (Claims) 也可以通过 控制台 > 自定义 JWT 配置为直接包含在 ID 令牌 (ID token) 中。详见 自定义 ID 令牌 (ID token)。

custom_data

Claim nameTypeDescriptionIncluded in ID token by default
custom_dataobject用户的自定义数据

identities

Claim nameTypeDescriptionIncluded in ID token by default
identitiesobject用户关联的身份
sso_identitiesarray用户关联的 SSO 身份

roles

Claim nameTypeDescriptionIncluded in ID token by default
rolesstring[]用户的角色 (Roles)✅

urn:logto:scope:organizations

Claim nameTypeDescriptionIncluded in ID token by default
organizationsstring[]用户所属的组织 (Organizations) ID✅
organization_dataobject[]用户所属的组织 (Organizations) 数据
备注:

这些组织 (Organizations) 声明 (Claims) 也可以在使用 不透明令牌 (Opaque token) 时通过 userinfo 端点获取。但不透明令牌 (Opaque tokens) 不能作为组织令牌 (Organization tokens) 用于访问组织专属资源。详见 不透明令牌 (Opaque token) 与组织 (Organizations)。

urn:logto:scope:organization_roles

Claim nameTypeDescriptionIncluded in ID token by default
organization_rolesstring[]用户所属组织 (Organizations) 的角色 (Roles),格式为 <organization_id>:<role_name>✅

API 资源​

我们建议首先阅读 🔐 基于角色的访问控制 (RBAC),以了解 Logto RBAC 的基本概念以及如何正确设置 API 资源。

配置 Logto 客户端​

一旦你设置了 API 资源,就可以在应用中配置 Logto 时添加它们:

app/app.config.ts
import { type ApplicationConfig } from '@angular/core';
import { provideLogto } from '@logto/angular';

export const appConfig: ApplicationConfig = {
providers: [
provideLogto({
endpoint: '<your-logto-endpoint>',
appId: '<your-app-id>',
resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'],
}),
// ...other providers
],
};

每个 API 资源都有其自己的权限(权限)。

例如,https://shopping.your-app.com/api 资源具有 shopping:read 和 shopping:write 权限,而 https://store.your-app.com/api 资源具有 store:read 和 store:write 权限。

要请求这些权限,你可以在应用中配置 Logto 时添加它们:

app/app.config.ts
import { type ApplicationConfig } from '@angular/core';
import { provideLogto } from '@logto/angular';

export const appConfig: ApplicationConfig = {
providers: [
provideLogto({
endpoint: '<your-logto-endpoint>',
appId: '<your-app-id>',
scopes: ['shopping:read', 'shopping:write', 'store:read', 'store:write'], // 权限 (Scopes)
resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'], // API 资源
}),
// ...other providers
],
};

你可能会注意到权限是与 API 资源分开定义的。这是因为 OAuth 2.0 的资源指示器 指定请求的最终权限将是所有目标服务中所有权限的笛卡尔积。

因此,在上述情况下,权限可以从 Logto 中的定义简化,两个 API 资源都可以拥有 read 和 write 权限,而无需前缀。然后,在 Logto 配置中:

app/app.config.ts
import { type ApplicationConfig } from '@angular/core';
import { provideLogto } from '@logto/angular';

export const appConfig: ApplicationConfig = {
providers: [
provideLogto({
endpoint: '<your-logto-endpoint>',
appId: '<your-app-id>',
scopes: ['read', 'write'], // 权限 (Scopes)
resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'], // API 资源
}),
// ...other providers
],
};

对于每个 API 资源,它将请求 read 和 write 权限。

备注:

请求 API 资源中未定义的权限是可以的。例如,即使 API 资源没有可用的 email 权限,你也可以请求 email 权限。不可用的权限将被安全地忽略。

成功登录后,Logto 将根据用户的角色向 API 资源发布适当的权限。

更改资源或权限 (Scopes) 后请重新登录,以便用户可以授权更新后的配置。

获取 API 资源的访问令牌 (Access token)​

要获取特定 API 资源的访问令牌 (access token),你可以使用 getAccessToken() 方法:

app/api-resource.component.ts
import { Component, inject, signal } from '@angular/core';
import { LogtoService } from '@logto/angular';

@Component({
selector: 'app-api-resource',
standalone: true,
template: `
@if (logto.error(); as error) {
<p role="alert">{{ error.message }}</p>
}
@if (logto.isAuthenticated()) {
<button type="button" [disabled]="logto.isLoading()" (click)="loadAccessToken()">
获取 API 访问令牌 (Access token)
</button>
<pre>{{ accessToken() }}</pre>
}
`,
})
export class ApiResourceComponent {
readonly logto = inject(LogtoService);
readonly accessToken = signal('');

async loadAccessToken() {
this.accessToken.set(await this.logto.getAccessToken('https://shopping.your-app.com/api'));
}
}

此方法将返回一个 JWT 访问令牌 (access token),当用户具有相关权限时,可以用来访问 API 资源。如果当前缓存的访问令牌 (access token) 已过期,此方法将自动尝试使用刷新令牌 (refresh token) 获取新的访问令牌 (access token)。

请使用你配置中的精确资源标识符。每次发起 API 请求时调用 getAccessToken(resource),这样 SDK 能返回有效的令牌,而不是在组件中无限期保存令牌。

获取组织令牌 (Organization tokens)​

如果你对组织不熟悉,请阅读 🏢 组织(多租户) 以开始了解。

在配置 Logto 客户端时,你需要添加 UserScope.Organizations 权限:

app/app.config.ts
import { type ApplicationConfig } from '@angular/core';
import { provideLogto, UserScope } from '@logto/angular';

export const appConfig: ApplicationConfig = {
providers: [
provideLogto({
endpoint: '<your-logto-endpoint>',
appId: '<your-app-id>',
scopes: [UserScope.Organizations],
}),
// ...other providers
],
};

用户登录后,你可以获取用户的组织令牌:

app/organizations.component.ts
import { Component, effect, inject, signal } from '@angular/core';
import { LogtoService } from '@logto/angular';

@Component({
selector: 'app-organizations',
standalone: true,
template: `
@if (logto.error(); as error) {
<p role="alert">{{ error.message }}</p>
}
@if (logto.isAuthenticated()) {
<ul>
@for (organizationId of organizationIds(); track organizationId) {
<li>
<span>{{ organizationId }}</span>
<button
type="button"
[disabled]="logto.isLoading()"
(click)="loadOrganizationToken(organizationId)"
>
获取组织令牌 (Organization token)
</button>
</li>
}
</ul>
<pre>{{ organizationToken() }}</pre>
}
`,
})
export class OrganizationsComponent {
readonly logto = inject(LogtoService);
readonly organizationIds = signal<string[]>([]);
readonly organizationToken = signal('');

constructor() {
effect(() => {
if (!this.logto.isAuthenticated()) {
this.organizationIds.set([]);
this.organizationToken.set('');
return;
}

void this.logto
.getIdTokenClaims()
.then((claims) => {
this.organizationIds.set(claims.organizations ?? []);
})
.catch(() => {
// SDK 会通过 logto.error() 向模板暴露错误信息。
});
});
}

async loadOrganizationToken(organizationId: string) {
this.organizationToken.set(await this.logto.getOrganizationToken(organizationId));
}
}

将 UserScope.Organizations 与已有的权限 (Scopes) 合并,并在更新配置后重新登录。getOrganizationToken(organizationId) 会返回所选 Logto 组织 (Organization) 的令牌;如需 API 资源的令牌,请使用 getAccessToken(resource)。

将访问令牌 (Access token) 附加到请求头​

将令牌放入 Authorization HTTP 头中,使用 Bearer 格式(Bearer YOUR_TOKEN)。例如,可以将此方法添加到注入了 LogtoService 的认证组件中:

async fetchProducts() {
const accessToken = await this.logto.getAccessToken('https://shopping.your-app.com/api');
const response = await fetch('https://shopping.your-app.com/api/products', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});

if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}

return response.json();
}
备注:

示例中使用了 fetch。如果你使用 Angular 的 HttpClient,请在请求选项中设置同样的 Authorization 头。

延伸阅读​

终端用户流程:认证 (Authentication) 流程、账户流程和组织流程 配置连接器 (Connectors) 授权 (Authorization)