1. フロントエンドと Node.js の統合の概要
Keycloakをフロントエンドアプリケーションと統合する場合、次を使用します。PKCE を使用した認証コード フロー(コード交換用の証明キー) — シングル ページ アプリケーション (SPA) およびパブリック クライアント向けの最も標準的で安全なフロー。
┌───────────┐ ┌──────────┐ ┌──────────────┐
│ React / │────▶│ Keycloak │ │ Node.js │
│ Angular │ │ Server │ │ Backend API │
│ (SPA) │ └────┬─────┘ └──────┬───────┘
│ │ │ │
│ 1. Login │──────────▶ 2. Auth Code │
│ │◀────────── + PKCE │
│ 3. Exchange code │ │
│ for tokens ─────▶│ │
│ │◀──── 4. Access Token │
│ │ + Refresh Token │
│ 5. API call with Bearer token ────────▶│
│ │◀──── 6. Protected response │
└───────────┘ │
│
7. Validate JWT ──────────────▶│
(jwks-uri) │
| プラットフォーム | 図書館 | アプローチ |
|---|---|---|
| 反応する | 反応-oidc-コンテキスト(推奨) /キークローク-js | OIDC クライアント/Keycloak アダプター |
| 角度のある | 角度認証-oidc-クライアント | OIDCクライアント |
| Node.js | ホセ / パスポート-キークローク-接続 | JWT 検証 / パスポート戦略 |
2. React + Keycloak の統合
2.1 アプローチ 1:react-oidc-context (推奨)
反応-oidc-コンテキスト最新の OIDC ベースのライブラリですoidc-クライアント-ts、新しい React アプリケーションに推奨:
# Cài đặt dependencies
npm install react-oidc-context oidc-client-ts
2.1.1 AuthProviderのセットアップ
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { AuthProvider } from 'react-oidc-context';
import App from './App';
const oidcConfig = {
authority: 'http://localhost:8080/realms/my-realm',
client_id: 'my-react-client',
redirect_uri: window.location.origin + '/callback',
post_logout_redirect_uri: window.location.origin,
// PKCE code flow (default và recommended)
response_type: 'code',
scope: 'openid profile email',
// Silent refresh
automaticSilentRenew: true,
// Lưu tokens trong sessionStorage (an toàn hơn localStorage)
userStore: undefined, // Mặc định sessionStorage
};
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<AuthProvider {...oidcConfig}>
<App />
</AuthProvider>
</React.StrictMode>
);
2.1.2 useAuthフックの使用法
// src/components/UserInfo.tsx
import { useAuth } from 'react-oidc-context';
function UserInfo() {
const auth = useAuth();
if (auth.isLoading) {
return <div>Loading...</div>;
}
if (auth.error) {
return <div>Error: {auth.error.message}</div>;
}
if (!auth.isAuthenticated) {
return (
<div>
<p>Bạn chưa đăng nhập</p>
<button onClick={() => auth.signinRedirect()}>
Đăng nhập
</button>
</div>
);
}
return (
<div>
<h2>Welcome, {auth.user?.profile.preferred_username}</h2>
<p>Email: {auth.user?.profile.email}</p>
<p>Token expires in: {auth.user?.expires_in}s</p>
<button onClick={() => auth.removeUser()}>
Đăng xuất
</button>
</div>
);
}
export default UserInfo;
2.1.3 アクセストークンを使用した API 呼び出し
// src/hooks/useApi.ts
import { useAuth } from 'react-oidc-context';
import { useCallback } from 'react';
export function useApi() {
const auth = useAuth();
const fetchWithAuth = useCallback(
async (url: string, options: RequestInit = {}) => {
if (!auth.user?.access_token) {
throw new Error('Not authenticated');
}
const response = await fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${auth.user.access_token}`,
'Content-Type': 'application/json',
},
});
if (response.status === 401) {
// Token expired, trigger silent renew
await auth.signinSilent();
// Retry request
return fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${auth.user.access_token}`,
'Content-Type': 'application/json',
},
});
}
return response;
},
[auth]
);
return { fetchWithAuth };
}
// Sử dụng trong component
function UserList() {
const { fetchWithAuth } = useApi();
const [users, setUsers] = useState([]);
useEffect(() => {
fetchWithAuth('http://localhost:8081/api/users')
.then(res => res.json())
.then(data => setUsers(data));
}, [fetchWithAuth]);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.username}</li>
))}
</ul>
);
}
2.1.4 保護されたルート
// src/components/PrivateRoute.tsx
import { useAuth } from 'react-oidc-context';
import { Navigate } from 'react-router-dom';
interface PrivateRouteProps {
children: React.ReactNode;
requiredRoles?: string[];
}
function PrivateRoute({ children, requiredRoles = [] }: PrivateRouteProps) {
const auth = useAuth();
if (auth.isLoading) {
return <div>Loading...</div>;
}
if (!auth.isAuthenticated) {
// Redirect to Keycloak login
auth.signinRedirect();
return <div>Redirecting to login...</div>;
}
// Kiểm tra roles nếu được yêu cầu
if (requiredRoles.length > 0) {
const userRoles = getUserRoles(auth.user);
const hasRequiredRole = requiredRoles.some(role =>
userRoles.includes(role)
);
if (!hasRequiredRole) {
return <Navigate to="/unauthorized" replace />;
}
}
return <>{children}</>;
}
// Extract roles từ Keycloak token claims
function getUserRoles(user: any): string[] {
if (!user?.profile) return [];
const realmRoles = user.profile.realm_access?.roles || [];
const clientRoles =
user.profile.resource_access?.['my-react-client']?.roles || [];
return [...realmRoles, ...clientRoles];
}
export default PrivateRoute;
// src/App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import PrivateRoute from './components/PrivateRoute';
import Home from './pages/Home';
import Dashboard from './pages/Dashboard';
import AdminPanel from './pages/AdminPanel';
import Unauthorized from './pages/Unauthorized';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
{/* Protected route - chỉ cần authenticated */}
<Route path="/dashboard" element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
} />
{/* Protected route - yêu cầu role ADMIN */}
<Route path="/admin" element={
<PrivateRoute requiredRoles={['ADMIN']}>
<AdminPanel />
</PrivateRoute>
} />
<Route path="/unauthorized" element={<Unauthorized />} />
</Routes>
</BrowserRouter>
);
}
export default App;
2.1.5 ロールベースの UI レンダリング
// src/components/RoleBasedContent.tsx
import { useAuth } from 'react-oidc-context';
function getUserRoles(user: any): string[] {
if (!user?.profile) return [];
return user.profile.realm_access?.roles || [];
}
function hasRole(user: any, role: string): boolean {
return getUserRoles(user).includes(role);
}
function Navigation() {
const auth = useAuth();
const user = auth.user;
return (
<nav>
<ul>
<li><a href="/">Home</a></li>
{auth.isAuthenticated && (
<li><a href="/dashboard">Dashboard</a></li>
)}
{/* Chỉ hiển thị cho ADMIN */}
{hasRole(user, 'ADMIN') && (
<>
<li><a href="/admin">Admin Panel</a></li>
<li><a href="/admin/users">User Management</a></li>
</>
)}
{/* Chỉ hiển thị cho EDITOR */}
{hasRole(user, 'EDITOR') && (
<li><a href="/content">Content Management</a></li>
)}
</ul>
</nav>
);
}
export default Navigation;
2.2 アプローチ 2: keycloak-js アダプター
キークローク-jsKeycloak の公式アダプターです。まだ正常に動作しますが、反応-oidc-コンテキストOIDC 標準が強化されているため、新しいプロジェクトが優先されます。
npm install keycloak-js
// src/keycloak.ts
import Keycloak from 'keycloak-js';
const keycloak = new Keycloak({
url: 'http://localhost:8080',
realm: 'my-realm',
clientId: 'my-react-client',
});
export default keycloak;
// src/components/KeycloakProvider.tsx
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import Keycloak from 'keycloak-js';
import keycloak from '../keycloak';
interface KeycloakContextType {
keycloak: Keycloak;
initialized: boolean;
authenticated: boolean;
}
const KeycloakContext = createContext<KeycloakContextType>({
keycloak,
initialized: false,
authenticated: false,
});
export function useKeycloak() {
return useContext(KeycloakContext);
}
interface KeycloakProviderProps {
children: ReactNode;
}
export function KeycloakProvider({ children }: KeycloakProviderProps) {
const [initialized, setInitialized] = useState(false);
const [authenticated, setAuthenticated] = useState(false);
useEffect(() => {
keycloak
.init({
onLoad: 'check-sso', // hoặc 'login-required'
pkceMethod: 'S256', // PKCE method
checkLoginIframe: false, // Tắt iframe check (recommended)
silentCheckSsoRedirectUri:
window.location.origin + '/silent-check-sso.html',
})
.then((auth) => {
setAuthenticated(auth);
setInitialized(true);
// Setup token refresh
if (auth) {
setInterval(() => {
keycloak
.updateToken(70) // Refresh nếu token hết hạn trong 70s
.catch(() => {
console.warn('Token refresh failed, logging out');
keycloak.logout();
});
}, 60000); // Check mỗi 60 giây
}
})
.catch((err) => {
console.error('Keycloak init failed:', err);
setInitialized(true);
});
}, []);
if (!initialized) {
return <div>Loading Keycloak...</div>;
}
return (
<KeycloakContext.Provider value={{ keycloak, initialized, authenticated }}>
{children}
</KeycloakContext.Provider>
);
}
// 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>
);
}
2.3 SPAのKeycloakクライアント構成
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
3. Angular + Keycloak の統合
3.1 angular-auth-oidc-client をインストールする
npm install angular-auth-oidc-client
3.2 OIDCモジュールの構成
// 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,
},
}),
],
};
3.3 認証サービス
// 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;
}
}
3.4 認証ガード
// 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;
})
);
};
3.5 HTTP インターセプター
// 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);
})
);
};
3.6 ルーティング設定
// 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
),
},
];
3.7 コンポーネントは認証を使用します
// 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 || [];
});
}
}
4. Node.js/Express + Keycloakの統合
4.1 アプローチ 1: jose JWT 検証 (推奨)
図書館を利用するホセJWT トークンを検証するには、フレームワーク固有のアダプターに依存しない軽量のアプローチを使用します。
npm install jose express
npm install -D @types/express typescript
// 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();
};
}
4.2 エクスプレスアプリケーション
// 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;
4.3 アプローチ 2: パスポート-キークローク-接続
プロジェクトですでに Passport.js を使用している場合は、それを使用できますパスポート-キークローク-接続:
npm install passport passport-keycloak-connect express-session
npm install -D @types/passport @types/express-session
// src/passport-setup.ts
import passport from 'passport';
import KeycloakStrategy from 'passport-keycloak-connect';
import session from 'express-session';
import express from 'express';
const app = express();
// Session configuration (required for Passport)
app.use(session({
secret: process.env.SESSION_SECRET || 'your-session-secret',
resave: false,
saveUninitialized: false,
cookie: { secure: false }, // true cho HTTPS
}));
app.use(passport.initialize());
app.use(passport.session());
// Keycloak strategy
const keycloakConfig = {
host: 'http://localhost:8080',
realm: 'my-realm',
clientId: 'my-node-client',
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
callbackURL: '/auth/callback',
};
passport.use('keycloak', new KeycloakStrategy(keycloakConfig,
(accessToken: string, refreshToken: string, profile: any, done: Function) => {
// Profile chứa user info từ Keycloak
return done(null, {
id: profile.id,
username: profile.username,
email: profile.email,
roles: profile.realm_access?.roles || [],
});
}
));
passport.serializeUser((user: any, done) => done(null, user));
passport.deserializeUser((user: any, done) => done(null, user));
// Routes
app.get('/auth/login',
passport.authenticate('keycloak', { scope: ['openid', 'profile', 'email'] })
);
app.get('/auth/callback',
passport.authenticate('keycloak', { failureRedirect: '/login-failed' }),
(req, res) => {
res.redirect('/dashboard');
}
);
app.get('/api/me',
(req, res, next) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: 'Not authenticated' });
}
next();
},
(req, res) => {
res.json(req.user);
}
);
5. サイレントトークンリフレッシュ
トークンの更新は、シームレスなユーザー エクスペリエンスを確保するために重要です。主な方法は 2 つあります。
5.1 リフレッシュトークンのローテーション
Client ──── Access Token (5 min) ────▶ API Server
│ │
│ Token expired (401) │
│◀───────────────────────────│
│ │
Client ──── Refresh Token ──────────▶ Keycloak
│ │
│ New Access Token │
│ + New Refresh Token │
│◀───────────────────────────│
│ │
Client ──── New Access Token ────────▶ API Server
5.2 反応-oidc-contextの自動更新
// Đã 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;
}
5.3 SSO HTML のサイレントチェック
ファイルの作成public/silent-check-sso.htmlサイレント SSO チェックの場合:
<!DOCTYPE html>
<html>
<head>
<title>Silent Check SSO</title>
</head>
<body>
<script>
parent.postMessage(location.href, location.origin);
</script>
</body>
</html>
6. Keycloakクライアント設定リファレンス
次のタイプのアプリケーションについては、「Keycloak クライアント構成」を参照してください。
| 設定 | SPA (React/Angular) | Node.js バックエンド |
|---|---|---|
| クライアントプロトコル | オープンID接続 | オープンID接続 |
| アクセスタイプ | 公共。公共 | 機密。機密 |
| 標準流量 | の上 | オン (Web アプリの場合) |
| 直接アクセス | オフ | オフ |
| サービスアカウント | オフ | オン (必要な場合) |
| PKCE | S256 | 該当なし |
| 有効なリダイレクト URI | http://localhost:3000/* | http://localhost:8081/* |
| ウェブオリジン | http://localhost:3000 | + |
7. まとめ
| プラットフォーム | 図書館 | アドバンテージ | 注記 |
|---|---|---|---|
| 反応する | 反応-oidc-コンテキスト | OIDC標準、APIフック、自動リフレッシュ | リダイレクト URI を正しく構成する必要がある |
| 反応する | キークローク-js | 公式アダプター、完全な API | Keycloakと密接に連携 |
| 角度のある | 角度認証-oidc-クライアント | Angular ネイティブ、ガード、インターセプター | ReactよりもConfigが複雑 |
| Node.js | ホセ | 軽量、依存性なし、標準 | ミドルウェアを自分で実装する必要がある |
| Node.js | パスポート-キークローク-接続 | Passport.js エコシステムを統合する | セッション管理が必要 |
次の記事では、Keycloak を API Gateway (Nginx、Kong、Traefik) およびマイクロサービス アーキテクチャと統合する方法を学びます。