본문으로 건너뛰기

RBAC 및 JWT 검증으로 Spring Boot API 보호하기

이 가이드는 역할 기반 접근 제어 (RBAC)와 Logto에서 발급된 JSON Web Token (JWT)을 사용하여 Spring Boot API를 안전하게 보호하는 인가 (Authorization) 구현 방법을 안내합니다.

시작하기 전에​

클라이언트 애플리케이션은 Logto에서 액세스 토큰 (Access token)을 받아야 합니다. 아직 클라이언트 통합을 설정하지 않았다면, React, Vue, Angular 또는 기타 클라이언트 프레임워크를 위한 빠른 시작이나 서버 간 접근을 위한 기계 간 (M2M) 가이드를 확인하세요.

이 가이드는 Spring Boot 애플리케이션에서 이러한 토큰의 서버 측 검증에 중점을 둡니다.

이 가이드의 중점을 보여주는 그림

학습 내용​

  • JWT 검증: 액세스 토큰 (Access token)을 검증하고 인증 (Authentication) 정보를 추출하는 방법 학습
  • 미들웨어 구현: API 보호를 위한 재사용 가능한 미들웨어 생성
  • 권한 모델: 다양한 인가 (Authorization) 패턴 이해 및 구현:
    • 애플리케이션 전체 엔드포인트를 위한 글로벌 API 리소스
    • 테넌트별 기능 제어를 위한 조직 권한
    • 다중 테넌트 데이터 접근을 위한 조직 수준 API 리소스
  • RBAC 통합: API 엔드포인트에서 역할 기반 권한 (Role) 및 스코프 (Scope) 적용

사전 준비 사항​

  • 최신 안정 버전의 Java 설치
  • Spring Boot 및 웹 API 개발에 대한 기본 이해
  • Logto 애플리케이션 구성 완료 (빠른 시작 참고)

권한 모델 개요​

보호를 구현하기 전에, 애플리케이션 아키텍처에 맞는 권한 모델을 선택하세요. 이는 Logto의 세 가지 주요 인가 (Authorization) 시나리오와 일치합니다:

글로벌 API 리소스 RBAC
  • 사용 사례: 애플리케이션 전체에서 공유되는 API 리소스를 보호 (조직별이 아님)
  • 토큰 유형: 글로벌 대상이 포함된 액세스 토큰 (Access token)
  • 예시: 공개 API, 핵심 제품 서비스, 관리자 엔드포인트
  • 적합 대상: 모든 고객이 사용하는 API가 있는 SaaS 제품, 테넌트 분리가 없는 마이크로서비스
  • 자세히 알아보기: 글로벌 API 리소스 보호하기

💡 진행하기 전에 모델을 선택하세요 - 이 가이드 전반에서 선택한 접근 방식을 참고하게 됩니다.

빠른 준비 단계​

Logto 리소스 및 권한 구성​

  1. API 리소스 생성: 콘솔 → API 리소스로 이동하여 API를 등록하세요 (예: https://api.yourapp.com)
  2. 권한 정의: read:products, write:orders와 같은 스코프를 추가하세요 – 권한과 함께 API 리소스 정의하기 참고
  3. 글로벌 역할 생성: 콘솔 → 역할로 이동하여 API 권한이 포함된 역할을 생성하세요 – 글로벌 역할 구성 참고
  4. 역할 할당: API 접근이 필요한 사용자 또는 M2M 애플리케이션에 역할을 할당하세요
RBAC가 처음이신가요?:

역할 기반 접근 제어 가이드에서 단계별 설정 방법을 시작하세요.

클라이언트 애플리케이션 업데이트​

클라이언트에서 적절한 스코프를 요청하세요:

이 과정에는 일반적으로 클라이언트 설정을 다음 중 하나 이상을 포함하도록 업데이트하는 것이 포함됩니다:

  • OAuth 플로우의 scope 파라미터
  • API 리소스 접근을 위한 resource 파라미터
  • 조직 컨텍스트를 위한 organization_id
코딩 전에:

테스트 중인 사용자 또는 M2M 앱에 API에 필요한 권한이 포함된 적절한 역할 또는 조직 역할이 할당되어 있는지 확인하세요.

API 프로젝트 초기화하기​

새로운 Spring Boot 프로젝트를 초기화하려면 Spring Initializr를 사용하거나 다음 단계를 따르세요:

Spring Initializr를 방문하여 다음을 선택하세요:

  • 프로젝트: Maven
  • 언어: Java
  • Spring Boot: 최신 안정 버전
  • 종속성: Spring Web, Spring Security

또는 수동으로 생성하세요:

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>

<groupId>com.example</groupId>
<artifactId>your-api-name</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>your-api-name</name>

<properties>
<java.version>17</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
</project>

기본 Spring Boot 애플리케이션을 생성하세요:

src/main/java/com/example/Application.java
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
노트:

컨트롤러, 서비스 및 기타 기능 설정 방법에 대한 자세한 내용은 Spring Boot 문서를 참조하세요.

상수 및 유틸리티 초기화하기​

코드에서 필요한 상수와 유틸리티를 정의하여 토큰 추출 및 유효성 검사를 처리하세요. 유효한 요청에는 Authorization 헤더가 반드시 Bearer <액세스 토큰 (Access token)> 형식으로 포함되어야 합니다.

AuthorizationException.java
public class AuthorizationException extends RuntimeException {
private final int statusCode;

public AuthorizationException(String message) {
this(message, 403); // 기본값은 403 금지(Forbidden)
}

public AuthorizationException(String message, int statusCode) {
super(message);
this.statusCode = statusCode;
}

public int getStatusCode() {
return statusCode;
}
}

Logto 테넌트 정보 가져오기​

Logto에서 발급한 토큰을 검증하려면 다음 값들이 필요합니다:

  • JSON Web Key Set (JWKS) URI: JWT 서명을 검증하는 데 사용되는 Logto의 공개 키 URL입니다.
  • 발급자 (Issuer): 예상되는 발급자 값 (Logto의 OIDC URL).

먼저, Logto 테넌트의 엔드포인트를 찾아야 합니다. 다음 위치에서 확인할 수 있습니다:

  • Logto 콘솔에서 설정 → 도메인에서 확인하세요.
  • Logto에서 구성한 애플리케이션 설정의 설정 → 엔드포인트 & 자격 증명에서 확인하세요.

OpenID Connect 디스커버리 엔드포인트에서 가져오기​

이 값들은 Logto의 OpenID Connect 디스커버리 엔드포인트에서 가져올 수 있습니다:

https://<your-logto-endpoint>/oidc/.well-known/openid-configuration

예시 응답입니다 (다른 필드는 생략):

{
"jwks_uri": "https://your-tenant.logto.app/oidc/jwks",
"issuer": "https://your-tenant.logto.app/oidc"
}

Logto는 JWKS URI 또는 발급자 (Issuer)를 커스터마이즈할 수 없으므로, 이 값들을 코드에 하드코딩할 수 있습니다. 하지만, 향후 설정이 변경될 경우 유지보수 부담이 커질 수 있으므로 프로덕션 애플리케이션에서는 권장하지 않습니다.

  • JWKS URI: https://<your-logto-endpoint>/oidc/jwks
  • 발급자 (Issuer): https://<your-logto-endpoint>/oidc

토큰 및 권한 검증하기​

토큰을 추출하고 OIDC 구성을 가져온 후, 다음을 검증하세요:

  • 서명: JWT는 유효해야 하며 Logto(JWKS를 통해)에서 서명되어야 합니다.
  • 발급자 (Issuer): Logto 테넌트의 발급자와 일치해야 합니다.
  • 대상 (Audience): Logto에 등록된 API의 리소스 지표 또는 해당되는 경우 조직 컨텍스트와 일치해야 합니다.
  • 만료: 토큰이 만료되지 않아야 합니다.
  • 권한 (스코프, Permissions): 토큰에는 API/동작에 필요한 스코프가 포함되어야 합니다. 스코프는 scope 클레임에 공백으로 구분된 문자열입니다.
  • 조직 컨텍스트: 조직 수준의 API 리소스를 보호하는 경우, organization_id 클레임을 검증하세요.

JWT 구조와 클레임에 대해 더 알아보려면 JSON Web Token 을 참고하세요.

각 권한 모델별로 확인해야 할 사항​

클레임과 검증 규칙은 권한 모델에 따라 다릅니다:

  • Audience 클레임 (aud): API 리소스 지표
  • Organization 클레임 (organization_id): 없음
  • 확인할 스코프(권한) (scope): API 리소스 권한

비-API 조직 권한의 경우, 조직 컨텍스트는 aud 클레임(예: urn:logto:organization:abc123)으로 표현됩니다. organization_id 클레임은 조직 수준 API 리소스 토큰에만 존재합니다.

팁:

안전한 멀티 테넌트 API를 위해 항상 권한(스코프)과 컨텍스트(대상, 조직)를 모두 검증하세요.

검증 로직 추가하기​

프레임워크에 따라 서로 다른 JWT 라이브러리를 사용합니다. 필요한 종속성을 설치하세요:

pom.xml에 다음을 추가하세요:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
JwtSecurityConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/protected/**").authenticated()
.anyRequest().permitAll()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.decoder(jwtDecoder()))
);
return http.build();
}

@Bean
public JwtDecoder jwtDecoder() {
// 배포 환경에서 이 환경 변수들을 반드시 설정하세요
String jwksUri = System.getenv("JWKS_URI");
String issuer = System.getenv("JWT_ISSUER");

return NimbusJwtDecoder.withJwkSetUri(jwksUri)
.issuer(issuer)
.build();
}
}
JwtValidator.java
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
import java.util.List;

@Component
public class JwtValidator {

public void verifyPayload(Jwt jwt) {
// 발급자 (Issuer) 검증은 Spring Security JWT 디코더에서 자동으로 처리됩니다
// 권한 (Permission) 모델에 따라 추가 검증 로직을 여기에 구현하세요
// 아래의 헬퍼 메서드를 사용하여 클레임 (Claim)을 추출할 수 있습니다

// 예시: throw new AuthorizationException("Insufficient permissions");
// 상태 코드는 Spring Security의 예외 처리에서 처리됩니다
}

// Spring Boot JWT를 위한 헬퍼 메서드
private List<String> extractAudiences(Jwt jwt) {
return jwt.getAudience();
}

private String extractScopes(Jwt jwt) {
return jwt.getClaimAsString("scope");
}

private String extractOrganizationId(Jwt jwt) {
return jwt.getClaimAsString("organization_id");
}
}

권한 (Permission) 모델에 따라 적절한 검증 로직을 구현하세요:

// audience 클레임이 API 리소스 지표와 일치하는지 확인
List<String> audiences = extractAudiences(token); // 프레임워크별 추출
if (!audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience");
}

// 글로벌 API 리소스에 필요한 스코프 확인
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // 실제 필요한 스코프로 교체
String scopes = extractScopes(token); // 프레임워크별 추출
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();

if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient scope");
}

클레임을 추출하는 헬퍼 메서드는 프레임워크별로 다릅니다. 위의 프레임워크별 검증 파일에서 구현 세부 정보를 확인하세요.

미들웨어를 API에 적용하기​

이제, 보호된 API 라우트에 미들웨어를 적용하세요.

ProtectedController.java
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

@RestController
public class ProtectedController {

@GetMapping("/api/protected")
public Map<String, Object> protectedEndpoint(@AuthenticationPrincipal Jwt jwt) {
// JWT에서 액세스 토큰 (Access token) 정보를 직접 가져옵니다
String scopes = jwt.getClaimAsString("scope");
List<String> scopeList = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();

return Map.of(
"sub", jwt.getSubject(),
"client_id", jwt.getClaimAsString("client_id"),
"organization_id", jwt.getClaimAsString("organization_id"),
"scopes", scopeList,
"audience", jwt.getAudience()
);
}
}

보호된 API 테스트하기​

액세스 토큰 (Access token) 받기​

클라이언트 애플리케이션에서: 클라이언트 통합을 설정했다면, 앱이 토큰을 자동으로 획득할 수 있습니다. 액세스 토큰 (Access token)을 추출하여 API 요청에 사용하세요.

curl / Postman으로 테스트할 때:

  1. 사용자 토큰: 클라이언트 앱의 개발자 도구에서 localStorage 또는 네트워크 탭에서 액세스 토큰 (Access token)을 복사하세요.

  2. 기계 간 (Machine-to-machine) 토큰: 클라이언트 자격 증명 플로우를 사용하세요. 다음은 curl을 사용한 비공식 예시입니다:

    curl -X POST https://your-tenant.logto.app/oidc/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=client_credentials" \
    -d "client_id=your-m2m-client-id" \
    -d "client_secret=your-m2m-client-secret" \
    -d "resource=https://your-api-resource-indicator" \
    -d "scope=api:read api:write"

    API 리소스 (API resource)와 권한 (Permission)에 따라 resource 및 scope 파라미터를 조정해야 할 수 있습니다. API가 조직 범위라면 organization_id 파라미터도 필요할 수 있습니다.

팁:

토큰 내용을 확인해야 하나요? 우리의 JWT 디코더를 사용하여 JWT를 디코드하고 검증하세요.

보호된 엔드포인트 테스트하기​

유효한 토큰 요청
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
http://localhost:3000/api/protected

예상 응답:

{
"auth": {
"sub": "user123",
"clientId": "app456",
"organizationId": "org789",
"scopes": ["api:read", "api:write"],
"audience": ["https://your-api-resource-indicator"]
}
}
토큰 없음
curl http://localhost:3000/api/protected

예상 응답 (401):

{
"error": "Authorization header is missing"
}
잘못된 토큰
curl -H "Authorization: Bearer invalid-token" \
http://localhost:3000/api/protected

예상 응답 (401):

{
"error": "Invalid token"
}

권한 (Permission) 모델별 테스트​

글로벌 스코프로 보호된 API 테스트 시나리오:

  • 유효한 스코프: 필요한 API 스코프(예: api:read, api:write)가 포함된 토큰으로 테스트하세요.
  • 스코프 누락: 토큰에 필요한 스코프가 없으면 403 Forbidden을 예상하세요.
  • 잘못된 대상 (Audience): 대상이 API 리소스와 일치하지 않으면 403 Forbidden을 예상하세요.
# 스코프가 누락된 토큰 - 403 예상
curl -H "Authorization: Bearer token-without-required-scopes" \
http://localhost:3000/api/protected

추가 자료​

실전 RBAC: 애플리케이션을 위한 안전한 인가 (Authorization) 구현하기

멀티 테넌트 SaaS 애플리케이션 구축: 설계부터 구현까지 완벽 가이드