在 Angular 19 中实现 OpenID Connect 身份验证(无 NgModules)

发布: (2026年2月26日 GMT+8 19:48)
6 分钟阅读
原文: Dev.to

Source: Dev.to

(抱歉,未提供需要翻译的正文内容。如能提供文章的其余部分,我将为您完成简体中文翻译。)

技术栈

  • Node v20.18.3
  • Angular 19.2.20
  • angular-auth-oidc-client
  • Identity Server 作为 OpenID 提供者
  • 没有 NgModules —— 一切都是独立的

第一步 – 安装 OIDC 客户端

npm install angular-auth-oidc-client

该库负责繁重的工作:令牌交换、存储、刷新以及状态处理。不要手动管理令牌。

步骤 2 – 在 app.config.ts 中配置身份验证

因为这是一个独立的 Angular 应用,所有配置都通过 ApplicationConfig 完成。

import { ApplicationConfig, provideAppInitializer, inject } from '@angular/core';
import { provideRouter, Router } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { importProvidersFrom } from '@angular/core';
import { AuthModule, StsConfigLoader, OidcSecurityService } from 'angular-auth-oidc-client';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),

    // OIDC module
    importProvidersFrom(
      AuthModule.forRoot({
        loader: {
          provide: StsConfigLoader,
          useFactory: OidcConfigLoaderFactory,
        },
      })
    ),

    // Run checkAuth on app start
    provideAppInitializer(() => {
      const oidcSecurityService = inject(OidcSecurityService);
      const router = inject(Router);

      return oidcSecurityService.checkAuth().toPromise().then(result => {
        if (result?.isAuthenticated) {
          router.navigate(['/dashboard']);
        }
      });
    })
  ]
};

重要: 在应用启动时调用 checkAuth 是强制性的。如果不这样做,来自 Identity Server 的重定向将不会被处理,登录看起来会出现问题。这是我遇到的第一个主要问题——令牌已返回,但 Angular 从未获取到它们。

Step 3 – OIDC 配置(config loader)

{
  authority: 'https://your-identity-server',
  redirectUrl: window.location.origin,
  postLogoutRedirectUri: window.location.origin,
  clientId: 'your-client-id',
  scope: 'openid profile api',
  responseType: 'code',
  silentRenew: true,
  useRefreshToken: true
}

提示: 始终在 PKCE 中使用 responseType: 'code'

第4步 – 实现登录

一个最小的登录服务:

login(): void {
  this.oidcSecurityService.authorize(undefined, {
    customParams: { prompt: 'login' }
  });
}

为什么使用 prompt=login

我最初看到应用在没有任何提示的情况下静默认证并返回令牌,因为 Identity Server 上已经存在 SSO 会话。添加 prompt=login 会强制显示登录页面,确保用户显式进行认证。

第5步 – 保护你的路由

路由

export const routes: Routes = [
  { path: 'login', component: LoginComponent },

  {
    path: '',
    component: LayoutComponent,
    canActivate: [AuthorizationGuard],
    children: [
      { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
      { path: 'dashboard', component: DashboardComponent },
      { path: 'management', component: LicenceManagementComponent }
    ]
  },

  { path: '**', redirectTo: '/login' }
];

守卫

canActivate(): Observable {
  return this.oidcSecurityService.checkAuth().pipe(
    map(({ isAuthenticated }) => {
      return isAuthenticated ? true : this.router.parseUrl('/login');
    })
  );
}

注意: 我最初把布尔值解构为对象 ({ isAuthenticated }),导致导航失败并产生重定向循环。正确处理 Observable 解决了此问题。

第6步 – 登录后重定向

如果 redirectUrl 指向 /login,用户在成功登录后会再次返回登录页。通过在确认已认证后进行导航来修复此问题。

ngOnInit(): void {
  this.securityService.isAuthenticated().subscribe(isAuth => {
    if (isAuth) {
      this.router.navigate(['/dashboard']);
    }
  });
}

现在已认证的用户不会停留在登录界面。

第7步 – 不要手动存储令牌

库已经通过 DefaultLocalStorageService 将令牌存储在 localStorage 中。如果需要获取个人资料信息,请通过该服务读取令牌:

this.oidcSecurityService.getAccessToken().subscribe(token => {
  const decoded = jwtDecode(token);
  console.log(decoded.sub);
});

保持令牌处理交给库——避免重复存储或手动解码。

额外奖励 – 懒加载已认证区域

{
  path: '',
  component: LayoutComponent,
  canActivate: [AuthorizationGuard],
  loadChildren: () =>
    import('./features/app.routes').then(m => m.APP_ROUTES)
}

对受保护模块进行懒加载可以提升初始加载性能。

我遇到的挑战

IssueRoot CauseFix
Login appeared to do nothingActive SSO sessionSet prompt=login
Tokens were returned but Angular did not recognize authenticationMissing checkAuth on startupAdded provideAppInitializer
After login it kept returning to the login pageIncorrect guard observable typingCorrected the guard implementation
Redirect URL landed on login instead of dashboardRedirect inside LoginComponent when authenticated

最后思考

Angular 19 中实现 OpenID Connect 且不使用 NgModules,在了解生命周期后实际上相当简洁。

关键要点

  • 启动时始终调用 checkAuth
  • 让库来管理令牌。
  • 保持守卫(guards)简洁。
  • 理解重定向 URL 如何影响路由。
  • 仅在必要时强制弹出登录提示。

身份验证更多是关于理解流程,而不是编写代码。一旦理清流程,一切都将变得可预期。

👉 GitHub Repository

如果你在 Angular 19、Node 20 以及独立 API 上实现此方案,这种结构应能为你节省数小时的调试时间——也可能减少一些挫败感。

编码愉快!

👉 For More Blogs

0 浏览
Back to Blog

相关文章

阅读更多 »