Chuyển đến nội dung chính

第 21 課:整合 React、Angular 和 Node.js

將Keycloak與前端整合(React使用keycloak-js或react-oidc-context,Angular使用angular-auth-oidc-client)、身份驗證狀態管理、靜默令牌刷新、受保護的路由、基於角色的UI渲染。後端 Node.js/Express 具有 Passport-keycloak-connect 或 jose JWT 驗證、中介軟體驗證、API 閘道模式。

🔒 DevSecOps — 第 21 課 第 21 課:整合 React、Angular 和 Node.js

Keycloak從基礎到高級

第 6 部分:整合實際應用__HTMLTAG_60___

xdev.asia

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)和微服務架構整合。