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

Angular 17集成Keycloak页面刷新后自动登出问题求助

问题:Angular 17集成Keycloak刷新页面后意外重定向到登录页

我在集成Keycloak进行身份认证的Angular 17应用中遇到异常:应用使用keycloak-angular和keycloak-js库管理认证,通过继承KeycloakAuthGuard的自定义AuthGuard保护路由。首次登录流程正常,但用户刷新页面时,尽管本地存储存在有效令牌,仍会被意外重定向至Keycloak登录页。

技术栈版本

  • Angular CLI: 17.3.8
  • Node.js: v18.16.0
  • keycloak-angular: ^15.3.0
  • keycloak-js: ^23.0.7

请问该问题源于Angular代码还是Keycloak配置?


appconfig.ts

import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { hitlInterceptor } from './core/interceptors/hitl.interceptor';
import { provideToastr } from 'ngx-toastr';
import { KeycloakService } from 'keycloak-angular';
import { AuthService } from './core/services/auth.service';
import { initializeKeycloak } from './core/keycloak_init/keycloak-init.factory';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(withInterceptors([hitlInterceptor])),
    provideAnimationsAsync(),
    provideToastr(),
    {
      provide: APP_INITIALIZER,
      useFactory: initializeKeycloak,
      multi: true,
      deps: [KeycloakService],
    },
    AuthService,
    KeycloakService,
  ],
};

auth guard

import { Injectable } from '@angular/core';
import {
  ActivatedRouteSnapshot,
  Router,
  RouterStateSnapshot,
} from '@angular/router';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';

@Injectable({
  providedIn: 'root',
})
export class authGuard extends KeycloakAuthGuard {
  constructor(
    protected override readonly router: Router,
    protected readonly keycloak: KeycloakService
  ) {
    super(router, keycloak);
  }

  public async isAccessAllowed(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ) {
    if (!this.authenticated) {
      await this.keycloak.login({
        redirectUri: window.location.origin + state.url,
      });
    }
    // return true;
    // Get the roles required from the route.
    const requiredRoles = route.data['roles'];
    // Allow the user to proceed if no additional roles are required to access the route.
    if (!Array.isArray(requiredRoles) || requiredRoles.length === 0) {
      return true;
    }
    // Allow the user to proceed if all the required roles are present.
    return requiredRoles.every((role) => this.roles.includes(role));
  }
}

keycloak.init.ts

import { KeycloakService, KeycloakEventType } from 'keycloak-angular';
import Keycloak from 'keycloak-js';
function storeTokens(keycloakInstance: Keycloak) {
  if (keycloakInstance.token) {
    localStorage.setItem(`keycloak_token`, keycloakInstance.token);
  }
  if (keycloakInstance.refreshToken) {
    localStorage.setItem(
      `keycloak_refreshToken`,
      keycloakInstance.refreshToken
    );
  }
}

export function initializeKeycloak(
  keycloak: KeycloakService
): () => Promise<boolean> {
  return (): Promise<boolean> =>
    new Promise(async (resolve, reject) => {
      try {
        await keycloak.init({
          config: {
       
            url: 'http://cs.com/keycloak/auth',
            realm: 'realm',
            clientId: 'frontend',
          },
          initOptions: {
            onLoad: 'check-sso',
            silentCheckSsoRedirectUri:
              window.location.origin + '/assets/silent-check-sso.html',
          },
          enableBearerInterceptor: true,
          bearerExcludedUrls: ['/assets'],
        });

        const keycloakInstance = keycloak.getKeycloakInstance();
        const isLoggedIn = await keycloak.isLoggedIn();

        if (isLoggedIn) {
          storeTokens(keycloakInstance);
        } else {
          await keycloak.login();
          storeTokens(keycloakInstance);
        }

        keycloak.keycloakEvents$.subscribe({
          next: (event) => {
            if (event.type === KeycloakEventType.OnAuthRefreshSuccess) {
              storeTokens(keycloakInstance);
            }
          },
        });

        resolve(true);
      } catch (error) {
        console.error('Keycloak initialization failed', error);
        reject(error);
      }
    });
}

问题分析与修复方案

这个问题主要源于Angular代码的初始化时序和逻辑问题,与Keycloak配置无关,具体问题点及修复方式如下:

1. 核心问题:路由守卫执行早于Keycloak初始化完成

刷新页面时,Angular的路由守卫会优先于APP_INITIALIZER完成前触发,此时this.authenticated还未被Keycloak正确初始化,导致守卫误判为未认证,直接跳转登录。

2. 初始化逻辑冗余导致误判

你在初始化中使用了onLoad: 'check-sso'(该选项会自动静默校验用户登录状态),但额外添加的isLoggedIn判断+强制登录逻辑,覆盖了check-sso的正常流程,导致初始化时序紊乱时误触发登录。

3. 自定义令牌存储未被复用

你手动将令牌存入localStorage,但Keycloak-js本身会自行管理令牌,手动存储并未让Keycloak在初始化时读取这些令牌,反而增加了逻辑复杂度。


修复方案

方案一:调整AuthGuard逻辑,等待Keycloak初始化完成

修改守卫的isAccessAllowed方法,确保先完成Keycloak初始化再判断认证状态:

public async isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
  // 确保Keycloak完成初始化
  await this.keycloak.init({
    config: {
      url: 'http://cs.com/keycloak/auth',
      realm: 'realm',
      clientId: 'frontend',
    },
    initOptions: {
      onLoad: 'check-sso',
      silentCheckSsoRedirectUri: window.location.origin + '/assets/silent-check-sso.html',
    }
  });

  if (!this.authenticated) {
    await this.keycloak.login({
      redirectUri: window.location.origin + state.url,
    });
  }

  // 原有角色校验逻辑
  const requiredRoles = route.data['roles'];
  if (!Array.isArray(requiredRoles) || requiredRoles.length === 0) {
    return true;
  }
  return requiredRoles.every((role) => this.roles.includes(role));
}

方案二:简化Keycloak初始化逻辑

删除冗余的isLoggedIn判断和强制登录,让check-sso自然处理登录校验:

export function initializeKeycloak(keycloak: KeycloakService): () => Promise<boolean> {
  return (): Promise<boolean> =>
    new Promise(async (resolve, reject) => {
      try {
        // init返回值直接表示是否认证成功
        const authenticated = await keycloak.init({
          config: {
            url: 'http://cs.com/keycloak/auth',
            realm: 'realm',
            clientId: 'frontend',
          },
          initOptions: {
            onLoad: 'check-sso',
            silentCheckSsoRedirectUri: window.location.origin + '/assets/silent-check-sso.html',
          },
          enableBearerInterceptor: true,
          bearerExcludedUrls: ['/assets'],
        });

        const keycloakInstance = keycloak.getKeycloakInstance();
        if (authenticated) {
          storeTokens(keycloakInstance);
        }

        keycloak.keycloakEvents$.subscribe({
          next: (event) => {
            if (event.type === KeycloakEventType.OnAuthRefreshSuccess) {
              storeTokens(keycloakInstance);
            }
          },
        });

        resolve(true);
      } catch (error) {
        console.error('Keycloak初始化失败', error);
        reject(error);
      }
    });
}

方案三:确保静默SSO文件存在

确认/assets目录下存在silent-check-sso.html文件,内容如下:

<!DOCTYPE html>
<html>
<body>
  <script>
    parent.postMessage(location.href, location.origin);
  </script>
</body>
</html>

该文件是check-sso模式静默校验令牌的必要依赖。


内容的提问来源于stack exchange,提问作者Dileep Gireesh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 03:24:51