Rust 백엔드에서 OAuth 2.0 인증 코드 흐름을 이용한 보안 구축
Grace Collins
Solutions Engineer · Leapcell

소개: OAuth 2.0으로 Rust 애플리케이션 보안 강화
오늘날 상호 연결된 디지털 환경에서 사용자 인증 및 권한 부여는 모든 웹 애플리케이션에 매우 중요합니다. 개발자로서 우리는 안전하고 확장 가능하며 사용자 친화적인 시스템을 구축하기 위해 노력합니다. 민감한 자격 증명을 직접 처리하지 않고 사용자 액세스를 관리하는 것과 관련하여 OAuth 2.0은 널리 채택되고 강력한 프레임워크로 두드러집니다. 특히 인증 코드 흐름은 액세스 토큰 노출을 최소화하므로 웹 애플리케이션에 권장되는 가장 안전한 방법입니다.
Rust는 성능, 메모리 안전성 및 동시성에 중점을 두어 백엔드 서비스를 구축하는 데 매력적인 선택이 되었습니다. OAuth 2.0을 Rust 백엔드에 통합하면 개발자는 Rust의 강점을 활용하는 동시에 보호된 리소스에 안전하게 액세스할 수 있습니다. 이 글에서는 Rust 백엔드에서 OAuth 2.0 인증 코드 흐름을 구현하는 과정을 안내하고, 기본 원칙을 설명하며, 실제 코드 예제를 시연합니다.
OAuth 2.0 인증 코드 흐름의 핵심 개념
구현 세부 사항을 자세히 살펴보기 전에 OAuth 2.0 인증 코드 흐름에 관련된 주요 역할과 단계를 명확하게 이해해 봅시다.
- 리소스 소유자: 보호된 리소스(예: 사진, 프로필 데이터 등)를 소유하고 액세스 권한을 부여하는 최종 사용자입니다.
- 클라이언트(귀하의 Rust 애플리케이션): 리소스 소유자의 보호된 리소스에 액세스하려고 하는 애플리케이션입니다. 인증 서버에 등록되어 있습니다.
- 인증 서버: 리소스 소유자의 인증을 담당하고 리소스 소유자의 동의를 얻은 후 클라이언트에 액세스 토큰을 발급합니다.
- 리소스 서버: 보호된 리소스를 호스팅하고 클라이언트에 대한 액세스를 허용하기 위해 액세스 토큰을 수락합니다. 종종 인증 서버와 리소스 서버는 동일한 엔터티이거나 긴밀하게 통합됩니다.
인증 코드 흐름은 다음과 같은 높은 수준의 단계로 진행됩니다.
- 인증 요청: 클라이언트(프런트엔드에서 시작된 귀하의 Rust 백엔드)는 리소스 소유자의 브라우저를 인증 서버로 리디렉션합니다. 이 요청에는 클라이언트 ID, 요청된 범위 및
redirect_uri
가 포함됩니다. - 사용자 인증 및 동의: 인증 서버는 리소스 소유자를 인증하고(이미 로그인하지 않은 경우) 요청된 범위에 대해 클라이언트에 대한 액세스를 부여하거나 거부하도록 프롬프트를 표시합니다.
- 인증 부여(인증 코드): 리소스 소유자가 액세스를 부여하면 인증 서버는
authorization_code
를 포함하여 브라우저를 클라이언트의redirect_uri
로 다시 리디렉션합니다. - 토큰 요청: 클라이언트(귀하의 Rust 백엔드)는 인증 서버의 토큰 엔드포인트로 직접 서버 간 요청을 보냅니다. 이 요청에는
authorization_code
, 클라이언트 ID, 클라이언트 시크릿 및redirect_uri
가 포함됩니다. - 토큰 응답: 인증 서버는 요청을 검증하고 성공하면
access_token
,refresh_token
(선택 사항) 및expires_in
(토큰 수명)을 반환합니다. - 리소스 액세스: 클라이언트는
access_token
을 사용하여 보호된 리소스에 대한 요청을 리소스 서버로 보냅니다.
Rust에서 OAuth 2.0 인증 코드 흐름 구현
Rust를 사용하여 단순화된 구현을 살펴보겠습니다. 주로 인증 코드를 처리하고 토큰으로 교환하는 백엔드의 역할에 중점을 둡니다. 웹 프레임워크로는 actix-web
을, HTTP 요청에는 reqwest
를 사용합니다. 프런트엔드가 초기 리디렉션을 처리하고 궁극적으로 브라우저를 통해 인증 코드를 수신한다고 가정합니다.
프로젝트 설정
먼저 새 Rust 프로젝트를 만들고 필요한 종속성을 추가합니다.
# cargo new oauth2_backend --bin # cd oauth2_backend
그런 다음 Cargo.toml
에 다음을 추가합니다.
[dependencies] actix-web = "4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" reqwest = { version = "0.11", features = ["json", "blocking"]} # 단순화를 위해 blocking 사용; 실제 앱에서는 async 권장 url = "2.2" dotenv = "0.15" # 환경 변수 관리를 쉽게 하기 위한 것
oauth2_backend
디렉터리에 .env
파일을 만들어 OAuth 2.0 자격 증명 및 기타 구성을 저장합니다.
CLIENT_ID="your_client_id" CLIENT_SECRET="your_client_secret" REDIRECT_URI="http://localhost:8080/callback" AUTH_SERVER_AUTH_URL="https://example.com/oauth/authorize" # 인증 서버 URL로 대체 AUTH_SERVER_TOKEN_URL="https://example.com/oauth/token" # 인증 서버 URL로 대체
핵심 로직: 콜백 및 토큰 교환 처리
Rust 백엔드는 주로 인증 서버가 인증 코드를 보내는 redirect_uri
엔드포인트를 처리합니다.
// src/main.rs use actix_web::{web, App, HttpResponse, HttpServer, Responder, http::header}; use serde::{Deserialize, Serialize}; use url::Url; use dotenv::dotenv; use std::env; // OAuth 2.0 클라이언트 세부 정보를 보유할 구성 구조체 struct AppConfig { client_id: String, client_secret: String, redirect_uri: String, auth_server_auth_url: String, auth_server_token_url: String, } // 인증 서버에서 들어오는 쿼리 매개변수를 나타내는 구조체 #[derive(Deserialize, Debug)] struct OAuthCallbackQuery { code: String, state: Option<String>, } // 토큰 요청 본문을 나타내는 구조체 #[derive(Serialize, Debug)] struct TokenRequest { grant_type: String, client_id: String, client_secret: String, redirect_uri: String, code: String, } // 인증 서버의 토큰 응답을 나타내는 구조체 #[derive(Deserialize, Debug)] struct TokenResponse { access_token: String, token_type: String, expires_in: u32, refresh_token: Option<String>, scope: Option<String>, } async fn index() -> impl Responder { // 실제 애플리케이션에서는 일반적으로 OAuth 흐름을 시작하기 위한 프런트엔드 리디렉션입니다. // 시연을 위해 링크만 표시하겠습니다. HttpResponse::Ok().body("Welcome! <a href=\"/login\">Login with OAuth</a>") } async fn login(data: web::Data<AppConfig>) -> impl Responder { let mut auth_url = Url::parse(&data.auth_server_auth_url).unwrap(); auth_url.query_pairs_mut() .append_pair("client_id", &data.client_id) .append_pair("redirect_uri", &data.redirect_uri) .append_pair("response_type", "code") .append_pair("scope", "openid profile email") // 예시 범위 .append_pair("state", "random_string_for_csrf_protection"); // CSRF에 필수 HttpResponse::Found() .insert_header((header::LOCATION, auth_url.to_string())) .finish() } async fn oauth_callback( query: web::Query<OAuthCallbackQuery>, data: web::Data<AppConfig>, ) -> impl Responder { println!("Received OAuth callback with code: {:?}", query.code); // 실제 앱에서는 CSRF 공격을 방지하기 위해 'state' 매개변수를 검증해야 합니다. let token_request = TokenRequest { grant_type: "authorization_code".to_string(), client_id: data.client_id.clone(), client_secret: data.client_secret.clone(), redirect_uri: data.redirect_uri.clone(), code: query.code.clone(), }; println!("Exchanging authorization code for tokens..."); let client = reqwest::blocking::Client::new(); match client .post(&data.auth_server_token_url) .header(header::ACCEPT, "application/json") .form(&token_request) // x-www-form-urlencoded를 위해 .form() 사용 .send() { Ok(response) => { if response.status().is_success() { match response.json::<TokenResponse>() { Ok(token_response) => { println!("Successfully obtained tokens: {:?}", token_response.access_token); // 토큰을 안전하게 저장(예: 세션, 데이터베이스) // 보호된 리소스 또는 대시보드로 리디렉션 HttpResponse::Ok().body(format!( "Login successful! Access Token: {}", token_response.access_token )) } Err(e) => { eprintln!("Failed to parse token response: {:?}", e); HttpResponse::InternalServerError().body(format!("Failed to parse token response: {}", e)) } } } else { let status = response.status(); let text = response.text().unwrap_or_else(|_| "N/A".to_string()); eprintln!("Token exchange failed with status: {} and body: {}", status, text); HttpResponse::InternalServerError().body(format!( "Token exchange failed: {} - {}", status, text )) } } Err(e) => { eprintln!("HTTP request for token exchange failed: {:?}", e); HttpResponse::InternalServerError().body(format!("HTTP request for token exchange failed: {}", e)) } } } #[actix_web::main] async fn main() -> std::io::Result<()> { dotenv().ok(); // .env 파일에서 환경 변수 로드 let config = AppConfig { client_id: env::var("CLIENT_ID").expect("CLIENT_ID not set"), client_secret: env::var("CLIENT_SECRET").expect("CLIENT_SECRET not set"), redirect_uri: env::var("REDIRECT_URI").expect("REDIRECT_URI not set"), auth_server_auth_url: env::var("AUTH_SERVER_AUTH_URL").expect("AUTH_SERVER_AUTH_URL not set"), auth_server_token_url: env::var("AUTH_SERVER_TOKEN_URL").expect("AUTH_SERVER_TOKEN_URL not set"), }; println!("Server running on http://127.0.0.1:8080"); HttpServer::new(move || { App::new() .app_data(web::Data::new(config.clone())) // 앱 구성 공유 .route("/", web::get().to(index)) .route("/login", web::get().to(login)) .route("/callback", web::get().to(oauth_callback)) }) .bind(("127.0.0.1", 8080))? // .run() .await }
코드 설명
AppConfig
: 환경 변수에서 로드된 OAuth 클라이언트 자격 증명을 보유하는 구조체입니다.index
및login
핸들러:index
경로는 플레이스홀더입니다.login
경로는 백엔드(또는 일반적으로 프런트엔드)가 인증 URL을 구성하고 사용자 브라우저를 인증 서버로 리디렉션하는 방법을 보여줍니다.oauth_callback
핸들러:- 이것은 백엔드 OAuth 구현의 핵심입니다. 인증 서버가 사용자 동의 후 인증 코드를 보내는
redirect_uri
입니다. web::Query
를 사용하여 쿼리 매개변수에서code
를 역직렬화합니다.TokenRequest
구조체가 생성되며, 이는 인증 코드를 액세스 토큰으로 교환하는 데 필요한 모든 매개변수를 포함합니다.client_secret
은 클라이언트 측이 아닌 안전한 백엔드에서 직접 전송됩니다.reqwest::blocking::Client
(이 예제에서는 단순화를 위해 사용)를 사용하여 인증 서버의 토큰 엔드포인트로 POST 요청을 보냅니다.TokenResponse
가 역직렬화됩니다. 성공하면access_token
과 잠재적으로refresh_token
이 성공적으로 얻어진 것입니다.- 중요 후속 조치(이 기본 예제에는 없음):
- 상태 매개변수 검증: 항상
state
매개변수를 검증하여 CSRF 공격을 방지합니다. 인증 서버로 리디렉션하기 전에 서버에서 무작위state
를 생성하고 저장합니다(예: 세션에). 콜백이 수신되면 수신된state
와 저장된state
를 비교합니다. - 토큰 저장:
access_token
(예: 암호화된 세션 쿠키 또는 사용자에게 연결된 데이터베이스에)을 안전하게 저장합니다. refresh_token
사용:refresh_token
이 제공되면 안전하게 저장합니다. 현재access_token
이 만료될 때 사용자가 다시 인증할 필요 없이 새access_token
을 얻는 데 사용합니다.- 사용자 정보: 종종 액세스 토큰을 획득한 후 인증 서버의 사용자 정보 엔드포인트(또는 다른 보호된 리소스)로 추가 요청을 보내 기본 사용자 세부 정보(예:
openid profile
범위)를 가져옵니다.
- 상태 매개변수 검증: 항상
- 이것은 백엔드 OAuth 구현의 핵심입니다. 인증 서버가 사용자 동의 후 인증 코드를 보내는
main
함수:dotenv
를 사용하여 환경 변수를 로드합니다.AppConfig
를 초기화합니다.actix-web
라우트를 구성하고web::Data
를 사용하여 핸들러 전반에AppConfig
를 공유합니다.
애플리케이션 시나리오
이 구현은 몇 가지 일반적인 패턴에 대한 기반을 제공합니다.
- 단일 로그인(SSO): Google, GitHub 또는 Okta와 같은 ID 공급자와 통합하여 사용자가 기존 계정을 사용하여 로그인할 수 있도록 합니다.
- 타사 통합: 사용자의 동의를 얻어 애플리케이션이 다른 서비스의 사용자 데이터에 액세스하도록 허용합니다. 예를 들어 Google Calendar에서 캘린더를 가져오거나 소셜 미디어 플랫폼에서 게시물을 가져옵니다.
- API 인증: 클라이언트 애플리케이션에 액세스 토큰을 발급하여 자체 API를 보호하고, 승인된 클라이언트만 보호된 백엔드 리소스에 액세스할 수 있도록 합니다.
결론: Rust 백엔드를 위한 강력한 인증
Rust 백엔드에서 OAuth 2.0 인증 코드 흐름을 구현하는 것은 사용자 인증 및 권한 부여를 관리하는 안전하고 유연하며 업계 표준 방법을 제공합니다. 서버 측에서 인증 코드를 액세스 토큰으로 교환하는 작업을 신중하게 처리함으로써 민감한 자격 증명을 보호하고 애플리케이션이 사용자를 대신하여 보호된 리소스에 안전하게 액세스할 수 있도록 보장합니다. Rust의 고유한 안전 기능과 결합된 이 강력한 접근 방식은 안전하고 고성능 웹 서비스를 구축하는 데 견고한 기반을 제공합니다.