웹 기술이 발전함에 따라 이를 보호하기 위한 방법과 프로토콜도 함께 발전하고 있습니다. OAuth 2.0 및 OpenID Connect 프로토콜은 새로운 보안 위협과 웹 애플리케이션의 증가하는 복잡성에 대응하여 크게 진화해 왔습니다. 한때 효과적이었던 전통적인 인증 방식은 이제 현대적인 단일 페이지 애플리케이션(SPA)에서는 시대에 뒤떨어지고 있으며, 새로운 보안 과제에 직면하고 있습니다. 이러한 맥락에서 SPA와 백엔드 시스템 간의 상호작용을 조직하는 데 권장되는 해결책으로 ‘Backend-For-Frontend(BFF)’ 아키텍처 패턴이 등장했습니다. BFF는 인증 및 세션 관리를 보다 안전하고 관리 가능한 방식으로 제공합니다. 이 글에서는 .NET과 React를 사용한 최소 솔루션을 통해 BFF 패턴의 실용적인 적용을 심도 있게 다루며, 이를 통해 웹 애플리케이션의 보안과 기능을 향상시키는 방법을 명확하게 이해할 수 있습니다.
역사적 배경
OAuth 2.0 및 OpenID Connect의 역사는 인터넷 기술의 지속적인 발전을 반영하고 있습니다. 이 프로토콜과 현대 웹 애플리케이션에 미치는 영향을 자세히 살펴보겠습니다.
2012년에 도입된 OAuth 2.0 프로토콜은 인증을 위한 널리 채택된 표준이 되었습니다. 이는 제3자 애플리케이션이 사용자의 자격 증명을 클라이언트에 노출하지 않고 사용자 리소스에 제한된 액세스를 얻을 수 있도록 허용합니다. OAuth 2.0은 다양한 사용 사례에 유연하게 적응할 수 있도록 설계된 여러 흐름을 지원합니다.
OAuth 2.0 기반으로 2014년 OpenID Connect (OIDC) 프로tokoll이 시작되었으며, 기본적인 인증 기능을 추가하였습니다. 클라이언트 应用程序에게 표준化的 접근 지점을 통해 사용자의 인증을 확인하고 JWT(JSON Web Token) 형식의 ID 토큰을 얻어 사용자의 기본 정보를 얻는 표준적인 방법을 제공합니다.
脅威 모델의 進化
SPA의 능력이 grow하고 인기가 기울이다 胁威 모델도 동시에 進化하였습니다. XSS(Cross-Site Scripting)과 CSRF(Cross-Site Request Forgery)과 같은 취약성은 더욱 널리 분산되었습니다. SPA는 자주 API를 통해 서버와 인터랙션하기 때문에 액세스 토큰과 리 freshing 토큰을 안전하게 보관하고 사용하는 것이 보안에서 중요해졌습니다.
시대의 요구에 따라 OAuth과 OpenID Connect 프로tokoll이 계속해서 進化하여 새로운 기술과 增长的 胁威에 맞게 변화합니다. 동시에 胁威의 계속 진화와 보안 관련 기술의 개선은 과거의 방법론이 현재의 보안 요구에 더 이상 만족되지 않는다는 것을 의미합니다. 따라서 OpenID Connect 프로tokoll은 현재 폭넓은 기능을 제공하고 있지만, 이 중 많은 기능은 이미 또는 скоро 과거의 기능으로 간주되며 보안에 취약하게 될 수 있습니다. 이러한 diversity는 SPA 개발자들이 OAuth 2.0과 OpenID Connect 서버에 대한 가장 적절하고 안전한 방법을 선택하는 데에 어려움을 引き起こす结果입니다.
특히 Implicit Flow는 이제 더 이상 권장되지 않으며, SPA, 모바일 애플리케이션, 데스크탑 애플리케이션을 포함한 모든 유형의 클라이언트에 대해 Authorization Code Flow와 Proof Key for Code Exchange (PKCE)를 함께 사용하는 것이 강력히 권장됩니다.
현대 SPA의 보안
Authorization Code Flow와 PKCE를 사용하는 경우에도 왜 현대 SPA가 여전히 취약하다고 간주되는가? 이에 대한 몇 가지 이유가 있습니다.
JavaScript 코드 취약성
JavaScript는 현대의 Single Page Applications(SPA)에서 중요한 역할을 하는 강력한 프로그래밍 언어입니다. 그러나 그 광범위한 기능과 보급력은 잠재적인 위협이 될 수 있습니다. React, Vue, Angular와 같은 라이브러리 및 프레임워크를 기반으로 구축된 현대 SPA는 방대한 수의 라이브러리와 의존성을 사용합니다. 이 라이브러리들은 node_modules
폴더에서 확인할 수 있으며, 그 수는 수백 또는 수천 개에 이를 수 있습니다. 이러한 라이브러리 각각은 다양한 정도의 심각성을 가진 취약성을 포함할 수 있으며, SPA 개발자는 사용된 모든 의존성 코드를 철저히 검사할 수 있는 능력을 갖추지 못한 경우가 많습니다. 종종 개발자들은 의존성이 서로 전이적으로 의존하기 때문에 전체 의존성 목록을 추적하지도 않습니다. 자신의 코드를 최고 수준의 품질과 보안 표준에 따라 개발한다고 해도, 완성된 애플리케이션에 취약성이 없다고 완전히 확신할 수는 없습니다.
악성 JavaScript 코드는 크로스 사이트 스크립(XSS) 공격이나 서파티 라이브러리의 손상을 통해 다양한 방식으로 애플리케이션에 주입될 수 있으며, 합법적인플리케이션 코드와 동일한 권한과 데이터 접근 수준을 얻습니다. 이를 통해 악드는 페이지에서 데이터를 훔치고, 애플리케이션 인터페이스와 상호작용하며, 백엔드로 요청을 보내고, 로컬 저장소(localStorage, IndexedDB)에서 데이터를 훔치며, 심지어 인증 세션을 자체적으로 시작하여 동일한 Authorization Code와 PKCE 흐름을 사용해 자체 접근 토큰을 얻을 수 있습니다.
Spectre 취약점
Spectre 취약점은 현대세서 아키텍처의 기능을 악용하여 격리되어야 할 데이터에 접근합니다. 이러한 취약점은 특히 SPAs에 위험합니다.
첫째, SPAs는 애플리케이션 상태를 관리하고 서버와 상호작용하기 위해 JavaScript를 집중적으로 사용합니다. 이는 Spectre 취약점을 악용할 수 있는 악성 JavaScript 코드에 대한 공격 표면을 증가시킵니다. 둘째, 전통적인 다중 페이지 애플리케이션(MPAs)과 달리 SPAs는 페이지를 거의 다시 로드하지 않으므로 로드된 코드가 오동안 활성 상태로 남습니다. 이는 공격자에게 악성 JavaScript 코드를 사용한 공격을 수행할 수 있는 시간을 크게 늘려줍니다.
Spectre 취약점은 공격자가 JavaScript 애플리케이션 메모리에 저장된 접근 토큰을 훔쳐 합적인 애플리케이션을 가장하여 보호된 리소스에 접근할 수 있게 합니다. 추측 실행을 통해 사용자 세션 데이터를 훔칠 수도 있어 SPA가 닫힌 후에도 공격을 계속할 수 있습니다.
미래에 Spectre와 유사한 다른 취약점이 발견될 가능성을 배제할 수 없습니다.
어떻게 해야 할까요?
중요한 중간 결론을 요약해 보겠습니다. 많은 수의 서드파티 자바스크립트 라이브러리에 의존하며 사용자 장치의 브라우저 환경에서 실행되는 현대적인 SPA는 개발자가 완전히 제어할 수 없는 소프트웨어 및 하드웨어 환경에서 작동합니다. 따라서 이러한 애플리케이션은 본질적으로 취약하다고 간주해야 합니다.
나열된 위협에 대응하기 위해, 더 많은 전문가들은 토큰을 브라우저에 저장하는 것을 완전히 피하고, 애플리케이션이 액세스 및 리프레시 토큰을 서버 측에서만 획득하고 처리하도록 설계해야 하며, 이 토큰들이 절대 브라우저 측으로 전달되지 않도록 해야 한다고 말합니다. 백엔드를 가진 SPA의 경우, 이는 Backend-For-Frontend (BFF) 아키텍처 패턴을 사용하여 달성할 수 있습니다.
인증 서버 (OP), BFF 패턴을 구현한 클라이언트 (RP), 서드파티 API (리소스 서버) 간의 상호 작용 방식은 다음과 같습니다:
BFF 패턴을 사용하여 SPA를 보호하면 여러 가지 이점이 있습니다. 액세스 및 리프레시 토큰이 서버 측에 저장되어 브라우저로 전달되지 않음으로써 취약성으로 인한 토큰 도난을 방지할 수 있습니다. 세션 및 토큰 관리는 서버에서 처리되어 더 나은 보안 제어와 신뢰할 수 있는 인증 검증이 가능합니다. 클라이언트 애플리케이션은 BFF를 통해 서버와 상호 작용하므로 애플리케이션 로직이 단순해지고 악성 코드 실행의 위험이 줄어듭니다.
.NET 플랫폼에서 Backend-For-Frontend 패턴 구현
.NET 플랫폼에서 BFF의 실제 구현으로 넘어가기 전에 필요한 구성 요소를 고려하고 우리의 행동을 계획해 봅시다. 이미 구성된 OpenID Connect 서버가 있고, 백엔드와 함께 작동하는 SPA를 개발하고 OpenID Connect를 사용한 인증을 구현하며 BFF 패턴을 사용하여 서버와 클라이언트 간의 상호작용을 조직해야 한다고 가정해 보겠습니다.
문서 브라우저 기반 애플리케이션을 위한 OAuth 2.0에 따르면, BFF 아키텍처 패턴은 백엔드가 OpenID Connect 클라이언트로 작동하고, PKCE를 사용한 Authorization Code Flow를 통해 인증하며, 액세스 및 리프레시 토큰을 자체적으로 획득하고 저장하며 절대 브라우저의 SPA 측으로 전달하지 않는다고 가정합니다. BFF 패턴은 또한 백엔드 측에 네 가지 주요 엔드포인트로 구성된 API의 존재를 가정합니다:
- 세션 확인: 활성 사용자 인증 세션을 확인하는 용도로 사용됩니다. 일반적으로 비동기 API(fetch)를 사용하여 SPA에서 호출되며, 성공 시 활성 사용자에 대한 정보를 반환합니다. 따라서 제3의 소스(예: CDN)에서 로드된 SPA는 인증 상태를 확인하고 사용자와의 작업을 계속하거나 OpenID Connect 서버를 사용하여 인증을 진행할 수 있습니다.
- 로그인: OpenID Connect 서버에 대한 인증 과정을 시작합니다. 보통, SPA가 단계 1에서 Check Session를 통해 인증된 사용자 데이터를 얻지 못하면, 브라우저를 이 URL로 리다이렉션합니다. 이 URL로 브라우저가 이동하면, OpenID Connect 서버로 compete request를 形成하여 리다이렉션합니다.
- 로그인: 인증 성공 시 서버로부터 보낸 인증 코드를 수신합니다. 인증 코드 + PKCE 코드 검증기를 사용하여 액세스 및 리프레시 토큰을 교환하기 위한 직접적인 요청을 OpenID Connect 서버에 보냅니다. 사용자에게 인증 쿠키를 발급하여 클라이언트 側에서 인증된 세션을 시작합니다.
- 로그아웃: 인증 세션을 종료하는 역할을 합니다. 보통, SPA는 이 URL로 브라우저를 리다이렉션합니다. 이 URL로 브라우저가 이동하면, OpenID Connect 서버의 End Session 엔드 포인트로 요청을 생성하여 세션을 종료하고, 클라이언트 측과 인증 쿠키를 제거합니다.
现在는 .NET 플랫폼이 기본적으로 제공하는 도구와 우리가 BFF 패턴을 구현할 수 있는 것을 보자. .NET 플랫폼은 Microsoft.AspNetCore.Authentication.OpenIdConnect
NuGet パッケージ을 제공しています. 이는 Microsoft이 지원하는 OpenID Connect 클라이언트의 준비되어 있는 实现이며, 인가 코드 플로우와 PKCE를 both 지원합니다. 이 패키지는 /signin-oidc относитель 경로의 엔드 포인트를 추가하여 필요한 로그인 엔드 포인트 기능을 이미 구현합니다 (위에서 3번째 포인트를 보세요). 따라서, 나머지 세 개의 엔드 포인트를 구현하면 되ます.
실제적인 통합 예제로, Abblix OIDC Server 라이브러리를 기반으로 구성된 시험 OpenID Connect 서버를 사용할 것입니다. 그러나, 아래에 언급된 모든 내용은 Facebook, Google, Apple, 그리고 OpenID Connect protocol specification을 따르는 的任何其他 publicly available server에도 적용되며, 이 문서를 쓰는 시점에서는 가장 일반적인 기술 스택입니다.
FE 側에서 SPA를 구현하기 위해 React library를 사용하고, BE 측에서 .NET WebAPI를 사용하ます. 이 문서를 쓰는 시점에서는 가장 일반적인 기술 스택입니다.
components와 그들之间的interaction의 전체 구조는 다음과 같습니다.
이 문서의 예제를 실행하기 위해서는 .NET SDK과 Node.js를 설치해야 합니다. 이 문서에서 사용되는 모든 예제는 .NET 8, Node.js 22, React 18로 개발되었으며, 쓰는 시점에서는 현재 정식 수정입니다.
React를 사용하여 Client SPA를 생성하고 .NET에서 Backend을 구현하는 것입니다.
빠른 클라이언트 应用程序 생성을 위해서는 이미 만들어진 emplate을 사용하는 것이 편리합니다. .NET 7 버전까지, SDK는 .NET WebAPI 응용 프로그램과 React SPA를 위한 내장 템플릿을 제공했습니다. Unfortunately, 이 템플릿은 .NET 8 버전에서 제거되었습니다. 따라서 Abblix 团队은 자신의 템플릿을 만들었습니다, 该 template는 .NET WebApi backend, React library를 기반으로 된 frontend SPA, 以及 TypeScript로 Vite를 사용하여 built with 되었습니다. 이 template는 Abblix.Templates
包의 일부로 공개적으로 사용 가능하며, 다음과 같은 명령어를 실행하여 설치할 수 있습니다.:
dotnet new install Abblix.Templates
现在, abblix-react
이라는 이름의 template를 사용할 수 있습니다. let’s use it to create a new application called BffSample
:
dotnet new abblix-react -n BffSample
This command creates an application consisting of a .NET WebApi backend and a React SPA client. The files related to the SPA are located in the BffSample\ClientApp
folder.
After creating the project, the system will prompt you to run a command to install the dependencies:
cmd /c "cd ClientApp && npm install"
This action is necessary to install all the required dependencies for the client part of the application. For a successful project launch, it is recommended to agree and execute this command by entering Y
(yes).
Let’s immediately change the port number on which the BffSample
application runs locally to 5003. This action is not mandatory, but it will simplify further configuration of the OpenID Connect server. To do this, open the BffSample\Properties\launchSettings.json
file, find the profile named https
and change the value of the applicationUrl
property to https://localhost:5003
.
다음으로, BffSample
폴더로 이동하여 다음 명령어를 실행하여 OpenID Connect 클라이언트를 구현하는 NuGet 패키지를 추가합니다:
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
애플리케이션에서 Cookies
와 OpenIdConnect
라는 두 개의 인증 스키마를 설정하고 애플리케이션 설정에서 해당 설정을 읽어옵니다. 이를 위해 BffSample\Program.cs
파일을 변경합니다:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// ******************* START *******************
var configuration = builder.Configuration;
builder.Services
.AddAuthorization()
.AddAuthentication(options => configuration.Bind("Authentication", options))
.AddCookie()
.AddOpenIdConnect(options => configuration.Bind("OpenIdConnect", options));
// ******************** END ********************
var app = builder.Build();
그리고 BffSample\appsettings.json
파일에 OpenID Connect 서버에 연결하는 데 필요한 설정을 추가합니다:
{
// ******************* START *******************
"Authentication": {
"DefaultScheme": "Cookies",
"DefaultChallengeScheme": "OpenIdConnect"
},
"OpenIdConnect": {
"SignInScheme": "Cookies",
"SignOutScheme": "Cookies",
"SaveTokens": true,
"Scope": ["openid", "profile", "email"],
"MapInboundClaims": false,
"ResponseType": "code",
"ResponseMode": "query",
"UsePkce": true,
"GetClaimsFromUserInfoEndpoint": true
},
// ******************** END ********************
"Logging": {
"LogLevel": {
"Default": "Information",
그리고 BffSample\appsettings.Development.json
파일에:
{
// ******************* START *******************
"OpenIdConnect": {
"Authority": "https://localhost:5001",
"ClientId": "bff_sample",
"ClientSecret": "secret"
},
// ******************** END ********************
"Logging": {
"LogLevel": {
"Default": "Information",
각 설정과 그 목적을 간략히 검토해 보겠습니다:
인증
부분:DefaultScheme
속성은 기본적으로Cookies
스키마를 사용하여 인증하며,DefaultChallengeScheme
는 기본 스키마로 인증할 수 없는 경우OpenIdConnect
스키마를 사용하여 인증을 실시합니다. 따라서, 응용 프로그램에 未知の 사용자일 때 OpenID Connect 서버를 인증 호출할 것입니다. 그 이후, 인증된 사용자는 인증 쿠키를 받고, それ以降의 서버 호출은 이 쿠키를 사용하여 인증하며 OpenID Connect 서버와 통신하지 않습니다.OpenIdConnect
섹션:SignInScheme
및SignOutScheme
속성은Cookies
스케me를 사용하여 로그인 후 사용자 정보를 저장할 것인지 지정합니다.Authority
속성은 OpenID Connect 서버의 기본 URL을 포함합니다.ClientId
및ClientSecret
는 OpenID Connect 서버에 등록된 클라이언트 应用程序의 識別자와 시크릿 키입니다.SaveTokens
는 OpenID Connect 서버에서 인증 결과로 받은 토큰을 저장할 것인지 지정합니다.Scope
는BffClient
应用程序이 접근하고자 하는 스코프 목록을 포함합니다. 이 경우, 표준 스코프openid
(사용자 식별자),profile
(사용자 프로필), 및email
(이메일)를 요청합니다.MapInboundClaims
는 OpenID Connect 서버로부터 도착하는 CLAIMS를 응용 정보에 사용하기 위한 변환을 담당합니다.false
의 값은 CLAIMS가 OpenID Connect 서버에서 받은 형태로 인증된 사용자의 SESSION에 저장되는 것을 의미합니다.ResponseType
의code
값은 클라이언트가 인가 코드 流程를 사용할 것을 의미합니다.ResponseMode
는 인가 코드를 쿼리 문자열로 전달하는 것을 지정합니다. 이는 인가 코드 流程의 기본 방법입니다.UsePkce
속성은 인증 과정에서 Authorization Code의 인截을 防止하기 위해 PKCE를 사용할 것인지 지정합니다.GetClaimsFromUserInfoEndpoint
속성은 사용자 정보 엔드 포인트로부터 사용자 프로필 데이터를 얻을 것인지 지정합니다.
我们的 응용 프로그램은 인증 없이 사용자와 interaction을 하는 것을 가정하지 않기 때문에, React SPA가 인증 성공 후에만 로드되는지 보장할 것이다. 当然, SPA를 외부 소스로, 例如 Static Web Host로 로드하는 것이나, Content Delivery Network (CDN) 서버나 로컬 開発 서버로 npm start
명령어를 이용하여 실행되는 경우(例如, 我们的 예시를 debug 모드로 실행하는 때)에는 SPA 로드 전에 인증 상태를 확인할 수 없다. 그러나, 我們自己的 .NET backend이 SPA 로드를 담당하는 경우는 가능하다.
이러한 경우, BffSample\Program.cs
파일에 인증과 권한 middleware를 추가해야 한다:
app.UseRouting();
// ******************* START *******************
app.UseAuthentication();
app.UseAuthorization();
// ******************** END ********************
在 BffSample\Program.cs
파일의 末尾, SPA 로드를 직접 行う 곳에, 인증 상태가 필요하다는 사항을 추가하여, .RequireAuthorization()
를 씀니다:
app.MapFallbackToFile("index.html").RequireAuthorization();
OpenID Connect 서버 세팅하기
이전에 언급한 것처럼, 실제적인 통합 예시를 위해, Abblix OIDC Server 라이브러리를 기반으로 구성되어 있는 테스트 OpenID Connect 서버를 사용할 것입니다. ASP.NET Core MVC에 기반하는 应用程序의 기본 템플릿, Abblix OIDC Server
라이브러리를 사용하여, 이전에 설치한 Abblix.Templates
패키지에 따라 사용할 수 있습니다. 이 템플릿을 사용하여 OpenIDProviderApp
라고 이름付けられた 새 应用程序을 생성하겠습니다.
dotnet new abblix-oidc-server -n OpenIDProviderApp
서버를 구성하기 위해서는, BffClient
应用程序을 OpenID Connect 서버에 클라이언트로 등록하고 테스트 사용자를 추가해야 합니다. 이를 하기 위해서는, OpenIDProviderApp\Program.cs
파일에 다음과 같은 块들을 추가하여야 합니다.
var userInfoStorage = new TestUserStorage(
// ******************* START *******************
new UserInfo(
Subject: "1234567890",
Name: "John Doe",
Email: "[email protected]",
Password: "Jd!2024$3cur3")
// ******************** END ********************
);
builder.Services.AddSingleton(userInfoStorage);
// ...
// Abblix OIDC Server를 등록하고 구성하기
builder.Services.AddOidcServices(options =>
{
// 다음과 같은 OIDC 서버 옵션을 구성하십시오:
// ******************* START *******************
options.Clients = new[] {
new ClientInfo("bff_sample") {
ClientSecrets = new[] {
new ClientSecret {
Sha512Hash = SHA512.HashData(Encoding.ASCII.GetBytes("secret")),
}
},
TokenEndpointAuthMethod = ClientAuthenticationMethods.ClientSecretPost,
AllowedGrantTypes = new[] { GrantTypes.AuthorizationCode },
ClientType = ClientType.Confidential,
OfflineAccessAllowed = true,
PkceRequired = true,
RedirectUris = new[] { new Uri("https://localhost:5003/signin-oidc", UriKind.Absolute) },
PostLogoutRedirectUris = new[] { new Uri("https://localhost:5003/signout-callback-oidc", UriKind.Absolute) },
}
};
// ******************** END ********************
// 다음 URL은 AuthController의 Login 행동으로 이동합니다.
options.LoginUri = new Uri($"/Auth/Login", UriKind.Relative);
// 다음 行은 토큰 서명에 사용되는 새 キー를 생성합니다. 자신의 키를 사용하고자 하시면 替わる必要が 있습니다.
options.SigningKeys = new[] { JsonWebKeyFactory.CreateRsa(JsonWebKeyUseNames.Sig) };
});
코드를 자세히 검토해 보자. bff_sample
이entifier과 secret
이 secret key를 SHA512 해시로 저장하는 것을 의미하는 클라이언트를 등록하며, 토큰 acquisition시 client authentication을 사용하며 secret key가 POST 메시지로 전송되며 (ClientAuthenticationMethods.ClientSecretPost
) 를 의미한다. AllowedGrantTypes
는 클라이언트가 단지 인가 코드 플로우만 사용할 수 있음을 지정하고, ClientType
는 클라이언트를 confidentially define하는 것이며, 이를 의미하는 것은 그 비밀 钥를 securely store할 수 있다는 의미이다. OfflineAccessAllowed
는 클라이언트가 refresh token을 사용할 수 있게 해준다. PkceRequired
는 인증 과정에서 PKCE를 사용하라고 명령하며, RedirectUris
와 PostLogoutRedirectUris
는 인증 후 또는 세션 종료 후 리다이렉션이 허용되는 URL 목록을 포함한다.
다른 OpenID Connect 서버를 사용할 때, configurations에 의해 다른 것 뿐이다.
기본 BFF API를 구현하는 것
예전에 언급했듯이, Microsoft.AspNetCore.Authentication.OpenIdConnect
パッケ지를 사용하면 자동으로 이 サンプル 응용 프로그램에 Sign In 端点の実装을 추가해준다. 이제 BFF API의 나머지 部分을 구현하는 시간이 到了. 추가적인 端点을 사용하기 위해 ASP.NET MVC コント롤러를 사용하자. Controllers
폴더와 BffController.cs
파일을 BffSample
프로젝트에 추가하고 다음과 같은 코드를 넣는다.
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace BffSample.Controllers;
[ApiController]
[Route("[controller]")]
public class BffController : Controller
{
public const string CorsPolicyName = "Bff";
[HttpGet("check_session")]
[EnableCors(CorsPolicyName)]
public ActionResult> CheckSession()
{
// SPA가 인가 端点로 리다이렉션하도록 强制하기 위해 401 Unauthorized를 리턴합니다.
if (User.Identity?.IsAuthenticated != true)
return Unauthorized();
return User.Claims.ToDictionary(claim => claim.Type, claim => claim.Value);
}
[HttpGet("login")]
public ActionResult> Login()
{
// 인증 코드 流转送 로직
return Challenge(new AuthenticationProperties { RedirectUri = Url.Content("~/") });
}
[HttpPost("logout")]
public IActionResult Logout()
{
// 사용자 로그아웃 처리 로직
return SignOut();
}
}
이 클래스 코드를 상세히 분석하겠습니다:
- `[Route(“[controller]”)]` 어트리BUTE는 컨트롤러内에 들어가는 모든 동작의 기본 라우트를 설정합니다. 이 경우, 라우트는 컨트롤러의 이름과 일치하게 동작하며, 우리의 API 方法的 모든 경로가 `/bff/`로 시작합니다.
- 상수 `CorsPolicyName = “Bff”`는 later에 参照할 것이기 때문에 METHOD 어트리BUTE에서 사용할 CORS(Cross-Origin Resource Sharing) 정책의 이름을 정의합니다.
- `CheckSession`, `Login`, `Logout` 이라는 세 가지 方法的 구현은 이전에 described한 BFF 기능을 처리합니다. GET 요청을 `/bff/check_session`에서, `/bff/login`에서 하는 POST 요청을 `/bff/logout`에서 처리합니다.
- `CheckSession` 方法은 사용자의 인증 상태를 검사합니다. 사용자가 인증되지 않은 경우 `401 Unauthorized` 코드를 리턴하며, 이를 SPA에게 인가 端点로 리다이렉션하게 하는 것입니다. 인증이 성공적이면 이 method는 CLAIM과 그들의 값을 리턴합니다. 이 method는 사용자 인증에 사용되는 COOKIE를 包含하는 다음 도메인에 대한 cross-domain 이고, 이 method의 호출을 위한 CORS 정책 binding을 `CorsPolicyName` 이름으로 포함합니다.
Login
메서드는 이전CheckSession
호출이401 Unauthorized
를 반환한 경우 SPA에 의해 호출됩니다. 이는 사용자가 여전히 인증되지 않았음을 확인하고 구성된Challenge
프로세스를 시작하여 OpenID Connect 서버로 리디렉션, Authorization Code Flow 및 PKCE를 사용한 사용자 인증, 인증 쿠키 발급을 수행합니다. 이후, 제어는 우리의 애플리케이션 루트"~/"
로 돌아가며, 이는 SPA가 리로드되고 인증된 사용자로 시작되도록 트리거합니다.Logout
메서드도 SPA에 의해 호출되지만 현재 인증 세션을 종료합니다. 이는 서버 부분의BffSample
에서 발급한 인증 쿠키를 제거하고 OpenID Connect 서버 쪽의 End Session 엔드포인트를 호출합니다.
BFF를 위한 CORS 구성
위에서 언급했듯이, CheckSession
메서드는 SPA에서 비동기 호출(일반적으로 Fetch API를 사용)을 위해 설계되었습니다. 이 메서드의 적절한 작동은 브라우저에서 인증 쿠키를 보낼 수 있는 능력에 달려 있습니다. SPA가 CDN 또는 별도의 포트에서 실행되는 개발 서버와 같은 독립된 스태틱 웹 호스트에서 로드되는 경우, 이 호출은 교차 도메인이 됩니다. 따라서 CORS 정책을 구성하는 것이 필수적이며, 그렇지 않으면 SPA는 이 메서드를 호출할 수 없습니다.
우리는 이미 Controllers\BffController.cs
파일의 컨트롤러 코드에서 CorsPolicyName = "Bff"
라는 CORS 정책이 사용된다고 명시했습니다. 이제 우리의 과제를 해결하기 위해 이 정책의 매개변수를 구성할 때입니다. BffSample/Program.cs
파일로 돌아가 다음 코드 블록을 추가해 보겠습니다.
// ******************* START *******************
using BffSample.Controllers;
// ******************** END ********************
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// ...
builder.Services
.AddAuthorization()
.AddAuthentication(options => configuration.Bind("Authentication", options))
.AddCookie()
.AddOpenIdConnect(options => configuration.Bind("OpenIdConnect", options));
// ******************* START *******************
builder.Services.AddCors(
options => options.AddPolicy(
BffController.CorsPolicyName,
policyBuilder =>
{
var allowedOrigins = configuration.GetSection("CorsSettings:AllowedOrigins").Get();
if (allowedOrigins is { Length: > 0 })
policyBuilder.WithOrigins(allowedOrigins);
policyBuilder
.WithMethods(HttpMethods.Get)
.AllowCredentials();
}));
// ******************** END ********************
var app = builder.Build();
이 코드는 CORS 정책 方法的을 configuration에 지정한 strings 수组的 CorsSettings:AllowedOrigins
로부터 지정된 源于에 SPAs가 로드되는 경우 GET
方法을 사용하여 호출하는 것을 허용하며, 이 호출에서 クッキー를 보내도록 허용합니다. 또한 app.UseCors(...)
方法的 호출이 app.UseAuthentication()
方法の 직전에 置かれる 것을 보장합니다.:
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// ******************* START *******************
app.UseCors(BffController.CorsPolicyName);
// ******************** END ********************
app.UseAuthentication();
app.UseAuthorization();
CORS 정책이 제대로 동작하도록 하려면 BffSample\appsettings.Development.json
構成 ファイルに 해당하는 설정을 추가해야 합니다.:
{
// ******************* START *******************
"CorsSettings": {
"AllowedOrigins": [ "https://localhost:3000" ]
},
// ******************** END ********************
"OpenIdConnect": {
"Authority": "https://localhost:5001",
"ClientId": "bff_sample",
우리의 예에서는 https://localhost:3000
이 있는 곳에 React SPA를 npm run dev
명령어를 사용하여 開発 서버가 시작되는 곳입니다. 이 주소를 贵方의 경우에 대해 开展하기 위해 BffSample.csproj
파일을 개봉하고 SpaProxyServerUrl
매개 변수의 값을 찾으십시오. 실제 응용 프로그램에서는 CORS 정책이 CDN (Content Delivery Network) 또는 유사한 서비스의 주소를 포함할 수 있습니다. SPA가 BFF API를 제공하는 주소와 다르게 로드되는 경우 CORS 정책 구성에 이 주소를 추가해야 합니다.
리액트 애플리케이션에서 BFF를 통한 인증 구현
서버 측에 BFF API를 구현했습니다. 이제 React SPA에 집중하고 이 API를 호출하는 기능을 추가할 시간입니다. 먼저 BffSample\ClientApp\src\
폴더로 이동하여 components
폴더를 만들고 아래 내용을 포함하는 Bff.tsx
파일을 추가해 봅시다:
import React, { createContext, useContext, useEffect, useState, ReactNode, FC } from 'react';
// BFF 컨텍스트의 형상을 정의하는 것
interface BffContextProps {
user: any;
fetchBff: (endpoint: string, options?: RequestInit) => Promise;
checkSession: () => Promise;
login: () => void;
logout: () => Promise;
}
// BFF로 共有할 수 있는 상태와 함수를 applicaton 내에서 생성하기 위한 컨텍스트 만들기
const BffContext = createContext({
user: null,
fetchBff: async () => new Response(),
checkSession: async () => {},
login: () => {},
logout: async () => {}
});
interface BffProviderProps {
baseUrl: string;
children: ReactNode;
}
export const BffProvider: FC = ({ baseUrl, children }) => {
const [user, setUser] = useState(null);
// 기본 URL을 마지막 슬래시 제거하여 일관성있는 URL을 만들기 위해 정규화하는 것
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
const fetchBff = async (endpoint: string, options: RequestInit = {}): Promise => {
try {
// fetch 함수는 인증을 위해 필요한 쿠키를 처리하기 위해 인증 정보를 포함하는 것
return await fetch(`${baseUrl}/${endpoint}`, {
credentials: 'include',
...options
});
} catch (error) {
console.error(`Error during ${endpoint} call:`, error);
throw error;
}
};
// 로그인 함수는 사용자가 인증 필요할 때 로그인 페이지로 리다이렉션하는 것
const login = (): void => {
window.location.replace(`${baseUrl}/login`);
};
// checkSession 함수는 초기 render 시 user session을 인증하는 것
const checkSession = async (): Promise => {
const response = await fetchBff('check_session');
if (response.ok) {
// 세션이 유효하면, 받은 声明 데이터로 user state를 갱신하는 것
setUser(await response.json());
} else if (response.status === 401) {
// 사용자가 인증되지 않은 경우, 그를 로그인 페이지로 리다이렉션하는 것
login();
} else {
console.error('Unexpected response from checking session:', response);
}
};
// 사용자를 로그아웃 하는 함수
const logout = async (): Promise => {
const response = await fetchBff('logout', { method: 'POST' });
if (response.ok) {
// 로그아웃이 성공적으로 되면 主页으로 리다이렉션하는 것
window.location.replace('/');
} else {
console.error('Logout failed:', response);
}
};
// useEffect는 컴포넌트가 마운트되면 checkSession 함수를 실행하는 것
// 이렇게 어oplication이 로드되면 즉시 세션을 확인하는 것
useEffect(() => { checkSession(); }, []);
return (
// Applicaton 내에서 인증과 세션 관리에 관련된 함수와 상태를 사용할 수 있게 BFF 컨텍스트를 제공하는 것
{children}
);
};
// 다른 컴포넌트에서 BFF 컨텍스트를 쉽게 사용하기 위한 사용자 정의 훅
export const useBff = (): BffContextProps => useContext(BffContext);
// BFF 컨텍스트에 대한 액세스를 제공하기 위해 HOC를 экспорт하는 것
export const withBff = (Component: React.ComponentType) => (props: any) =>
{context => }
;
이 파일은 다음을 экспорт합니다:
- BFF Provider 컴포넌트, 이는 BFF의 컨텍스트를 생성하고 인증과 세션 관리에 관한 함수와 상태를 전체 Applicaton에서 사용할 수 있도록 제공합니다.
- 현재 사용자 상태와 BFF를 작업하기 위한 기능을 가진 오브젝트를 반환하는 사용자 정의 훅
useBff()
이며, 기능적인 React 컴포넌트에서 사용하기 위해 만들어졌습니다.checkSession
,login
,logout
과 같은 함수를 포함하고 있습니다. - 클래스 기반 React 컴포넌트에서 사용할 하이어rd 오브젝트 컴포넌트
withBff
次に, 인증 성공 후 현재 사용자의 声明을 표시하는 UserClaims
컴포넌트를 만듭니다. BffSample\ClientApp\src\components
폴더에 UserClaims.tsx
파일을 다음 내용으로 생성합니다.
import React from 'react';
import { useBff } from './Bff';
export const UserClaims: React.FC = () => {
const { user } = useBff();
if (!user)
return <div>Checking user session...</div>;
return (
<>
<h2>User Claims</h2>
{Object.entries(user).map(([claim, value]) => (
<div key={claim}>
<strong>{claim}</strong>: {String(value)}
</div>
))}
</>
);
};
이 코드는 useBff()
훅을 사용하여 인증된 사용자를 확인하고, 사용자가 인증되면 사용자의 声明을 목록形式으로 표시합니다. 사용자 데이터가 아직 사용 가능하지 않으면, Checking user session...
텍스트를 보여줍니다.
지금 BffSample\ClientApp\src\App.tsx
파일에 가서 필요한 코드로 대체하십시오. components/Bff.tsx
から BffProvider
import하고 components/UserClaims.tsx
から UserClaims
import하고 주요 컴포넌트 코드를 삽입하십시오.
import { BffProvider, useBff } from './components/Bff';
import { UserClaims } from './components/UserClaims';
const LogoutButton: React.FC = () => {
const { logout } = useBff();
return (
<button className="logout-button" onClick={logout}>
Logout
</button>
);
};
const App: React.FC = () => (
<BffProvider baseUrl="https://localhost:5003/bff">
<div className="card">
<UserClaims/>
</div>
<div className="card">
<LogoutButton />
</div>
</BffProvider>
);
export default App;
ここで, baseUrl
매개 변수는 우리의 BFF API의 기본 URL https://localhost:5003/bff
를 지정합니다. 이 간단ification은 簡単성 때문에 의도적으로 하였고, 실제 응용 프로그램에서는 이를 동적으로 제공하는 것이 좋습니다. 이러한 방법은 다양하지만, 이 記事의 cope를 벗어나는 것이기 때문에 자세히 discuss하지 않습니다.
로그아웃
버튼은 사용자가 로그아웃할 수 있도록 합니다. 이 버튼은 useBff
훅을 통해 사용할 수 있는 logout
함수를 호출하고 사용자의 브라우저를 /bff/logout
엔드포인트로 리다이렉트하여 서버 측에서 사용자의 세션을 종료합니다.
이 단계에서 BffSample
애플리케이션과 OpenIDProviderApp
을 함께 실행하여 기능을 테스트할 수 있습니다. 각 프로젝트에서 dotnet run -lp https
명령어나 선호하는 IDE를 사용하여 애플리케이션을 시작할 수 있습니다. 두 애플리케이션은 동시에 실행되어야 합니다.
이후, 브라우저를 열고 https://localhost:5003
으로 이동합니다. 모든 것이 올바르게 설정되었다면, SPA가 로드되고 /bff/check_session
을 호출합니다. /check_session
엔드포인트는 401 응답을 반환하며, SPA는 브라우저를 /bff/login
으로 리다이렉트하여 OpenID Connect Authorization Code Flow를 사용한 서버 인증을 시작합니다. 이 요청 시퀀스를 확인하려면 브라우저에서 개발자 콘솔을 열고 네트워크 탭으로 이동하십시오. 사용자 자격 증명([email protected]
, Jd!2024$3cur3
)을 성공적으로 입력하면 SPA로 제어가 돌아오며 브라우저에서 인증된 사용자 클레임을 확인할 수 있습니다.
sub: 1234567890
sid: V14fb1VQbAFG6JXTYQp3D3Vpa8klMLcK34RpfOvRyxQ
auth_time: 1717852776
name: John Doe
email: [email protected]
추가로, 로그아웃
버튼을 클릭하면 브라우저가 /bff/logout
으로 리다이렉트되어 사용자가 로그아웃되며 로그인 페이지가 다시 표시되고 사용자 이름과 비밀번호를 입력하라는 메시지가 나타납니다.
만약 오류가 발생하면, 您的 코드와 GitHub 저장库 Abblix/Oidc.Server.GettingStarted를 비교할 수 있으며, 이 예시와 기타 실행 ready-to-run 예시를 포함합니다.
HTTPS cerificate Trust Issues 해결
HTTPS를 사용하는 웹 응용 프로그램을 로컬에서 테스트 할 때, 브라우저로 SSL cerificate가 신뢰되지 않는 경고를 받을 수 있습니다. 이러한 이슈는 ASP.NET Core가 사용하는 開発 cerificate가 인지 받은 인증 기관 (CA)에 의해 발급되지 않고, 자신으로 서명되거나 시스템에서 전혀 존재하지 않기 때문입니다. 이러한 경고를 없애는 것은 다음과 같은 명령을 한 번 실행하면 가능합니다.
dotnet dev-certs https --trust
이 명령은 localhost
을 위한 자신으로 서명tesertificate를 생성하고 시스템에 설치하여 이 cerificate를 신뢰하도록 합니다. ASP.NET Core가 로컬에서 웹 응용 프로그램을 실행하기 위해 이 cerificate를 사용합니다.이 명령을 실행한 다음, 브라우저를 다시 시작하여 변경을 적용시키십시오.
Chrome 사용자 전용 주의 사항: 開発 cerificate를 신뢰로 설정한 다음에도, Chrome의 某些 version는 安全管理적이라고 생각하여 localhost
사이트에 대한 assess를 제한할 수 있습니다. Chrome에서 오류가 나며 localhost
사이트에 대한 接入을 Chrome로 차단되는 것을 경고하면, 이러한 제한을 规避하는 것이 가능합니다.
- 에러 страницы의 어느 곳에 마우스 표시를 Place하고
thisisunsafe
orbadidea
를 입력하십시오. 이러한 tast sequence는 Chrome의 规避 명령으로 사용되며, 이를 통해localhost
사이트에 들어가는 것을 허용합니다.
이러한 bypass 方法은 開発 시나리오에서만 사용하는 것이 중요하며, 실제 보안 риск를 인지하십시오.
BFF를 통한 第三方 API 호출
우리는 BffSample
应用程序에 인증을 성공적으로 구현했습니다. 이제 第三次方的 API를 호출하는 것을 배워봅시다.
이를 상상하자 마이크로 서비스가 필요한 데이터, 예를 들어 날씨 예보를 제공하며, 이를 이용하는 것은 アクセス トークン only로 허용됩니다. BffSample
应用程序의 서버 부분은 반대 proxy로 동작하는 역할을 합니다. つまり SPA로부터 데이터 요청을 수신하고 인증하여, 접근 토큰을 추가하고, 이 요청을 날씨 서비스로 转发하고, 이 서비스의 응답을 SPA로 돌려줍니다.
ApiSample 서비스 생성
BFF를 통해 remote API 호출을 示唆하기 전에, 이 예에서 API로 동작할 응용 프로그램을 생성해야 합니다.
응용 프로그램을 생성하기 위해서는 .NET이 제공하는 템플릿을 사용하我们将 OpenIDProviderApp
과 BffSample
프로젝트를 포함하는 폴더로 이동하여 ApiSample
응용 프로그램을 생성하기 위한 다음 명령을 실행합니다.
dotnet new webapi -n ApiSample
이 ASP.NET Core Minimal API 응용 프로그램은 單一的 端點 /weatherforecast
로 날씨 정보를 JSON 형식으로 제공하는 것을 서비스합니다.
처음으로, `ApiSample` 应用程序에 사용되는 로컬에서 arbitrarily assigned port number를 고정 포트 5004로 변경합니다. 이전에 말씀하셨듯이, 이 단계는 필수가 아니지만, 我们的设置을 간단하게 만들어줍니다. 이를 하기 위해, `ApiSample\Properties\launchSettings.json` 파일을 오픈하여, `https`로 named profile를 찾고, `applicationUrl` property의 value를 `https://localhost:5004`로 변경합니다.
이제, 날씨 API가 access token only로 사용할 수 있게 하겠습니다. `ApiSample` project folder로 이동하여, JWT Bearer token authentication의 NuGet package을 추가합니다.:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
`ApiSample\Program.cs` 파일에서, `WeatherApi`로 named authentication scheme와 authorization policy를 구성합니다.:
// ******************* START *******************
using System.Security.Claims;
// ******************** END ********************
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// ******************* START *******************
var configuration = builder.Configuration;
builder.Services
.AddAuthentication()
.AddJwtBearer(options => configuration.Bind("JwtBearerAuthentication", options));
const string policyName = "WeatherApi";
builder.Services.AddAuthorization(
options => options.AddPolicy(policyName, policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireAssertion(context =>
{
var scopeValue = context.User.FindFirstValue("scope");
if (string.IsNullOrEmpty(scopeValue))
return false;
var scope = scopeValue.Split(' ', StringSplitOptions.RemoveEmptyEntries);
return scope.Contains("weather", StringComparer.Ordinal);
});
}));
// ******************** END ********************
var app = builder.Build();
이 代码块은 应用程序 settings에서 구성 정보를 읽어서 인증을 셋업하고, JWT (JSON Web Tokens)를 사용하여 인가를 포함하고, `WeatherApi`로 named 인증 정책을 구성합니다. `WeatherApi` 인증 정책은 다음과 같은 요구 사항을 세팅합니다.:
policy.RequireAuthenticatedUser()
: 인증된 사용자만 보호된 리소스에 アクセス할 수 있게 합니다.policy.RequireAssertion(context => ...)
: 사용자는weather
값을 포함하는scope
CLAIM을 가져야 합니다.scope
CLAIM은 RFC 8693에 따라 공백을 사용하여 여러 값을 나누어 있을 수 있기 때문에, 실제scope
값은 個々의 部分로 분리되고, 이를 통해 필요한weather
값을 포함하는지 여부를 확인합니다.
이러한 조건은 인증된 사용자이며, weather
스코pe에 대한 인가 토큰을 가진 것을 보장합니다.
이 정책을 /weatherforecast
端点에 적용해야 합니다. 아래와 같이 RequireAuthorization()
호출을 추가합니다.
app.MapGet("/weatherforecast", () =>
{
// ...
})
.WithName("GetWeatherForecast")
// ******************* START *******************
.WithOpenApi()
.RequireAuthorization(policyName);
// ******************** END ********************
authentication 스키마에 대한 필요한 구성 옵션을 ApiSample
应用程序의 appsettings.Development.json
파일에 추가합니다.
{
// ******************* START *******************
"JwtBearerAuthentication": {
"Authority": "https://localhost:5001",
"MapInboundClaims": false,
"TokenValidationParameters": {
"ValidTypes": [ "at+jwt" ],
"ValidAudience": "https://localhost:5004",
"ValidIssuer": "https://localhost:5001"
}
},
// ******************** END ********************
"Logging": {
"LogLevel": {
"Default": "Information",
각 옵션을 상세히 다룰 것입니다.
Authority
: 이 URL은 JWT 토큰을 발행하는 OpenID Connect 인증 서버의 주소를 나타냅니다.ApiSample
应用程序에 구성한 인증 제공자는 이 URL을 사용하여 토큰 검증을 위해 必要한 정보, 예를 들어 서명 キー를 얻는 것을 도와줍니다.MapInboundClaims
: 이 옵션은 ASP.NET Core 내에서 JWT 토큰から来る 이웃 CLAIM을 내부 CLAIM로 매핑하는 방법을 제어한다. 이 옵션이false
로 설정되어 있으므로, 이웃 CLAIM은 JWTからの 原标题을 사용한다는 의미이다.TokenValidationParameters
:ValidTypes
:at+jwt
로 설정되어 있으며, RFC 9068 2.1에 따라 Access Token이 JWT 형식으로 나타나는 것을 나타낸다.ValidAudience
: 어플리케이션이 클라이언트https://localhost:5004
에게 발급された 토큰을 인정하게 되는 것을 지정한다.ValidIssuer
: 어플리케이션이 서버https://localhost:5001
에서 발급한 토큰을 인정하게 되는 것을 지정한다.
OpenIDProviderApp의 extra 설정
인증 서비스 OpenIDProviderApp
과 클라이언트 应用程序 BffSample
의 조합은 사용자 인증을 제공하는 데 좋습니다. 그러나 remote API로의 호출을 허용하기 위해서는 OpenIDProviderApp
에서 리소스로 ApiSample
应用程序을 등록해야 합니다. 我们的 예에서는 Abblix OIDC Server
를 사용하고 있으며, RFC 8707: OAuth 2.0의 리소스 인디케이터를 지원합니다. 따라서 ApiSample
应用程序을 weather 범위로 리소스로 등록할 것입니다. 다른 OpenID Connect 서버를 사용하고 있다면 Resource Indicators를 지원하지 않는 경우도 있으며 이를 위해 이 remote API에 대한 unique scope(예: 我们的 예에서 weather)를 등록하는 것이 좋습니다.
다음 코드를 OpenIDProviderApp\Program.cs
파일에 추가하십시오.
// Abblix OIDC Server 등록 및 구성
builder.Services.AddOidcServices(options => {
// ******************* START *******************
options.Resources =
[
new(new Uri("https://localhost:5004", UriKind.Absolute), new ScopeDefinition("weather")),
];
// ******************** END ********************
options.Clients = new[] {
new ClientInfo("bff_sample") {
이 예에서는 ApiSample
应用程序을 등록하고, 그 기본 주소 https://localhost:5004
를 리소스로 하여 이름이 weather
인 specific scope를 정의합니다. 실제 applicaton, 특히 많은 엔드 포인트로 구성되어 있는 複雑한 API를 사용하는 경우, 각 개별 엔드 포인트 또는 그룹에 대한 独自 scope를 정의하는 것이 좋습니다. 이러한 접근은 더 精巧한 접근 제어를 가능하며, 인가 권한 관리에 유연性을 제공합니다. 예를 들어, 다른 operatoin, 응용 모듈, 또는 사용자 인가 수준에 대한 독특한 scope를 생성할 수 있으며, API의 특정 부분에 대한 더 세부한 제어를 가능하게 합니다.
BffSample 에서 리모ote API로 요청 proxying하는 방법
BffSample
클라이언트 应用程序은 이제 ApiSample
에 대한 액세스 토큰을 얻는 것だけ이 아니라 SPA로부터 리mote API로의 요청을 처리해야 합니다. 이를 위해 OpenIDProviderApp
서비스에서 얻은 액세스 토큰을 이러한 요청에 추가하고, 이를 리mote 서버로 转发하고, 그 서버의 응답을 SPA로 돌려 줄 필요가 있습니다. 그러다보니 BffSample
는 reverese proxy server로 동작해야 합니다.
client application에서 수동으로 request proxying을 구현하는 것 대신, 마이크로soft에 의해 개발되어 있는 现成的 제조 제품 YARP(Yet Another Reverse Proxy)를 사용하겠습니다. YARP는 .NET에서 쓰여진 reverse proxy server로 NuGet package로 제공됩니다.
BffSample
应用程序에 YARP을 사용하려면 우선 NuGet package를 추가합니다 :
dotnet add package Yarp.ReverseProxy
BffSample\Program.cs
파일의 시작에 다음 namespaces를 추가하십시오 :
using Microsoft.AspNetCore.Authentication;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using System.Net.Http.Headers;
using Yarp.ReverseProxy.Transforms;
전화 var app = builder.Build();
직전에 코드를 추가하세요:
builder.Services.AddHttpForwarder();
그리고 app.MapControllerRoute()
과 app.MapFallbackToFile()
之间的 것에:
app.MapForwarder(
"/bff/{**catch-all}",
configuration.GetValue("OpenIdConnect:Resource") ?? throw new InvalidOperationException("Unable to get OpenIdConnect:Resource from current configuration"),
builderContext =>
{
// 요청 경로에서 "/bff" 접두사를 제거합니다.
builderContext.AddPathRemovePrefix("/bff");
builderContext.AddRequestTransform(async transformContext =>
{
// 인증 과정 동안 받은 액세스 토큰을 얻습니다.
var accessToken = await transformContext.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
// 프록시 요청에 액세스 토큰을 헤더로 추가합니다.
transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
});
}).RequireAuthorization();
이 코드가 무엇을 하는지 분석해 봅시다:
builder.Services.AddHttpForwarder()
는 YARP가 필요한 서비스를 DI 컨테이너에 등록합니다.app.MapForwarder
는 다른 서버나 엔드포인트로 요청을 转发하기 위한 설정을 합니다."/bff/{**catch-all}"
는 리버스 프록시가 响应用户的路徑 패턴입니다./bff/
로 시작하는 모든 요청은 YARP를 이용하여 처리됩니다.{**catch-all}
는/bff/
다음에 나오는 URL의 나머지 부분을 捕获합니다.configuration.GetValue<string>("OpenIdConnect:Resource")
는 응용 정보에서OpenIdConnect:Resource
节에 있는 값을 얻습니다. 이 값은 요청이 转发되는 리소스 주소를 지정합니다. 我们的 예제에서, 이 값은https://localhost:5004
가 될 것입니다. –ApiSample
응용을 실행시키는 기본 URL입니다.-
- YARP가 SPA로부터 들어오는 모든 요청에 실행할 必要的 변환을
builderContext => ...
에 추가합니다. 我们的 경우, 두 가지 변환이 있을 것입니다: builderContext.AddPathRemovePrefix("/bff")
는/bff
접두사를 原始的 요청 경로에서 제거합니다.
builderContext.AddRequestTransform(async transformContext => ...)
는 이전에 인증 과정에서 얻은 액세스 토큰을 포함하는Authorization
HTTP 헤더를 요청에 추가합니다. 따라서, SPA가 远程 API로 부터 들어오는 요청은 액세스 토큰을 사용하여 인증되며, SPA가 이 토큰에 직접 アクセス할 수 있지 않아도 됩니다. - YARP가 SPA로부터 들어오는 모든 요청에 실행할 必要的 변환을
.RequireAuthorization()
는 전달 요청에 인가가 필요하다고 지정합니다./bff/{**catch-all}
경로에 대한 的唯一 하위 유저가 인가 받은 것만큼 수락되는 것입니다.
https://localhost:5004
리소스의 인증 과정에서 액세스 토큰 요청하기 위해 BffSample/appsettings.Development.json
파일의 OpenIdConnect
구성에 Resource
매개변수를 https://localhost:5004
값으로 추가하십시오.
"OpenIdConnect": {
// ******************* START *******************
"Resource": "https://localhost:5004",
// ******************** END ********************
"Authority": "https://localhost:5001",
"ClientId": "bff_sample",
또한, BffSample/appsettings.json
파일에 scope
数组에 weather
값을 추가하십시오.
{
"OpenIdConnect": {
// ...
// ******************* START *******************
"Scope": ["openid", "profile", "email", "weather"],
// ******************** END ********************
// ...
}
}
Notes: 실제 프로젝트에서는 액세스 토큰의 만료를 감시하는 것이 필요합니다. 토큰이 만료되기 전에, 인증 서비스에서 새로운 토큰을 사용하여 advanced 하게 요청하거나, 원래 요청을 재시도하기 위해 새로운 토큰을 얻는 것과 원격 API에서 鉴权错误을 처리하는 것이 좋습니다. 이篇文章의 간략화를 위해 이러한 측면을 deliberatel y 省略했습니다.
React SPA 应用程序中通过 BFF 请求天气 API
後端이 준비되었습니다. ApiSample
应用程序, 토큰 기반 인가를 実装한 API를 구현하고 있으며, BffSample
应用程序, 안전한 인가를 제공하기 위한 내장된 반대 프록시 서버를 포함하고 있습니다. 마지막 단계는 이 API를 요청하고 리ACT SPA 내에서 얻은 데이터를 표시하는 기능을 추가하는 것입니다.
WeatherForecast.tsx
파일을 BffSample\ClientApp\src\components
디렉터리에 다음 내용으로 추가하세요.:
import React, { useEffect, useState } from 'react';
import { useBff } from './Bff';
interface Forecast {
date: string;
temperatureC: number;
temperatureF: number;
summary: string;
}
interface State {
forecasts: Forecast[];
loading: boolean;
}
export const WeatherForecast: React.FC = () => {
const { fetchBff } = useBff();
const [state, setState] = useState<State>({ forecasts: [], loading: true });
const { forecasts, loading } = state;
useEffect(() => {
fetchBff('weatherforecast')
.then(response => response.json())
.then(data => setState({ forecasts: data, loading: false }));
}, [fetchBff]);
const contents = loading
? <p><em>Loading...</em></p>
: (
<table className="table table-striped" aria-labelledby="tableLabel">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
{forecasts.map((forecast, index) => (
<tr key={index}>
<td>{forecast.date}</td>
<td align="center">{forecast.temperatureC}</td>
<td align="center">{forecast.temperatureF}</td>
<td>{forecast.summary}</td>
</tr>
))}
</tbody>
</table>
);
return (
<div>
<h2 id="tableLabel">Weather forecast</h2>
<p>This component demonstrates fetching data from the server.</p>
{contents}
</div>
);
};
이 코드를 분해해봅시다:
Forecast
인터페이스는 날씨 예보 데이터의 구조를 정의하며, 날짜, 攝氏度和 华氏度的 온도, 以及 날씨의 요약을 포함합니다.State
인터페이스는 컴포넌트의 상태 구조를 기술하며, 날씨 예보 数组와 로딩 상태 기능(loading flag)로 구성되어 있습니다.WeatherForecast
컴포넌트는useBff
훅에서fetchBff
함수를 가져와 서버로부터 날씨 데이터를 받아오는 것을 사용합니다. 컴포넌트의 상태는useState
훅을 사용하여 초기화되며, 예보의 빈 数组과 로딩 기능을 TRUE로 설정합니다.useEffect
훅은 컴포넌트가 마운트되면fetchBff
함수를 실행하며,/bff/weatherforecast
エンDPOINTから 서버로부터 날씨 예보 데이터를 가져오는 것을 시도합니다. 서버의 응답을 받고 JSON로 변환하면, 이 데이터가 컴포넌트의 상태(setState
를 통해)에 저장되고, 로딩 기능이false
로 更新されます.- 로딩 기능의 값에 따라 컴포넌트는 “Loading…” 메시지를 보여주거나, 날씨 예보 데이터를 나열하는 테이블을 렌더링합니다. 이 테이블은 날짜, 摄氏度和 华氏度的 온도, 以及 날씨의 요약이 각 예보에 대한 列로 구성되어 있습니다.
WeatherForecast
컴포넌트를 BffSample\ClientApp\src\App.tsx
에 추가하세요.:
// ******************* START *******************
import { WeatherForecast } from "./components/WeatherForecast";
// ******************** END ********************
// ...
// ******************* START *******************
// ******************** END ********************
실행 및 테스트
모든 설정이 완료되었다면 이제 세 개의 프로젝트를 모두 시작할 수 있습니다. 각 애플리케이션을 HTTPS로 실행하려면 콘솔 명령어 dotnet run -lp https
를 사용하세요.
세 개의 애플리케이션을 모두 실행한 후, 브라우저에서 BffSample
애플리케이션 (https://localhost:5003)을 열고 자격 증명 [email protected]
과 Jd!2024$3cur3
를 사용하여 인증하세요. 인증이 성공적으로 완료되면, 인증 서버에서 수신한 클레임 목록이 이전과 같이 표시됩니다. 그 아래에는 날씨 예보도 확인할 수 있습니다.
날씨 예보는 별도의 애플리케이션 ApiSample
이 제공하며, 이 애플리케이션은 인증 서비스 OpenIDProviderApp
가 발급한 액세스 토큰을 사용합니다. BffSample
애플리케이션 창에서 날씨 예보를 보는 것은 우리의 SPA가 성공적으로 BffSample
의 백엔드를 호출하고, 이후 액세스 토큰을 추가하여 ApiSample
로 호출을 프록시했음을 나타냅니다. ApiSample
은 호출을 인증하고 날씨 예보를 포함한 JSON으로 응답했습니다.
완전한 솔루션은 GitHub에서 확인할 수 있습니다
Article이 설명하는 것처럼 完全に 구현된 프로젝트를 ACCESS하기 위해서는 GitHub 저장库를 CLONE하시면 됩니다. Abblix/Oidc.Server.GettingStarted 이란 저장库을 CLONE하면 이 문서에 기술된 전체 구현이 완료 된 프로젝트를 사용할 수 있습니다. 이 자원은 문제 해결 도구로 사용되는 동시에 자신의 프로젝트 생성의 견고한 시작 지점을 제공합니다.
결론
OAuth 2.0과 OpenID Connect 등의 인증 protocol의 발전은 웹 보안과 브라우저 기능의 더 넓은 trends를 반영합니다. Implicit Flow 같은 오래된 방법에서 더 안전한 방법으로 Authorization Code Flow with PKCE를 사용하는 것으로 이동하여 보안이 飞跃적으로 강화되었습니다. 그러나 모ERN SPA를 보호하는 것이 어려울 수 있는 언어적인 취약성을 가지고 있으며, 백端에서 전역적으로 토큰을 보관하고 Backend-For-Frontend(BFF) 패턴을 적용하는 것은 риском 감소하고 강한 사용자 데이터 보호를 Ensure하는 有效的な 전략입니다.
開発자는 素的に 변화하는 脅威 지역에 대한 対応을 하는 것이 중요하며, 신규 인증 방법과 최신 建築적 접근法을 실제izar하는 것이다. 이러한 forestsative approach은 보안하고 신뢰할 수 있는 웹 应用程序을 만들기 위한 중요한 요소가 됩니다. 이 문서에서는 인기 있는 .NET과 React 기술 스택을 사용하여 OpenID Connect, BFF, SPA를 现代化 접근法을 통해 이해하고 실제izar했습니다. 이러한 접근法은 将来 자신의 프로젝트에서 강한 기반을 제공할 수 있습니다.
우리가 미래를 바라볼 때, 웹 보안의 지속적인 진화는 인증 및 아키텍처 패턴에서 더욱 큰 혁신을 요구할 것입니다. GitHub 저장소를 탐색하고, 현대 인증 솔루션 개발에 기여하며, 지속적인 발전에 관심을 가져주시길 권장합니다. 관심을 가져주셔서 감사합니다!
Source:
https://dzone.com/articles/modern-authentication-on-dotnet