Angular集成Keycloak登录后请求Token返回401错误求助
问题现象
首次尝试集成Angular 15与Keycloak 20,应用可正常重定向至Keycloak登录页面,登录后重定向回应用却显示空白页。浏览器控制台提示请求https://my.keycloack.domain/realms/myRealm/protocol/openid-connect/token返回401状态码;Keycloak日志显示CODE_TO_TOKEN_ERROR,错误类型为invalid_client_credentials,但已确认用户登录密码正确。
应用配置代码
app.module.ts中的Keycloak初始化代码
import { HttpClient, HttpClientModule } from '@angular/common/http'; import { APP_INITIALIZER, NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { RouteReuseStrategy } from '@angular/router'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { CalendarModule, DateAdapter } from 'angular-calendar'; import { adapterFactory } from 'angular-calendar/date-adapters/date-fns'; import {NgxPaginationModule} from 'ngx-pagination'; import { NgxGoogleAnalyticsModule } from 'ngx-google-analytics'; import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { environment } from '../environments/environment.prod'; import { KeycloakService } from 'keycloak-angular'; import { LoginInfo } from '../main'; const initializeKeycloak = (keycloak: KeycloakService) => async () => keycloak.init({ config: { url: (JSON.parse(localStorage.getItem('loginInfo')) as LoginInfo).baseUrl, realm: (JSON.parse(localStorage.getItem('loginInfo')) as LoginInfo).realm, clientId: (JSON.parse(localStorage.getItem('loginInfo')) as LoginInfo).client }, initOptions: { onLoad: 'check-sso', silentCheckSsoRedirectUri: window.location.origin + '/assets/silent-check-sso.html' }, shouldAddToken: (request) => { const { method, url } = request; const isGetRequest = 'GET' === method.toUpperCase(); const acceptablePaths = ['/assets', '/clients/public']; const isAcceptablePathMatch = acceptablePaths.some((path) => url.includes(path)); return !(isGetRequest && isAcceptablePathMatch); } }); @NgModule({ declarations: [AppComponent], // eslint-disable-next-line max-len imports: [FormsModule, BrowserModule, IonicModule.forRoot(), AppRoutingModule, HttpClientModule, BrowserAnimationsModule, CalendarModule.forRoot({ provide: DateAdapter, useFactory: adapterFactory }), NgxPaginationModule, NgxGoogleAnalyticsModule.forRoot(environment.ga4MeasurementId)], providers: [KeycloakService, { provide: APP_INITIALIZER, useFactory: initializeKeycloak, multi: true, deps: [KeycloakService] }, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }], bootstrap: [AppComponent] }) export class AppModule {}
/assets/silent-check-sso.html文件
<html> <body> <script> parent.postMessage(location.href, location.origin); </script> </body> </html>
route-guard.service.ts路由守卫代码
import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, Router } from '@angular/router'; import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular'; @Injectable({ providedIn: 'root' }) export class RouteGuardService extends KeycloakAuthGuard implements CanActivate { constructor( protected readonly router: Router, protected readonly keycloak: KeycloakService) { super(router, keycloak); } public async isAccessAllowed( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ) { // Force the user to log in if currently unauthenticated. if (!this.authenticated) { await this.keycloak.login({ redirectUri: window.location.origin + state.url }); } // 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 (!(requiredRoles instanceof Array) || 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)); } }
app-routing.module.ts路由配置
import { NgModule } from '@angular/core'; import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; import { RouteGuardService } from './guard/route-guard.service'; const routes: Routes = [ { path: '', redirectTo: 'prenotazioni', pathMatch: 'full', }, { path: 'prenotazioni', loadChildren: () => import('./prenotazioni/prenotazioni.module').then( m => m.PrenotazioniPageModule), canActivate: [RouteGuardService] }, { path: 'agenda', loadChildren: () => import('./agenda/agenda.module').then( m => m.AgendaPageModule), canActivate: [RouteGuardService] }, { path: 'configuratore', loadChildren: () => import('./configuratore/configuratore.module').then( m => m.ConfiguratorePageModule), canActivate: [RouteGuardService] }, { path: 'login', loadChildren: () => import('./login/login.module').then( m => m.LoginPageModule) }, { path: 'customer-sat-page', loadChildren: () => import('./customer-sat-page/customer-sat-page.module').then( m => m.CustomerSatPagePageModule) }, ]; @NgModule({ imports: [ RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }) ], exports: [RouterModule] }) export class AppRoutingModule {}
排查方向及解决方法
客户端访问类型配置错误
Angular是前端单页应用,无法安全存储客户端密钥,需确保Keycloak客户端的Access Type设置为public。若设置为confidential,Keycloak会要求请求token时携带客户端密钥,前端无法提供有效密钥,直接触发invalid_client_credentials错误。重定向URI未添加至允许列表
检查Keycloak客户端设置中的Valid Redirect URIs,确认包含应用的完整域名及路径(如https://your-app-domain/*),同时需包含silent-check-sso.html的地址(如https://your-app-domain/assets/silent-check-sso.html)。重定向URI不在允许列表中时,Keycloak会拒绝token交换请求。客户端ID不匹配
验证localStorage.getItem('loginInfo')获取的clientId与Keycloak后台配置的客户端ID完全一致,注意大小写、特殊字符,ID不匹配会被判定为无效客户端凭证。初始化配置冗余参数
确认keycloak.init的配置中未添加credentials参数,public类型客户端无需提供客户端密钥,多余的凭证参数会导致请求验证失败。
内容的提问来源于stack exchange,提问作者Lucio Crusca

