Angular实现Google登录:成功后弹窗不关闭、无法获取用户信息
问题背景
在Angular项目中集成Google登录能力,同时引入@abacritt/angularx-social-login与angular-oauth2-oidc依赖包,参考示例实现自定义登录提供者GoogleAuthenticationService后,出现两个异常:
- 完成Google账号授权操作后,授权弹窗无法自动关闭
- 登录流程走完后无法获取用户详情,
signIn()方法无返回,控制台不打印用户信息
问题复现代码
google-authentication.service.ts
@Injectable() export class GoogleAuthenticationService implements LoginProvider { constructor(private readonly _oAuthService: OAuthService) { this.createConfiguration(); } private readonly _tokenReceived$ = this._oAuthService.events.pipe( filter((e) => e.type === 'token_received'), map(() => true as const) ); private createConfiguration(): void { let redirectUri = window.location.origin + window.location.pathname; if (redirectUri.endsWith('/')) { redirectUri = redirectUri.substring(0, redirectUri.length - 1); } this._oAuthService.configure({ issuer: 'https://accounts.google.com', strictDiscoveryDocumentValidation: false, redirectUri, silentRefreshRedirectUri: redirectUri, useSilentRefresh: true, clientId: environment.googleClientId, scope: 'openid profile email' }); } async initialize(autoLogin?: boolean): Promise<void> { await this._oAuthService.loadDiscoveryDocument(); if (autoLogin) { await this._oAuthService.tryLoginImplicitFlow(); } } async signIn(): Promise<SocialUser> { const tokenReceivedPromise = firstValueFrom(this._tokenReceived$); await this._oAuthService.initImplicitFlowInPopup(); await tokenReceivedPromise; return this.createUser(this._oAuthService.getIdToken()); } async getLoginStatus(): Promise<SocialUser> { if (this._oAuthService.hasValidIdToken()) { return this.createUser(this._oAuthService.getIdToken()); } else { throw `No user is currently logged in`; } } async signOut(revoke?: boolean): Promise<void> { if (revoke) { this._oAuthService.revokeTokenAndLogout(true, true); } else { this._oAuthService.logOut(true); } } private createUser(idToken: string): SocialUser { const user = new SocialUser(); const payload = JSON.parse(window.atob(idToken.split('.')[1])); user.idToken = idToken; user.id = payload.sub; user.name = payload.name; user.email = payload.email; user.photoUrl = payload.picture; user.firstName = payload['given_name']; user.lastName = payload['family_name']; return user; } }
login.component.ts
ngOnInit(): void { this._googleAuthService.initialize(); } signUpWithGoogle(event: Event): void { this._googleAuthService .signIn() .then((user) => { //not showing anything in console console.log(user); }) .catch((error) => { console.log(error); }); }
解决方案
问题根因
- 弹窗无法关闭:
initialize方法仅在autoLogin参数为true时才执行tryLoginImplicitFlow()解析URL中的token。授权完成后弹窗加载回调页时,没有主动解析URL携带的id_token参数,既不会触发token_received事件通知父窗口,也不会执行弹窗关闭逻辑。同时silentRefreshRedirectUri与主应用重定向地址配置相同,导致弹窗会加载完整Angular应用,增加不必要的加载开销。 - 无法获取用户信息:因为
token_received事件从未触发,firstValueFrom一直处于pending状态,后续构造用户对象的逻辑不会执行,所以控制台没有任何输出。 - 初始化时机错误:仅在登录组件的
ngOnInit中调用初始化方法,如果弹窗回调时路由匹配到其他页面,会因为服务未初始化完全无法解析token。
修复步骤
1. 修正自定义登录服务逻辑
调整OAuth配置、初始化逻辑与异常处理,修改后的google-authentication.service.ts代码如下:
import { Injectable } from '@angular/core'; import { LoginProvider, SocialUser } from '@abacritt/angularx-social-login'; import { OAuthService } from 'angular-oauth2-oidc'; import { filter, firstValueFrom, map, timeout, catchError, throwError } from 'rxjs'; import { environment } from 'src/environments/environment'; @Injectable() export class GoogleAuthenticationService implements LoginProvider { constructor(private readonly _oAuthService: OAuthService) { this.createConfiguration(); } private readonly _tokenReceived$ = this._oAuthService.events.pipe( filter((e) => e.type === 'token_received'), map(() => true as const), timeout(120000), catchError(() => throwError(() => new Error('登录超时,请重试'))) ); private createConfiguration(): void { let redirectUri = window.location.origin + window.location.pathname; if (redirectUri.endsWith('/')) { redirectUri = redirectUri.substring(0, redirectUri.length - 1); } this._oAuthService.configure({ issuer: 'https://accounts.google.com', strictDiscoveryDocumentValidation: true, redirectUri, silentRefreshRedirectUri: `${redirectUri}/assets/silent-refresh.html`, useSilentRefresh: true, clientId: environment.googleClientId, scope: 'openid profile email', skipUserInfo: true, responseType: 'id_token', requireHttps: environment.production }); } async initialize(autoLogin?: boolean): Promise<void> { await this._oAuthService.loadDiscoveryDocument(); // 无论是否开启自动登录,都要先解析URL中携带的token,处理弹窗/重定向回调 await this._oAuthService.tryLoginImplicitFlow(); if (autoLogin && !this._oAuthService.hasValidIdToken()) { await this._oAuthService.initImplicitFlow(); } } async signIn(): Promise<SocialUser> { if (this._oAuthService.hasValidIdToken()) { return this.createUser(this._oAuthService.getIdToken()); } const tokenReceivedPromise = firstValueFrom(this._tokenReceived$); await this._oAuthService.initImplicitFlowInPopup({ width: 480, height: 600 }); await tokenReceivedPromise; const idToken = this._oAuthService.getIdToken(); if (!idToken) { throw new Error('获取用户凭证失败,请重试'); } return this.createUser(idToken); } async getLoginStatus(): Promise<SocialUser> { if (this._oAuthService.hasValidIdToken()) { return this.createUser(this._oAuthService.getIdToken()); } throw new Error('当前无登录用户'); } async signOut(revoke?: boolean): Promise<void> { if (revoke) { await this._oAuthService.revokeTokenAndLogout(true, true); } else { this._oAuthService.logOut(true); } } private createUser(idToken: string): SocialUser { try { const payloadPart = idToken.split('.')[1]; if (!payloadPart) throw new Error('无效的token格式'); const payload = JSON.parse(window.atob(payloadPart)); const user = new SocialUser(); user.idToken = idToken; user.id = payload.sub; user.name = payload.name; user.email = payload.email; user.photoUrl = payload.picture; user.firstName = payload['given_name']; user.lastName = payload['family_name']; return user; } catch (e) { throw new Error('用户信息解析失败'); } } }
2. 添加弹窗回调空白页
在src/assets目录下新建silent-refresh.html文件,作为弹窗与静默刷新的专用回调页,避免加载完整Angular应用,内容如下:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>授权中</title> </head> <body> <script> // 回调触发后将hash参数传递给父窗口,自动关闭弹窗 if (window.parent && window.parent !== window) { window.parent.postMessage({ type: 'oauth_callback', hash: location.hash }, location.origin); } </script> </body> </html>
若项目的
angular.json中已默认配置assets目录静态资源放行,无需额外修改配置。
3. 提前服务初始化时机
不要仅在登录组件中初始化服务,在AppModule中通过APP_INITIALIZER让服务在应用启动时就完成初始化,确保任何路由下的回调都能被正确处理:
// app.module.ts import { APP_INITIALIZER, NgModule } from '@angular/core'; import { GoogleAuthenticationService } from './services/google-authentication.service'; export function initAuth(authService: GoogleAuthenticationService) { return () => authService.initialize(); } @NgModule({ // ...其他模块、组件声明 providers: [ { provide: APP_INITIALIZER, useFactory: initAuth, deps: [GoogleAuthenticationService], multi: true } ] }) export class AppModule { }
4. 调整登录组件代码
移除组件中重复的初始化调用,补充点击事件的默认行为阻止:
// login.component.ts constructor(private readonly _googleAuthService: GoogleAuthenticationService) {} signUpWithGoogle(event: Event): void { event.preventDefault(); this._googleAuthService .signIn() .then((user) => { console.log('登录成功', user); // 后续业务逻辑:存储用户信息、跳转首页等 }) .catch((error) => { console.error('登录失败', error); }); }
修复完成后,授权完成后弹窗会自动关闭,同时可以正常拿到解析后的用户信息。
内容的提问来源于stack exchange,提问作者RAHUL KUNDU
相关产品推荐
相关产品推荐

