在 Angular 19 中实现 OpenID Connect 身份验证(无 NgModules)
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)
}
对受保护模块进行懒加载可以提升初始加载性能。
我遇到的挑战
| Issue | Root Cause | Fix |
|---|---|---|
| Login appeared to do nothing | Active SSO session | Set prompt=login |
| Tokens were returned but Angular did not recognize authentication | Missing checkAuth on startup | Added provideAppInitializer |
| After login it kept returning to the login page | Incorrect guard observable typing | Corrected the guard implementation |
| Redirect URL landed on login instead of dashboard | – | Redirect inside LoginComponent when authenticated |
最后思考
在 Angular 19 中实现 OpenID Connect 且不使用 NgModules,在了解生命周期后实际上相当简洁。
关键要点
- 启动时始终调用
checkAuth。 - 让库来管理令牌。
- 保持守卫(guards)简洁。
- 理解重定向 URL 如何影响路由。
- 仅在必要时强制弹出登录提示。
身份验证更多是关于理解流程,而不是编写代码。一旦理清流程,一切都将变得可预期。
如果你在 Angular 19、Node 20 以及独立 API 上实现此方案,这种结构应能为你节省数小时的调试时间——也可能减少一些挫败感。
编码愉快!