본문으로 건너뛰기

가이드: Spring Boot

이 튜토리얼에서는 클라이언트가 유효한 액세스 토큰을 가지고 있으며, 이를 요청 헤더에 Authorization: Bearer <access_token>으로 첨부했다고 가정합니다.

웹 애플리케이션은 Spring Boot 프레임워크를 사용하여 서버 측에서 실행될 수 있습니다. 현재로서는 Spring Boot에 Logto를 수동으로 통합해야 합니다. 이 문서는 단계별로 이를 완료하는 방법을 안내합니다. 예제로 Gradle, Java, Spring Security를 사용합니다.

Spring Boot 프로젝트 시작하기

Spring Initializr를 사용하여 Spring Boot 프로젝트를 빠르게 시작할 수 있습니다. 다음 옵션을 사용하세요:

  • Gradle Project
  • 언어: Java
  • Spring Boot: 2.7.2

프로젝트를 생성하고 엽니다.

종속성 추가하기

Gradle 프로젝트 빌드 파일 build.gradle에 종속성을 추가하세요:

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
}
노트:

Spring Boot와 Spring Security는 OAuth2 리소스 서버와 JWT 검증에 대한 내장 지원을 제공하므로, Logto에서 추가 라이브러리를 통합할 필요가 없습니다.

자세한 내용은 Spring Security OAuth 2.0 Resource ServerSpring Security Architecture를 참조하세요.

발급자 및 JWKS URI 가져오기

모든 토큰은 발급자에 의해 발급되며, JWK로 서명됩니다 (자세한 내용은 JWS를 참조하세요).

계속 진행하기 전에, 발급자와 JWKS URI를 얻어 Bearer 토큰 (액세스 토큰)의 발급자와 서명을 검증해야 합니다.

기본적으로, Logto의 발급자와 JWKS URI는 https://<your-logto-domain>/oidchttps://<your-logto-domain>/oidc/jwks입니다.

모든 최신 Logto 인가 서버 구성은 https://<your-logto-domain>/oidc/.well-known/openid-configuration에서 찾을 수 있으며, 여기에는 발급자, jwks_uri 및 기타 인가 구성이 포함됩니다. 예를 들어:

{
// ...
"issuer": "https://<your-logto-domain>/oidc",
"jwks_uri": "https://<your-logto-domain>/oidc/jwks"
// ...
}

애플리케이션 구성하기

서버 포트, 대상, OAuth2 리소스 서버를 구성하기 위해 application.properties 대신 application.yml 파일을 사용하세요.

# path/to/project/src/main/resources/application.yaml
server:
port: 3000

logto:
audience: http://localhost:3000/

spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: <your-logto-issuer-uri>
jwk-set-uri: <your-logto-jwks-uri>
  • audience: 보호된 API 리소스의 고유한 API 식별자 (즉, API 지표).
  • spring.security.oauth2.resourceserver.jwt.issuer-uri: Logto가 발급한 JWT의 iss 클레임 값과 발급자 URI. 이전 섹션에서 발급자 값을 입력하세요.
  • spring.security.oauth2.resourceserver.jwt.jwk-set-uri: Spring Security는 이 URI를 사용하여 JWT 서명을 검증하기 위해 인가 서버의 공개 키를 가져옵니다. 이전 섹션에서 jwks_uri 값을 입력하세요.

대상 검증기 제공하기

JWT에 필요한 대상이 있는지 검증하기 위해 OAuth2TokenValidator 인터페이스를 구현하는 AudienceValidator 클래스를 제공합니다.

// path/to/project/src/main/java/io/logto/springboot/sample/validator/AudienceValidator.java
package io.logto.springboot.sample.validator;

import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;

public class AudienceValidator implements OAuth2TokenValidator<Jwt> {

private final String audience;

public AudienceValidator(String audience) {
this.audience = audience;
}

@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
if (!jwt.getAudience().contains(audience)) {
return OAuth2TokenValidatorResult.failure(new OAuth2Error("invalid_token", "Required audience not found", null));
}

// 선택 사항: RBAC를 위해 JWT의 스코프를 검증합니다.
String scopes = jwt.getClaimAsString("scope");
if (scopes == null || !scopes.contains("read:profile")) {
return OAuth2TokenValidatorResult.failure(new OAuth2Error("invalid_token", "Insufficient permission", null));
}

return OAuth2TokenValidatorResult.success();
}
}

Spring Security 구성하기

Spring Security는 애플리케이션을 리소스 서버로 구성하고 요청 헤더의 Bearer 토큰에서 JWT를 검증하는 것을 쉽게 만듭니다.

JwtDecoderSecurityFilterChain 인스턴스를 제공하고 (Spring 빈으로), @EnableWebSecurity 주석을 추가해야 합니다.

노트:

Logto는 기본적으로 ES384 알고리즘을 사용하여 JWT를 서명합니다. JWT를 디코딩하려면 jwsAlgorithmES384로 명시적으로 설정해야 합니다. RSA를 사용하고 싶다면 Logto 관리 콘솔에서 서명 알고리즘을 회전할 수 있습니다. 자세한 내용은 서명 키를 참조하세요.

// path/to/project/src/main/java/io/logto/springboot/sample/configuration/SecurityConfiguration.java
package io.logto.springboot.sample.configuration;

import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
import com.nimbusds.jose.proc.SecurityContext;
import io.logto.springboot.sample.validator.AudienceValidator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.web.DefaultSecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

@Value("${logto.audience}")
private String audience;

@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;

@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
private String jwksUri;

@Bean
public JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwksUri)
// Logto는 기본적으로 ES384 알고리즘을 사용하여 JWT를 서명합니다.
.jwsAlgorithm(ES384)
// 디코더는 토큰 유형을 지원해야 합니다: 액세스 토큰 + JWT.
.jwtProcessorCustomizer(customizer -> customizer.setJWSTypeVerifier(
new DefaultJOSEObjectTypeVerifier<SecurityContext>(new JOSEObjectType("at+jwt"))))
.build();

jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
new AudienceValidator(audience),
new JwtIssuerValidator(issuer),
new JwtTimestampValidator()));

return jwtDecoder;
}

@Bean
public DefaultSecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults()))
.authorizeHttpRequests(requests -> requests
// 공개 API에 대한 모든 요청을 허용합니다.
.requestMatchers("/api/.wellknown/**").permitAll()
// 보호된 API에 대한 jwt 토큰 검증을 요구합니다.
.anyRequest().authenticated());

return http.build();
}
}

API 추가하기

보호된 API와 공개 API를 제공하기 위해 컨트롤러를 추가하세요:

// path/to/project/src/main/java/io/logto/springboot/sample/controller/ProtectedController.java
package io.logto.springboot.sample.controller;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

// 샘플을 위해 모든 출처를 허용합니다.
// (프로덕션 애플리케이션은 CORS를 신중하게 구성해야 합니다.)
@CrossOrigin(origins = "*")
@RestController
public class ProtectedController {
@GetMapping("/api/profile")
public String protectedProfile() {
return "Protected profile.";
}

@GetMapping("/api/.wellknown/config.json")
public String publicConfig() {
return "Public config.";
}
}

보호된 API에 접근하기

Spring Boot 웹 애플리케이션을 빌드하고 실행하세요. 예를 들어, bootRun Gradle 작업을 실행합니다.

./gradlew bootRun
노트:

이 튜토리얼은 요청을 하기 전에 API 리소스 http://localhost:3000/에 대한 액세스 토큰을 가지고 있다고 가정합니다. 준비가 되지 않았다면, 이것을 읽으세요 계속하기 전에.

Authorization 헤더에 액세스 토큰을 Bearer 토큰으로 사용하여 보호된 API에 요청하세요. 예를 들어, curl 명령을 실행합니다.

curl --include 'http://localhost:3000/api/profile' --header 'Authorization: Bearer <your-access-token>'

성공하면, 200 상태의 응답을 받게 됩니다:

HTTP/1.1 200
...

그렇지 않으면, 다음과 같은 401 상태의 응답을 받게 됩니다:

HTTP/1.1 401
...
WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: Signed JWT rejected: Invalid signature", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
...