Ionic集成Keycloak单点登录问题:错误排查与Deep Link实现
问题描述
我希望通过Keycloak实现SSO(单点登录)完成身份校验,但当前代码在模拟器运行时出现错误。同时需要实现Deep Link(深度链接)与InAppBrowser,以获取必要的Token,请问如何在现有代码中实现浏览器标签页与Deep Link功能来获取Token?
相关代码
app.module.ts
/* eslint-disable prefer-const */ import { ApplicationRef, DoBootstrap, NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { RouteReuseStrategy } from '@angular/router'; import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { HttpClientModule } from '@angular/common/http'; import { KeycloakService } from 'keycloak-angular'; import { CommonKeycloakService } from './services/keycloak/common-keycloak-service.service'; import { environment } from 'src/environments/environment'; import { InAppBrowser} from '@ionic-native/in-app-browser/ngx'; const keycloakService = new KeycloakService(); @NgModule({ declarations: [AppComponent], imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, HttpClientModule], providers: [CommonKeycloakService, { provide: KeycloakService, useValue: keycloakService }, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },InAppBrowser] // bootstrap: [AppComponent], }) export class AppModule implements DoBootstrap { constructor() { } ngDoBootstrap(appRef: ApplicationRef) { let configKeyClock: any = { url: environment.keycloak.url, realm: environment.keycloak.realm, clientId: environment.keycloak.clientId, }; keycloakService .init({ config: configKeyClock, initOptions: { onLoad: 'check-sso', checkLoginIframe: false, }, enableBearerInterceptor: true, }) .then(() => { console.log('[ngDoBootstrap] bootstrap app'); appRef.bootstrap(AppComponent); }) .catch((error: any) => console.error('[ngDoBootstrap] init Keycloak failed', error) ); } }
app.component.ts
/* eslint-disable max-len */ /* eslint-disable @typescript-eslint/dot-notation */ /* eslint-disable prefer-const */ import { Component } from '@angular/core'; import { InAppBrowser } from '@ionic-native/in-app-browser/ngx'; import { PrimaryService } from './primary.service'; import { CommonKeycloakService } from './services/keycloak/common-keycloak-service.service'; @Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrls: ['app.component.scss'], }) export class AppComponent { constructor(private user: PrimaryService, private commonKeycloakService: CommonKeycloakService, public keycloak: CommonKeycloakService,private iab: InAppBrowser) { } async ngOnInit() { await this.getAuthInfo(); if (sessionStorage.getItem('userDetails') == null) { this.login(); } }; login() { if (sessionStorage.getItem('userDeatails') == null) { let IDP = 'samlNew'; this.keycloak.login(IDP); const browser = this.iab.create('https://dbcluster.890enterprise.business/auth/realms/890Enterprise/protocol/openid-connect/auth', '_blank', 'location=yes'); } } async getAuthInfo() { if (this.commonKeycloakService.isUserLoggedIn()) { this.commonKeycloakService.keycloakAngular .loadUserProfile() .then(async (profiles) => { sessionStorage.setItem('userDetails', JSON.stringify(profiles)); // await this._httpService.generateJwt(profiles); let userInfo = sessionStorage.getItem('userDetails'); let userInfo1 = JSON.parse(userInfo); let userName: string = userInfo1['firstName']; // this._httpService.userName.next(userName); }) .catch((error) => { console.log('Error in loading profile' + JSON.stringify(error)); }); } else { } } }
Keycloak服务文件
/* eslint-disable max-len */ /* eslint-disable prefer-const */ /* eslint-disable @typescript-eslint/member-ordering */ import { Injectable } from '@angular/core'; import { KeycloakService } from 'keycloak-angular'; import { environment } from 'src/environments/environment'; import { Observable } from 'rxjs'; import { InAppBrowser as brand} from '@ionic-native/in-app-browser/ngx'; @Injectable({ providedIn: 'root' }) export class CommonKeycloakService { activityStart: any; constructor(public keycloakAngular: KeycloakService,public iab: brand) { // this.redirectUri = environment.keycloak.url + '/realms/' // + environment.keycloak.realm + '/protocol/openid-connect/logout?redirect_uri=' + // environment.baseUrl + '/index.html'; this.redirectUri = environment.keycloakRedirectBaseURl + '/index.html'; this.activityStart = Math.floor(Date.now() / 1000); } redirectUri: any; idpHint: string = environment.defaultTenant; tenantInfo: any; userInfo: any; loginId: any; private _tenantcontroller = `${environment.tenantUrl}`; isUserLoggedIn(): boolean { // return true; if (this.keycloakAngular.isLoggedIn()) { try { const userDetails = this.keycloakAngular.getUsername(); return true; } catch (e) { return false; } } else { return false; } } public getRedirectURI(): string { return this.redirectUri; } public logout() { this.signoutApi(); } private tInf() { let t: any = sessionStorage.getItem('tenantInfo'); return t; } private uInf() { let u: any = sessionStorage.getItem('userDetails'); return u; } public signoutApi() { this.keycloakAngular.logout(this.redirectUri); this.keycloakAngular.logout(environment.keycloakRedirectBaseURl + '/#/auth'); } public login(id: any) { this.idpHint = id; this.keycloakAngular.login({ idpHint: this.idpHint }); } public setTenantInfo(info: any) { sessionStorage.setItem('tenantInfo', JSON.stringify(info)); } public getTenantInfo() { return JSON.parse(this.tInf()); } public setLoginId(loginId: any) { sessionStorage.setItem('loginId', loginId); } public getLoginId() { let lgnID: any = sessionStorage.getItem('loginId'); return JSON.parse(lgnID); } public setTenantProfileInfo(info: any) { sessionStorage.setItem('tenantProfileInfo', JSON.stringify(info)); } public getuserDetails() { return JSON.parse(this.uInf()); } public getUserDetailsObservable(): Observable<any> { const userDetail = new Observable(observer => { setTimeout(() => { observer.next(JSON.parse(this.uInf())); }, 2500); }); return userDetail; } public getTenantProfileInfo1(): Observable<any> { let tPInfo: any = sessionStorage.getItem('tenantProfileInfo'); const tenantProfileInfo = new Observable(observer => { setTimeout(() => { observer.next(JSON.parse(tPInfo)); }, 1500); }); return tenantProfileInfo; } public getTenantProfileInfo() { let tPInfo: any = sessionStorage.getItem('tenantProfileInfo'); return JSON.parse(tPInfo); } public clearTenantInfo() { sessionStorage.removeItem('tenantInfo'); } public getUserDetails() { if (this.isUserLoggedIn()) { return JSON.stringify(this.keycloakAngular.loadUserProfile()); } else { return ''; } } public getToken() { return this.keycloakAngular.getToken(); } }
模拟器错误截图

解决方案
一、修复当前模拟器错误
从截图报错Uncaught (in promise): Error: Could not authenticate分析,核心问题是重复触发登录流程:login()方法同时调用了Keycloak默认登录和InAppBrowser,导致认证逻辑冲突。先修改app.component.ts的login()方法:
login() { // 修复拼写错误:userDeatails → userDetails if (sessionStorage.getItem('userDetails') == null) { let IDP = 'samlNew'; // 拼接完整的Keycloak授权URL,包含必要参数 const loginUrl = `${environment.keycloak.url}/realms/${environment.keycloak.realm}/protocol/openid-connect/auth` + `?client_id=${environment.keycloak.clientId}` + `&redirect_uri=${encodeURIComponent(environment.keycloakRedirectBaseURl)}` + `&response_type=code` + `&idp_hint=${IDP}`; // 用InAppBrowser打开登录页,替代Keycloak默认浏览器跳转 const browser = this.iab.create(loginUrl, '_blank', 'location=yes,hidden=no'); // 监听页面加载,捕获授权码回调 browser.on('loadstart').subscribe(event => { if (event.url.startsWith(environment.keycloakRedirectBaseURl)) { const urlParams = new URLSearchParams(event.url.split('?')[1]); const code = urlParams.get('code'); if (code) { browser.close(); // 用授权码交换Token this.commonKeycloakService.exchangeCodeForToken(code); } } }); } }
二、配置Deep Link接收回调
1. 移动端Deep Link插件配置(以Capacitor为例)
安装插件并同步配置:
npm install @capacitor/deeplinks npx cap sync
修改capacitor.config.ts:
import { CapacitorConfig } from '@capacitor/cli'; const config: CapacitorConfig = { appId: 'com.your.app.id', appName: 'YourApp', webDir: 'www', bundledWebRuntime: false, plugins: { Deeplinks: { appId: 'com.your.app.id', scheme: 'yourapp', // 自定义协议,如yourapp:// domains: ['your-domain.com'], // 可选:绑定域名回调 }, }, }; export default config;
2. Keycloak后台配置
在Keycloak管理后台找到对应客户端,修改Valid Redirect URIs为你的Deep Link地址,比如yourapp://callback/*或配置的域名地址。
3. 监听Deep Link回调
在app.component.ts添加监听逻辑:
import { Deeplinks } from '@capacitor/deeplinks'; async ngOnInit() { await this.setupDeepLinkListener(); await this.getAuthInfo(); if (sessionStorage.getItem('userDetails') == null) { this.login(); } }; async setupDeepLinkListener() { await Deeplinks.addListener('deepLinkOpen', async (event) => { const urlParams = new URLSearchParams(event.url.split('?')[1]); const code = urlParams.get('code'); if (code) { await this.commonKeycloakService.exchangeCodeForToken(code); } }); }
三、实现授权码交换Token
在CommonKeycloakService中添加Token交换方法:
import { HttpClient } from '@angular/common/http'; // 修改构造函数注入HttpClient constructor( public keycloakAngular: KeycloakService, public iab: brand, private http: HttpClient ) { // 原有代码... } async exchangeCodeForToken(code: string) { const tokenUrl = `${environment.keycloak.url}/realms/${environment.keycloak.realm}/protocol/openid-connect/token`; const body = new FormData(); body.append('grant_type', 'authorization_code'); body.append('client_id', environment.keycloak.clientId); body.append('code', code); body.append('redirect_uri', environment.keycloakRedirectBaseURl); try { const tokens = await this.http.post(tokenUrl, body).toPromise() as any; // 更新Keycloak实例的Token信息 this.keycloakAngular['instance'].token = tokens.access_token; this.keycloakAngular['instance'].refreshToken = tokens.refresh_token; this.keycloakAngular['instance'].idToken = tokens.id_token; // 加载用户信息 await this.loadUserProfile(); sessionStorage.setItem('userDetails', JSON.stringify(this.getuserDetails())); } catch (error) { console.error('Token交换失败:', error); } }
四、修复其他代码问题
- 修正
CommonKeycloakService中重复调用logout()的问题:
public signoutApi() { this.keycloakAngular.logout(environment.keycloakRedirectBaseURl + '/#/auth'); }
- 确保
environment.ts中配置正确的keycloakRedirectBaseURl,对应Deep Link地址或移动端可访问的回调地址。
五、验证流程
- 启动模拟器,App触发登录后打开InAppBrowser加载Keycloak登录页;
- 用户完成登录后,Keycloak跳转至配置的Deep Link,App捕获授权码;
- 自动完成Token交换,加载用户信息,完成SSO流程。
内容的提问来源于stack exchange,提问作者irfan shaikh
相关产品推荐
相关产品推荐

