You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
        });
}

解决方案

问题根因

  1. 弹窗无法关闭:initialize方法仅在autoLogin参数为true时才执行tryLoginImplicitFlow()解析URL中的token。授权完成后弹窗加载回调页时,没有主动解析URL携带的id_token参数,既不会触发token_received事件通知父窗口,也不会执行弹窗关闭逻辑。同时silentRefreshRedirectUri与主应用重定向地址配置相同,导致弹窗会加载完整Angular应用,增加不必要的加载开销。
  2. 无法获取用户信息:因为token_received事件从未触发,firstValueFrom一直处于pending状态,后续构造用户对象的逻辑不会执行,所以控制台没有任何输出。
  3. 初始化时机错误:仅在登录组件的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 23:18:19