HTMLTAG_65__HTMLTAG_66___1。前端和 Node.js 整合概述___HTMLTAG_67__HTMLTAG_68
將 Keycloak 與前端應用程式整合時,我們使用 帶有 PKCE 的授權代碼流(代碼交換的證明密鑰)- 單頁應用程式 (SPA) 和公共客戶端最標準、最安全的流程。
預編碼_0
___HTMLTAG_75__HTMLTAG_76___平台___HTMLTAG_77__HTMLTAG_78___函式庫___HTMLTAG_79__HTMLTAG_80___方法___HTMLTAG_81__HTMLTAG_82___ ___HTMLTAG_85__HTMLTAG_86___React___HTMLTAG_87__HTMLTAG_88__HTMLTAG_89___react-oidc-context(建議)/keycloak-js(建議)/keyclo_-js___HTMLTAG_____客戶端/Keycloak適配器___HTMLTAG_95__HTMLTAG_96___
___HTMLTAG_97__HTMLTAG_98___Angular____HTMLTAG_99__HTMLTAG_100__HTMLTAG_101___angular-auth-oidc-client___HTMLTAG_102__HTMLTAG_103__HTMLTAG_104___MLTAG_102__HTMLTAG_103__HTMLTAG_104___MLTAG_102__HTMLTAG_103__HTMLTAG_104___MLTAG_102__HTMLTAG_103__HTMLTAG_104____DCG10101074107070700707070707070707070707070707070707_707]。
___HTMLTAG_107__HTMLTAG_108___Node.js___HTMLTAG_109__HTMLTAG_110__HTMLTAG_111___jose / __passport-keycloak-conconnect___MLTAGMLGMLG113_____5驗證/護照策略___HTMLTAG_117__HTMLTAG_118___
HTMLTAG_121__HTMLTAG_122___2。 React + Keycloak 整合___HTMLTAG_123__HTMLTAG_124
___HTMLTAG_125__HTMLTAG_126___2.1 方法 1:react-oidc-context(建議)HTMLTAG_127__HTMLTAG_128
___HTMLTAG_129__HTMLTAG_130___react-oidc-context 是一個現代 OIDC 函式庫,基於 oidc-client-ts,推薦用於新的 React 應用程式:
預編碼_1
2.1.1 AuthProvider 設定
預編碼_2
2.1.2 useAuth 掛鉤用法
預編碼_3
2.1.3 使用存取權杖的 API 呼叫
預編碼_4
2.1.4 受保護的路由__HTMLTAG_142___
預編碼_5
預編碼_6
2.1.5 角色為基礎的 UI 渲染
預編碼_7
HTMLTAG_145__HTMLTAG_146___2.2 方法 2:keycloak-js 適配器___HTMLTAG_147__HTMLTAG_148
___HTMLTAG_149__HTMLTAG_150___keycloak-js 是 Keycloak 的官方適配器。仍然運作良好,但 react-oidc-context 優先用於新項目,因為它更符合 OIDC 標準:
預編碼_8
預編碼_9
預編碼_10
// Sử dụng keycloak-js trong component
import { useKeycloak } from './KeycloakProvider';
function Profile() {
const { keycloak, authenticated } = useKeycloak();
if (!authenticated) {
return <button onClick={() => keycloak.login()}>Login</button>;
}
return (
<div>
<h2>{keycloak.tokenParsed?.preferred_username}</h2>
<p>Email: {keycloak.tokenParsed?.email}</p>
{/* Role-based rendering */}
{keycloak.hasRealmRole('ADMIN') && (
<p>🔑 You are an Admin</p>
)}
{keycloak.hasResourceRole('editor', 'my-react-client') && (
<p>✏️ You have editor access</p>
)}
<button onClick={() => keycloak.logout()}>Logout</button>
</div>
);
}
HTMLTAG_155__HTMLTAG_156___2.3 SPA 的 Keycloak 用戶端設定___HTMLTAG_157__HTMLTAG_158
在 Keycloak 中為 React/Angular SPA 建立客戶端:
Client Settings:
Client ID: my-react-client
Client Protocol: openid-connect
Access Type: public (SPA không có client_secret)
Standard Flow: ON (Authorization Code Flow)
Direct Access: OFF (không dùng cho SPA)
Implicit Flow: OFF (deprecated, dùng PKCE thay thế)
Valid Redirect URIs:
- http://localhost:3000/*
- http://localhost:4200/*
Valid Post Logout Redirect URIs:
- http://localhost:3000/*
- http://localhost:4200/*
Web Origins:
- http://localhost:3000
- http://localhost:4200
- + (cho phép tất cả origin từ redirect URIs)
Advanced Settings:
Proof Key for Code Exchange (PKCE): S256
Access Token Lifespan: 5 minutes
Refresh Token: Enable
HTMLTAG_161__HTMLTAG_162___3。 Angular + Keycloak 整合___HTMLTAG_163__HTMLTAG_164
HTMLTAG_165__HTMLTAG_166___3.1 安裝 Angular-auth-oidc-client___HTMLTAG_167__HTMLTAG_168
npm install angular-auth-oidc-client
HTMLTAG_169__HTMLTAG_170___3.2 OIDC 模組配置___HTMLTAG_171__HTMLTAG_172
// src/app/app.config.ts (Angular 17+ standalone)
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAuth, LogLevel } from 'angular-auth-oidc-client';
import { routes } from './app.routes';
import { authInterceptor } from './auth/auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withInterceptors([authInterceptor])),
provideAuth({
config: {
authority: 'http://localhost:8080/realms/my-realm',
redirectUrl: window.location.origin + '/callback',
postLogoutRedirectUri: window.location.origin,
clientId: 'my-angular-client',
scope: 'openid profile email',
responseType: 'code',
// Silent renew
silentRenew: true,
silentRenewUrl: window.location.origin + '/silent-renew.html',
useRefreshToken: true,
// Logging
logLevel: LogLevel.Debug,
// Auto login
autoUserInfo: true,
// Token renewal
renewTimeBeforeTokenExpiresInSec: 30,
},
}),
],
};
HTMLTAG_173__HTMLTAG_174___3.3 驗證服務___HTMLTAG_175__HTMLTAG_176
// src/app/auth/auth.service.ts
import { Injectable, inject } from '@angular/core';
import { OidcSecurityService } from 'angular-auth-oidc-client';
import { map } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class AuthService {
private oidcService = inject(OidcSecurityService);
isAuthenticated$ = this.oidcService.isAuthenticated$.pipe(
map((result) => result.isAuthenticated)
);
userData$ = this.oidcService.userData$;
login(): void {
this.oidcService.authorize();
}
logout(): void {
this.oidcService
.logoff()
.subscribe((result) => console.log('Logged out', result));
}
getAccessToken(): string {
let token = '';
this.oidcService
.getAccessToken()
.subscribe((t) => (token = t));
return token;
}
hasRole(role: string): boolean {
let hasRole = false;
this.userData$.subscribe((userData) => {
const realmRoles = userData?.userData?.realm_access?.roles || [];
hasRole = realmRoles.includes(role);
});
return hasRole;
}
getUserRoles(): string[] {
let roles: string[] = [];
this.userData$.subscribe((userData) => {
roles = userData?.userData?.realm_access?.roles || [];
});
return roles;
}
}
HTMLTAG_177__HTMLTAG_178___3.4 驗證___HTMLTAG_179__HTMLTAG_180
// src/app/auth/auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { OidcSecurityService } from 'angular-auth-oidc-client';
import { map, take } from 'rxjs/operators';
export const authGuard: CanActivateFn = (route, state) => {
const oidcService = inject(OidcSecurityService);
const router = inject(Router);
return oidcService.isAuthenticated$.pipe(
take(1),
map(({ isAuthenticated }) => {
if (!isAuthenticated) {
oidcService.authorize();
return false;
}
// Kiểm tra required roles nếu có
const requiredRoles = route.data?.['roles'] as string[];
if (requiredRoles && requiredRoles.length > 0) {
// Kiểm tra roles từ token
const token = oidcService.getPayloadFromAccessToken();
const userRoles: string[] =
token?.realm_access?.roles || [];
const hasRole = requiredRoles.some((role) =>
userRoles.includes(role)
);
if (!hasRole) {
router.navigate(['/unauthorized']);
return false;
}
}
return true;
})
);
};
HTMLTAG_181__HTMLTAG_182___3.5 HTTP 攔截器___HTMLTAG_183__HTMLTAG_184
// src/app/auth/auth.interceptor.ts
import { HttpInterceptorFn, HttpHandlerFn, HttpRequest } from '@angular/common/http';
import { inject } from '@angular/core';
import { OidcSecurityService } from 'angular-auth-oidc-client';
import { switchMap, take } from 'rxjs/operators';
export const authInterceptor: HttpInterceptorFn = (
req: HttpRequest<unknown>,
next: HttpHandlerFn
) => {
const oidcService = inject(OidcSecurityService);
// Chỉ thêm token cho API calls
if (!req.url.includes('/api/')) {
return next(req);
}
return oidcService.getAccessToken().pipe(
take(1),
switchMap((token) => {
if (token) {
const clonedReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
},
});
return next(clonedReq);
}
return next(req);
})
);
};
HTMLTAG_185__HTMLTAG_186___3.6 路由設定___HTMLTAG_187__HTMLTAG_188
// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from './auth/auth.guard';
import { AutoLoginPartialRoutesGuard } from 'angular-auth-oidc-client';
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./pages/home/home.component').then((m) => m.HomeComponent),
},
{
path: 'callback',
loadComponent: () =>
import('./pages/callback/callback.component').then(
(m) => m.CallbackComponent
),
},
{
path: 'dashboard',
loadComponent: () =>
import('./pages/dashboard/dashboard.component').then(
(m) => m.DashboardComponent
),
canActivate: [authGuard],
},
{
path: 'admin',
loadComponent: () =>
import('./pages/admin/admin.component').then(
(m) => m.AdminComponent
),
canActivate: [authGuard],
data: { roles: ['ADMIN'] },
},
{
path: 'unauthorized',
loadComponent: () =>
import('./pages/unauthorized/unauthorized.component').then(
(m) => m.UnauthorizedComponent
),
},
];
HTMLTAG_189__HTMLTAG_190___3.7 元件使用 Auth___HTMLTAG_191__HTMLTAG_192
// src/app/pages/dashboard/dashboard.component.ts
import { Component, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { OidcSecurityService } from 'angular-auth-oidc-client';
import { AuthService } from '../../auth/auth.service';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [CommonModule],
template: `
<div *ngIf="isAuthenticated">
<h2>Dashboard</h2>
<p>Username: {{ userData?.preferred_username }}</p>
<p>Email: {{ userData?.email }}</p>
<p>Roles: {{ roles.join(', ') }}</p>
<div *ngIf="authService.hasRole('ADMIN')">
<h3>Admin Section</h3>
<p>Nội dung chỉ dành cho Admin</p>
</div>
<button (click)="authService.logout()">Logout</button>
</div>
`,
})
export class DashboardComponent implements OnInit {
authService = inject(AuthService);
private oidcService = inject(OidcSecurityService);
isAuthenticated = false;
userData: any = null;
roles: string[] = [];
ngOnInit(): void {
this.oidcService.isAuthenticated$.subscribe(
(result) => (this.isAuthenticated = result.isAuthenticated)
);
this.oidcService.userData$.subscribe((result) => {
this.userData = result.userData;
this.roles = this.userData?.realm_access?.roles || [];
});
}
}
HTMLTAG_193__HTMLTAG_194___4。 Node.js/Express + Keycloak 整合___HTMLTAG_195__HTMLTAG_196
___HTMLTAG_197__HTMLTAG_198___4.1 方法 1:jose JWT 驗證(建議)HTMLTAG_199__HTMLTAG_200
使用函式庫 jose 驗證 JWT 令牌 - 輕量級方法,不依賴於特定框架的適配器:
預編碼_20
// src/middleware/auth.ts
import { createRemoteJWKSet, jwtVerify, JWTPayload } from 'jose';
import { Request, Response, NextFunction } from 'express';
// Keycloak configuration
const KEYCLOAK_URL = process.env.KEYCLOAK_URL || 'http://localhost:8080';
const REALM = process.env.KEYCLOAK_REALM || 'my-realm';
const ISSUER = `${KEYCLOAK_URL}/realms/${REALM}`;
const JWKS_URI = `${ISSUER}/protocol/openid-connect/certs`;
// Tạo JWKS client (tự cache keys)
const JWKS = createRemoteJWKSet(new URL(JWKS_URI));
// Extended Request type
interface AuthenticatedRequest extends Request {
user?: {
sub: string;
username: string;
email: string;
roles: string[];
clientRoles: string[];
};
}
// JWT verification middleware
export async function authenticate(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): Promise<void> {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
res.status(401).json({ error: 'Missing or invalid Authorization header' });
return;
}
const token = authHeader.substring(7);
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: ISSUER,
// Optionally validate audience
// audience: 'my-client',
});
// Extract user info from JWT claims
req.user = {
sub: payload.sub || '',
username: (payload as any).preferred_username || '',
email: (payload as any).email || '',
roles: extractRealmRoles(payload),
clientRoles: extractClientRoles(payload, 'my-client'),
};
next();
} catch (err) {
console.error('JWT verification failed:', err);
res.status(401).json({ error: 'Invalid or expired token' });
}
}
// Extract realm roles từ JWT
function extractRealmRoles(payload: JWTPayload): string[] {
const realmAccess = (payload as any).realm_access;
return realmAccess?.roles || [];
}
// Extract client roles từ JWT
function extractClientRoles(
payload: JWTPayload,
clientId: string
): string[] {
const resourceAccess = (payload as any).resource_access;
return resourceAccess?.[clientId]?.roles || [];
}
// Role check middleware factory
export function requireRole(...roles: string[]) {
return (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): void => {
if (!req.user) {
res.status(401).json({ error: 'Not authenticated' });
return;
}
const userRoles = [...req.user.roles, ...req.user.clientRoles];
const hasRole = roles.some((role) => userRoles.includes(role));
if (!hasRole) {
res.status(403).json({
error: 'Forbidden',
message: `Required roles: ${roles.join(', ')}`,
});
return;
}
next();
};
}
HTMLTAG_205__HTMLTAG_206___4.2 快速申請___HTMLTAG_207__HTMLTAG_208
// src/app.ts
import express from 'express';
import cors from 'cors';
import { authenticate, requireRole } from './middleware/auth';
const app = express();
// Middleware
app.use(cors({
origin: ['http://localhost:3000', 'http://localhost:4200'],
credentials: true,
}));
app.use(express.json());
// ========== Public Routes ==========
app.get('/api/public/health', (req, res) => {
res.json({ status: 'UP' });
});
// ========== Protected Routes ==========
// Tất cả routes dưới đây yêu cầu authentication
app.use('/api', authenticate);
app.get('/api/me', (req: any, res) => {
res.json({
username: req.user.username,
email: req.user.email,
roles: req.user.roles,
});
});
// User routes
app.get('/api/users', requireRole('USER', 'ADMIN'), (req, res) => {
res.json({
message: 'User list',
users: [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' },
],
});
});
// Admin routes
app.get(
'/api/admin/dashboard',
requireRole('ADMIN'),
(req: any, res) => {
res.json({
message: 'Admin Dashboard',
adminUser: req.user.username,
});
}
);
app.post(
'/api/admin/users',
requireRole('ADMIN'),
(req: any, res) => {
res.json({
message: 'User created',
createdBy: req.user.username,
user: req.body,
});
}
);
// ========== Start Server ==========
const PORT = process.env.PORT || 8081;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
export default app;
HTMLTAG_209__HTMLTAG_210___4.3 方法 2:護照-鑰匙斗篷-連接___HTMLTAG_211__HTMLTAG_212
如果您在專案中使用了 Passport.js,則可以使用 passport-keycloak-connect:
npm install passport passport-keycloak-connect express-session
npm install -D @types/passport @types/express-session
預編碼_24
HTMLTAG_217__HTMLTAG_218___5。靜默令牌刷新___HTMLTAG_219__HTMLTAG_220
令牌刷新對於確保無縫的使用者體驗非常重要。主要有2個方法:
HTMLTAG_223__HTMLTAG_224___5.1 刷新令牌輪換___HTMLTAG_225__HTMLTAG_226
預編碼_25
HTMLTAG_227__HTMLTAG_228___5.2 React-oidc-context 自動刷新___HTMLTAG_229__HTMLTAG_230
// Đã bật trong config
const oidcConfig = {
// ...
automaticSilentRenew: true,
};
// Lắng nghe sự kiện token refresh
import { useAuth } from 'react-oidc-context';
function TokenMonitor() {
const auth = useAuth();
useEffect(() => {
// Sự kiện khi token được renew thành công
return auth.events.addAccessTokenExpiring(() => {
console.log('Token is about to expire, renewing...');
});
}, [auth.events]);
useEffect(() => {
return auth.events.addAccessTokenExpired(() => {
console.log('Token expired, signing in silently...');
auth.signinSilent().catch(() => {
// Silent renew failed, redirect to login
auth.signinRedirect();
});
});
}, [auth.events, auth]);
return null;
}
HTMLTAG_231__HTMLTAG_232___5.3 靜默檢查 SSO HTML___HTMLTAG_233__HTMLTAG_234
建立檔案 public/silent-check-sso.html 用於靜默 SSO 檢查:
預編碼_27
HTMLTAG_239__HTMLTAG_240___6。 Keycloak 用戶端設定參考___HTMLTAG_241__HTMLTAG_242
請參閱 Keycloak 用戶端設定以了解應用程式類型:
___HTMLTAG_247__HTMLTAG_248___設定___HTMLTAG_249__HTMLTAG_250___SPA(React/Angular)____HTMLTAG_251__HTMLTAG_252___Node.js 後端_MLTAG_252___Node.js 後端____ ___HTMLTAG_257__HTMLTAG_258___客戶端協定___HTMLTAG_259__HTMLTAG_260___openid-connect___HTMLTAG_261__HTMLTAG_262___openid-connect____HTMLTAG_263__HTMLTAG_262___openid-connect____HTMLTAG_263__HTMLTAG_264___ ___HTMLTAG_265__HTMLTAG_266___存取類型___HTMLTAG_267__HTMLTAG_268__HTMLTAG_269___公___HTMLTAG_270__HTMLTAG_271__HTMLTAG_272__HTMLTAG_270__HTMLTAG_271__HTMLTAG_272__HTMLTAGML_273____HTMLTAMLTA475____MLTAG ___HTMLTAG_277__HTMLTAG_278___標準流程___HTMLTAG_279__HTMLTAG_280___開啟___HTMLTAG_281__HTMLTAG_282___開啟(如果是網路應用)___HTMLTAG_283__HTMLTAG_284___ ___HTMLTAG_285__HTMLTAG_286___直接存取___HTMLTAG_287__HTMLTAG_288___關閉___HTMLTAG_289__HTMLTAG_290____關閉___HTMLTAG_291__HTMLTAG_292______ ___HTMLTAG_293__HTMLTAG_294___服務帳戶___HTMLTAG_295__HTMLTAG_296___關閉___HTMLTAG_297__HTMLTAG_298___開啟(如果需要)___HTMLTAG_299__HTMLTAG_300___ ___HTMLTAG_301__HTMLTAG_302___PKCE____HTMLTAG_303__HTMLTAG_304__HTMLTAG_305___S256____HTMLTAG_306__HTMLTAG_307__HTMLTAG_308___不適用___MLGML____41307________ ___HTMLTAG_311__HTMLTAG_312___有效重定向 URI___HTMLTAG_313__HTMLTAG_314__HTMLTAG_315__URL_1___>___HTMLTAG_316__HTMLTAG_317__HTMLTAG_318__URL_316__HTMLTAG_317__HTMLTAG_318__URL________20___4_______ ___HTMLTAG_321__HTMLTAG_322___網頁來源___HTMLTAG_323__HTMLTAG_324__HTMLTAG_325__URL_3___>___HTMLTAG_326__HTMLTAG_327__HTMLTAG_328___HTMLTAG_326__HTMLTAG_327__HTMLTAG_328___HTMLTAG_326__HTMLTAG_327__HTMLTAG_328___+___MLG____ML313____]HTMLTAG_334__HTMLTAG_335___7。摘要___HTMLTAG_336__HTMLTAG_337
___HTMLTAG_340__HTMLTAG_341___平台____HTMLTAG_342__HTMLTAG_343___函式庫___HTMLTAG_344__HTMLTAG_345_______HTMLTAG_346__HTMLTAG_347_____345_______HTMLTAG_346__HTMLTAG_347____________MLG____MLTAGMLHTG3_______________ ___HTMLTAG_352__HTMLTAG_353___React___HTMLTAG_354__HTMLTAG_355__HTMLTAG_356___react-oidc-context____HTMLTAG_357__H TMLTAG_358__HTMLTAG_359___OIDC標準、鉤子API、自動刷新___HTMLTAG_360__HTMLTAG_361___需要配置重定向URI正確___HTMLTAG_362__HTMLTAG_363___ ___HTMLTAG_364__HTMLTAG_365___React___HTMLTAG_366__HTMLTAG_367__HTMLTAG_368____keycloak-js___HTMLTAG_369__ HTMLTAG_370__HTMLTAG_371___官方適配器,完整API___HTMLTAG_372__HTMLTAG_373___緊密耦合鑰匙斗篷___HTMLTAG_374__HTMLTAG_375___ ___HTMLTAG_376__HTMLTAG_377___Angular____HTMLTAG_378__HTMLTAG_379__HTMLTAG_380___angular-auth-oidc-client___HTMLTAG_381__HTMLTAG_382__HTMLTAGular_383_____HTMLTAGng原生、守衛、攔截器___HTMLTAG_384__HTMLTAG_385___設定比反應___HTMLTAG_386__HTMLTAG_387___ ___HTMLTAG_388__HTMLTAG_389___Node.js___HTMLTAG_390__HTMLTAG_391__HTMLTAG_392____jose___HTMLTAG_393__HTM LTAG_394__HTMLTAG_395___輕量級,無依賴,標準___HTMLTAG_396__HTMLTAG_397___需要實現中間件你自己___HTMLTAG_398__HTMLTAG_399___ ___HTMLTAG_400__HTMLTAG_401___Node.js___HTMLTAG_402__HTMLTAG_403__HTMLTAG_404___passport-keycloak-connect___HTMLTAG_405__HTMLTAG_406__HTMLTAG_407___Passport.整合生態系統___HTMLTAG_408__HTMLTAG_409___需要會話管理___HTMLTAG_410__HTMLTAG_411___在下一篇文章中,我們將學習如何將Keycloak與API網關(Nginx、Kong、Traefik)和微服務架構整合。