Spring Security — OAuth2와 JWT 한 번에

2026-05-02AWS SAA-C03 스터디

Spring Boot 3 핵심 정리 시리즈 7편. Spring Security가 처음엔 왜 어렵게 느껴지는지부터 회사 출입 보안 시스템 비유로 풀어가며 — HTTP Basic 인증의 한계, SecurityFilterChain 람다 DSL, OAuth2의 4가지 역할(Resource Owner·Client·Authorization Server·Resource Server), JWT 헤더·페이로드·서명 구조와 비대칭 키 검증, Spring Authorization Server 1.0 직접 구축, Resource Server에 issuer-uri 한 줄 설정으로 JWT 검증, 테스트에서 .with(jwt()) 사용까지 처음 다루는 분도 따라올 수 있게 친절하게 풀어쓴 7편.

📚 Spring Boot 3 핵심 정리 · 7편 / 14편 — OAuth2와 JWT 한 번에

이 글은 Spring Boot 3 핵심 정리 시리즈의 일곱 번째 편입니다. 6편까지 따라오셨다면 이제 데이터 모델·관계·트랜잭션까지 단단하게 다룰 수 있을 거예요. 그런데 지금까지 만든 API는 — 누구든지 URL만 알면 그냥 호출할 수 있어요. 운영 환경에서는 절대 있어선 안 되는 상태죠. 7편의 주제는 바로 Spring Security 입니다. 인증(누구인지)과 인가(뭘 할 수 있는지)를 다루는 Spring 생태계의 표준 보안 프레임워크예요.

이번 편에서는 Spring Security 의존성 한 줄 추가로 시작해 — HTTP Basic 인증의 한계, OAuth2의 4가지 역할, JWT의 구조와 검증 원리, Spring Authorization Server 직접 구축까지 단계별로 풀어 갑니다. 이름이 어렵게 들려도 회사 출입 보안 시스템에 비유해 보면 한 번에 잡혀요.

왜 Spring Security가 처음엔 어렵게 느껴질까요

이유는 네 가지예요.

첫째, 용어가 비슷비슷합니다. 인증(Authentication) vs 인가(Authorization), Resource Owner vs Resource Server, Authorization Server vs Resource Server — 이름이 길고 한 글자만 다른 단어가 줄지어 있어 머리가 어지러워져요.

둘째, OAuth2가 등장하는 순간 갑자기 4명의 역할이 동시에 나옵니다. 사용자, 클라이언트 앱, 인증 서버, 리소스 서버 — 누가 무슨 정보를 누구에게 보내는지 한눈에 안 잡혀요.

셋째, SecurityFilterChain 안에 람다가 중첩으로 박혀 있는 코드가 처음엔 어렵게 보여요. Spring Security 6 이전 방식은 .and() 체이닝이었는데, 6에서는 람다 기반 DSL로 바뀌었어요. 인터넷에 오래된 예제가 많아 헷갈립니다.

넷째, JWT가 추상화 한 단계 더 높습니다. "토큰"이라는 단어가 무엇을 가리키는지, 왜 비밀번호 대신 토큰을 쓰는지, 토큰 안에 뭐가 들어 있는지 — 처음에는 와닿지 않아요.

해결법은 한 가지예요. Spring Security를 "회사 출입 보안 시스템" 으로 잡고, OAuth2를 "외부 인증서로 출입증 받기" 로 풀면 갑자기 명확해집니다. 인증은 정문 경비, 인가는 부서별 출입권, JWT는 사번이 박힌 임시 출입증 — 이 비유로 풀어 갑니다.

인증 vs 인가 — 정문 경비와 부서별 출입권

먼저 기본 용어부터 정리하고 가요. 이 둘이 비슷해 보여도 완전히 다른 일을 합니다.

인증(Authentication)"당신이 누구인가?" 를 확인하는 과정이에요. 회사로 치면 — 정문 경비가 사번 카드를 찍어서 "OO부서 OOO 님 맞으시군요" 확인하는 단계입니다. 사용자가 제시한 자격 증명(아이디/비밀번호 또는 토큰)이 유효한지 검증해요.

인가(Authorization)"당신이 이걸 할 수 있는가?" 를 결정하는 과정이에요. 회사로 치면 — 사원증으로 정문은 통과했지만, 임원 회의실에는 또 별도 권한이 필요한 식이죠. 인증된 사용자가 특정 리소스나 기능에 접근할 권한이 있는지 검사합니다.

Spring Security는 이 두 과정을 필터 체인(Filter Chain) 방식으로 처리해요. 요청이 들어오면 여러 필터를 순서대로 거치면서 인증·인가·CSRF 보호 등을 적용합니다.

Client → HTTP Request
         ↓
    Security Filter Chain
    ├── AuthenticationFilter
    ├── AuthorizationFilter
    └── ...
         ↓
    DispatcherServlet → Controller → Service → Repository

HTTP Basic 인증 — 가장 단순한 출발점

가장 쉬운 인증 방식부터 시작할게요. HTTP Basic 인증은 사용자 이름과 비밀번호를 username:password 형식으로 결합한 후 Base64로 인코딩해서 Authorization: Basic 헤더에 박아 보내는 방식이에요.

Authorization: Basic dXNlcjE6cGFzc3dvcmQ=
# Base64 디코딩하면 "user1:password"

여기서 정말 중요한 시험 함정 — Base64는 암호화가 아니라 단순 인코딩입니다. 누구든지 디코더 한 줄이면 원본 비밀번호를 볼 수 있어요. 따라서 HTTP Basic 인증은 반드시 HTTPS 환경에서만 써야 합니다. HTTP에서 쓰면 네트워크 감청자가 그냥 비밀번호를 가져갈 수 있어요.

Spring Boot에서는 spring-boot-starter-security 의존성 한 줄로 즉시 활성화돼요.

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
# application.properties — 개발용 고정 자격 증명 (운영에선 환경변수)
spring.security.user.name=user1
spring.security.user.password=password

의존성을 추가한 순간부터 모든 엔드포인트가 자동으로 보호되며, 별도 설정 없이도 user라는 기본 사용자와 콘솔에 출력되는 임의 비밀번호가 생성돼요.

여기서 시험 함정이 하나 있어요. Spring Security를 추가하면 기존 MockMvc 테스트가 401 Unauthorized로 다 실패합니다. 인증 정보가 없으니 거부당하는 거죠. 해결법은 spring-security-test 의존성을 추가하고 .with(httpBasic(...))로 인증 정보를 박는 거예요.

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;

@Test
void testListProducts() throws Exception {
    mockMvc.perform(get(PRODUCT_PATH)
                    .with(httpBasic("user1", "password")))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.content").isArray());
}

SecurityFilterChain 커스터마이징 — Spring Security 6의 람다 DSL

Spring Security 6에서는 람다 기반 DSL로 보안 설정을 구성해요. 이전의 .and() 메서드 체이닝 방식은 deprecated 됐습니다.

@Configuration
public class SpringSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> {
                authorize.anyRequest().authenticated();
            })
            // CSRF는 RESTful API에서 일반적으로 비활성화
            // REST API는 Stateless하므로 CSRF 공격 대상이 아님
            .csrf(csrf -> {
                csrf.ignoringRequestMatchers("/api/**");
            })
            .httpBasic(Customizer.withDefaults());

        return http.build();
    }
}

람다 안에 람다가 들어 있어 처음 보면 어색하지만, 익숙해지면 — 각 보안 영역을 명확하게 묶어 표현한다는 장점이 보여요. authorizeHttpRequests는 인가 규칙, csrf는 CSRF 보호, httpBasic는 Basic 인증 활성화 — 각자 영역 안에서 람다로 세부 설정합니다.

비교표를 한 번 박아 두면 좋아요.

// Spring Security 5 방식 (deprecated)
http.csrf().disable()
    .authorizeRequests().anyRequest().authenticated()
    .and()
    .httpBasic();

// Spring Security 6 방식 (권장)
http
    .csrf(csrf -> csrf.disable())
    .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
    .httpBasic(Customizer.withDefaults());

여기서 시험 함정이 하나 있어요. 2020년 이전의 인터넷 예제는 거의 모두 5 방식이라 그대로 따라 적으면 deprecation 경고가 줄지어 뜹니다. 반드시 최신 6 공식 문서나 6용 예제를 참고하세요.

CSRF — REST API에서는 끄는 게 정석

CSRF(Cross-Site Request Forgery) 는 사용자가 로그인된 상태로 다른 사이트의 악성 링크를 클릭했을 때, 그 사이트가 사용자의 세션 쿠키를 도용해 원래 사이트에 위조 요청을 보내는 공격이에요. Spring Security는 기본적으로 CSRF 보호를 활성화합니다.

다만 REST API는 Stateless라서 — 매 요청에 토큰을 따로 보내는 구조라 — CSRF 공격 대상이 아니에요. CSRF 보호를 켜 두면 POST·PUT·DELETE 요청이 403 Forbidden으로 거부당해 오히려 정상 호출을 막습니다.

// REST API 패스만 CSRF 비활성화
.csrf(csrf -> csrf.ignoringRequestMatchers("/api/**"))

// 또는 REST API만 있는 서비스라면 전체 비활성화
.csrf(csrf -> csrf.disable())

> 한 줄 정리 — REST API = Stateless = CSRF 비활성화, 폼 기반 웹 앱 = CSRF 활성화 유지.

OAuth2 등장 — 4명의 역할

HTTP Basic 인증은 단순하지만 한계가 큽니다. 매 요청마다 비밀번호를 보내야 하고, 비밀번호 자체가 토큰 역할을 하니 — 한 번 새면 끝이에요. 그래서 등장한 게 OAuth 2.0 입니다.

OAuth2는 한 줄로 — "비밀번호 대신 임시 토큰으로 출입한다" 는 패턴이에요. 회사 비유로 — 외부 협력사 직원이 출입할 때, 우리 회사가 그 사람의 회사 비밀번호를 알 필요는 없죠. 그 사람 회사가 발급한 인증서를 보여주면, 우리는 거기에 적힌 권한만 보고 임시 출입증을 줘요. 이 흐름이 OAuth2입니다.

OAuth2에는 4가지 역할이 등장해요. 이 4명의 관계만 잡으면 OAuth2의 절반은 끝납니다.

역할설명예시
Resource Owner리소스를 소유한 사용자애플리케이션 사용자
Client리소스에 접근하려는 애플리케이션모바일 앱, 다른 마이크로서비스
Authorization Server토큰을 발급하는 서버Spring Authorization Server
Resource Server보호된 리소스를 제공하는 서버Spring MVC REST API

회사 비유로 풀면 — Resource Owner는 우리 회사 직원, Client는 외부에서 일을 의뢰하는 협력사 앱, Authorization Server는 인사부(출입증 발급), Resource Server는 실제 데이터가 있는 부서 사무실이에요.

Client Credentials 흐름 — 서버 간 통신

서비스끼리 통신할 때(machine-to-machine)는 사용자가 개입하지 않아요. 이때 쓰는 게 Client Credentials 흐름입니다.

Client → (client_id + client_secret) → Authorization Server
                                        ↓ JWT Access Token
Client → (Bearer JWT Token) → Resource Server
                               ↓ 검증 후 리소스 반환
Client ← 리소스 데이터 ←

순서를 풀면 이래요. (1) 클라이언트 앱이 자기 ID와 secret을 인증 서버로 보냄. (2) 인증 서버가 JWT 토큰을 발급. (3) 클라이언트는 그 토큰을 들고 리소스 서버에 호출. (4) 리소스 서버가 토큰을 검증하고 데이터 반환. 비밀번호가 한 번도 리소스 서버에 가지 않는다는 게 핵심이에요.

JWT — 임시 출입증의 구조

OAuth2가 발급하는 토큰의 표준 형식이 JWT(JSON Web Token) 예요. 세 부분이 점(.)으로 구분돼 있어요.

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.           ← 헤더 (알고리즘, 타입)
eyJzdWIiOiJjbGllbnQxIiwic2NvcGUiOiJyZWFkIn0.    ← 페이로드 (클레임)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV...           ← 서명 (변조 방지)

각 부분은 Base64Url로 인코딩돼 있어요. 페이로드 안에는 다음 같은 표준 클레임이 들어갑니다.

{
  "sub": "client1",                  // Subject: 토큰 발급 대상
  "iss": "http://localhost:9000",    // Issuer: 토큰 발급자
  "iat": 1698764400,                 // Issued At: 발급 시간
  "exp": 1698768000,                 // Expiration: 만료 시간
  "scope": "read write"              // 허용된 권한 범위
}

여기서 정말 중요한 시험 함정 — JWT의 페이로드는 Base64로만 인코딩돼 있어 누구나 읽을 수 있어요. 비밀 정보(비밀번호 등)는 절대 페이로드에 박지 않습니다. 그러면 왜 안전한가요?

답은 서명 부분에 있어요. JWT는 비대칭 암호화(RSA 또는 EC) 로 서명돼 있습니다.

Authorization Server:
  - 개인 키로 JWT 서명
  - /oauth2/jwks 엔드포인트로 공개 키 제공

Resource Server:
  - /oauth2/jwks에서 공개 키 가져와 캐싱
  - 요청의 JWT를 공개 키로 검증
  - 매 요청마다 Authorization Server에 문의할 필요 없음 (Stateless!)

인증 서버는 자기만 가진 개인 키로 서명을 박고, 리소스 서버는 공개 키로 그 서명을 검증해요. 누군가 페이로드를 변조하면 서명이 깨져 즉시 거부됩니다. 그리고 공개 키는 한 번 캐싱해 두면 매 요청마다 인증 서버에 다시 묻지 않아도 돼요 — 이게 Stateless 검증의 핵심 장점입니다.

Spring Authorization Server 구축

이제 직접 인증 서버를 띄워 볼게요. Spring 생태계에는 Spring Authorization Server라는 공식 프로젝트가 있어요. 2023년 1.0 GA가 출시돼 프로덕션에서도 쓸 수 있습니다.

의존성

Spring Initializr에는 없으니 수동으로 추가합니다.

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>1.0.1</version>
</dependency>

SecurityConfig — 핵심 빈 4개

인증 서버의 핵심 빈은 4개예요. 한 번에 다 보여 드릴게요.

@Configuration
public class SecurityConfig {

    // 1. 인증 서버의 핵심 보안 필터 체인
    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(
            HttpSecurity http) throws Exception {

        // OAuth2 인증 서버 기본 보안 설정 적용
        // 다음 엔드포인트들이 활성화됨:
        // POST /oauth2/token  - 토큰 발급
        // GET  /oauth2/jwks   - 공개 키 제공
        // POST /oauth2/revoke - 토큰 취소
        // GET  /.well-known/openid-configuration - 인증 서버 메타데이터
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults()); // OpenID Connect 활성화

        http
            .exceptionHandling(ex -> ex
                .defaultAuthenticationEntryPointFor(
                    new LoginUrlAuthenticationEntryPoint("/login"),
                    new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                )
            )
            .oauth2ResourceServer(resourceServer ->
                resourceServer.jwt(Customizer.withDefaults())
            );

        return http.build();
    }

    // 2. 일반 보안 설정 (폼 로그인 등)
    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize ->
                authorize.anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());

        return http.build();
    }

    // 3. 인메모리 사용자 (개발용)
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails userDetails = User.withDefaultPasswordEncoder()
                .username("user")
                .password("password")
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(userDetails);
    }

    // 4. 클라이언트 등록 정보 — 어떤 앱이 토큰을 받을 수 있는지
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient registeredClient = RegisteredClient
                .withId(UUID.randomUUID().toString())
                .clientId("messaging-client")
                .clientSecret("{noop}secret")     // 운영에서는 BCrypt 사용
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .scope("message.read")
                .scope("message.write")
                .tokenSettings(TokenSettings.builder()
                        .accessTokenTimeToLive(Duration.ofHours(1))
                        .build())
                .build();

        return new InMemoryRegisteredClientRepository(registeredClient);
    }

    // 5. JWT 서명을 위한 RSA 키 쌍
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    private static KeyPair generateRsaKey() {
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            return keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }

    // 6. 인증 서버 설정 (발급자 URI 등)
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder()
                .issuer("http://localhost:9000")
                .build();
    }
}

코드가 길어 보이지만 큰 그림은 단순해요. (1) 인증 서버용 필터 체인을 우선순위 1로 등록 + (2) 일반 폼 로그인용 필터 체인을 우선순위 2로 등록 + (3) 사용자 저장소 + (4) 클라이언트 저장소 + (5) JWT 서명용 RSA 키 + (6) 발급자 URI. 빈 6개로 OAuth2 인증 서버가 완성됩니다.

여기서 시험 함정이 하나 있어요. @Order(Ordered.HIGHEST_PRECEDENCE) — 인증 서버 필터 체인이 가장 먼저 실행되도록 하는 게 핵심입니다. 일반 폼 로그인 필터가 먼저 실행되면 토큰 엔드포인트도 폼 로그인으로 보내려 해서 — 클라이언트의 토큰 요청이 깨져요.

Resource Server 구성 — issuer-uri 한 줄

리소스 서버는 인증 서버보다 훨씬 간단해요. 의존성 추가 + 설정 한 줄 + 필터 체인 작성 — 끝입니다.

의존성

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

application.properties — 한 줄

# 인증 서버의 발급자 URI 설정
# 리소스 서버는 이 URI로 OpenID 구성을 자동 발견
# (/.well-known/openid-configuration 엔드포인트 조회)
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000

이 한 줄로 — 리소스 서버는 시작 시 인증 서버에 메타데이터를 요청해 자동으로 공개 키 위치를 알아내고, 그 공개 키로 들어오는 JWT를 검증해요.

SecurityFilterChain

@Configuration
public class SpringSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> {
                authorize.anyRequest().authenticated();
            })
            // HTTP Basic 인증 제거
            // JWT 토큰 기반 인증으로 전환
            .oauth2ResourceServer(oauth2 -> {
                // JWT 검증 활성화
                // issuer-uri를 통해 공개 키를 자동으로 가져와 캐싱
                oauth2.jwt(Customizer.withDefaults());
            })
            // REST API는 Stateless이므로 세션 불필요
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            // REST API는 CSRF 불필요
            .csrf(csrf -> csrf.disable());

        return http.build();
    }
}

여기서 시험 함정이 하나 있어요. SessionCreationPolicy.STATELESS — 리소스 서버는 세션을 만들지 말아야 합니다. JWT 자체가 자기 정보를 담고 있어 세션이 불필요해요. 세션을 만들면 메모리 낭비 + 분산 환경에서 세션 동기화 문제가 생깁니다.

또 하나 — 인증 서버가 실행 중이지 않을 때 리소스 서버를 띄우면 Unable to resolve the Configuration with the provided Issuer 오류가 납니다. 리소스 서버는 시작 시 인증 서버에 공개 키를 요청하기 때문이에요. 개발 환경에서는 인증 서버를 먼저 띄우고 그다음 리소스 서버가 정석입니다.

테스트 — JWT Mock 사용

OAuth2로 전환하면 기존 HTTP Basic 테스트가 모두 401로 실패해요. 이번엔 SecurityMockMvcRequestPostProcessors.jwt() 로 JWT를 모의해서 테스트합니다.

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;

@Test
void testListProducts() throws Exception {
    mockMvc.perform(get(PRODUCT_PATH)
                    .with(jwt()))  // 기본 JWT Mock
            .andExpect(status().isOk());
}

@Test
void testListProductsWithCustomClaims() throws Exception {
    mockMvc.perform(get(PRODUCT_PATH)
                    .with(jwt()
                            .jwt(token -> token
                                    .subject("client1")
                                    .claim("scope", "message.read")
                            )
                    ))
            .andExpect(status().isOk());
}

// 인증 없이 접근 시 401 응답 확인
@Test
void testListProductsNoAuth() throws Exception {
    mockMvc.perform(get(PRODUCT_PATH))
            .andExpect(status().isUnauthorized());
}

// 변조된 JWT는 거부되어야 함
@Test
void testInvalidToken() throws Exception {
    mockMvc.perform(get(PRODUCT_PATH)
                    .header(HttpHeaders.AUTHORIZATION, "Bearer invalid.jwt.token"))
            .andExpect(status().isUnauthorized());
}

.with(jwt()) 한 줄로 가짜 JWT가 박혀 — 실제 인증 서버 없이도 테스트가 돌아갑니다. 클레임을 커스터마이즈할 수 있고, 변조된 토큰이 거부되는지도 함께 검증하는 게 정석이에요.

HTTP Basic vs OAuth2 + JWT — 한 번에 비교

지금까지 풀어 본 두 방식을 한 표로 묶어 정리할게요.

항목HTTP Basic 인증OAuth2 + JWT
자격 증명 전송매 요청마다 비밀번호 전송토큰만 전송 (비밀번호 노출 없음)
보안 수준낮음 (Base64 인코딩에 불과)높음 (서명된 JWT)
토큰 만료없음있음 (exp 클레임)
세밀한 권한없음있음 (scope)
분산 시스템매 요청마다 자격 증명 검증 필요공개 키로 로컬 검증 가능
구현 복잡도매우 낮음높음
적합한 상황내부 개발용, 빠른 프로토타입프로덕션, 마이크로서비스

OAuth2 Grant Type 비교

OAuth2에는 시나리오별로 다른 흐름이 있어요. 주요 5가지를 정리합니다.

Grant Type사용 시나리오사용자 개입적합한 경우
Client Credentials서버 간 통신없음마이크로서비스 간 통신
Authorization Code사용자 대리있음소셜 로그인, 사용자 동의 필요
Implicit구형 SPA (deprecated)있음더 이상 권장하지 않음
Resource Owner Password직접 자격 증명 (deprecated)있음더 이상 권장하지 않음
Refresh Token액세스 토큰 갱신없음장기 세션 유지
Device CodeIoT, CLI 도구별도 기기브라우저 없는 환경

가장 자주 쓰는 두 가지는 Client Credentials(서버끼리)와 Authorization Code(사용자 로그인)예요. 나머지는 사양 진화 과정에서 deprecated 됐거나 특수 시나리오용입니다.

Postman으로 직접 토큰 발급해 보기

이론을 코드로 풀어 봤으니 한 번 직접 토큰을 발급받아 호출해 보면 — 그림이 한 번에 잡혀요.

# 1단계: Authorization Server에서 토큰 발급
POST http://localhost:9000/oauth2/token
Authorization: Basic bWVzc2FnaW5nLWNsaWVudDpzZWNyZXQ=
# Authorization 헤더 값은 base64("messaging-client:secret")
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=message.read

# 응답:
{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "scope": "message.read"
}

# 2단계: Resource Server API 호출
GET http://localhost:8080/api/v1/product
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

직접 한 번 받아 보면 — jwt.io에 토큰을 붙여넣어 클레임을 눈으로 확인하는 것도 학습에 큰 도움이 됩니다.

더 자세히 — 공식 문서

Spring Security와 OAuth2의 세부 사양은 Spring Security 공식 레퍼런스OAuth 2.0 표준 RFC에서 확인할 수 있어요. Spring Authorization Server의 RegisteredClient 옵션, JWT 클레임 커스터마이즈, 다양한 Grant Type 흐름까지 친절하게 정리돼 있습니다.

시험 직전 한 번 더 — 자주 헷갈리는 함정 모음

여기까지가 7편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.

  • 인증(Authentication) = "누구인가" / 인가(Authorization) = "뭘 할 수 있는가"
  • Spring Security는 필터 체인 기반 — 요청이 여러 필터를 순서대로 거침
  • spring-boot-starter-security 한 줄 = 모든 엔드포인트 자동 보호
  • HTTP Basic = username:password Base64 인코딩 → 암호화 아님!
  • HTTP Basic은 반드시 HTTPS에서만 사용 — HTTP는 비밀번호 그대로 노출
  • 운영 환경에서 비밀번호 하드코딩 금지 — ${APP_USER_PASSWORD} 환경변수
  • Spring Security 6 = 람다 기반 DSL / WebSecurityConfigurerAdapter deprecated
  • SecurityFilterChain 빈 직접 등록 방식이 권장
  • REST API = Stateless = CSRF 비활성화 (csrf.disable() 또는 ignoringRequestMatchers)
  • 폼 기반 웹 앱 = CSRF 활성화 유지
  • OAuth2 4가지 역할 — Resource Owner / Client / Authorization Server / Resource Server
  • Client Credentials = 서버 간 통신 (사용자 개입 없음)
  • Authorization Code = 사용자 대리 (소셜 로그인)
  • Implicit·Resource Owner Password = deprecated
  • JWT 구조 = 헤더.페이로드.서명 (점으로 구분, Base64Url 인코딩)
  • 페이로드는 누구나 읽을 수 있음 — 비밀 정보 박지 말기
  • JWT 서명 = 비대칭 키 (개인 키 서명, 공개 키 검증)
  • /oauth2/jwks = 공개 키 제공 엔드포인트 (인증 서버)
  • 리소스 서버는 공개 키 캐싱 → Stateless 검증
  • Spring Authorization Server 1.0 GA = 프로덕션 사용 가능
  • 인증 서버 핵심 빈 = SecurityFilterChain + RegisteredClientRepository + JWKSource + AuthorizationServerSettings
  • @Order(Ordered.HIGHEST_PRECEDENCE) = 인증 서버 필터가 가장 먼저 실행
  • 리소스 서버 = spring-boot-starter-oauth2-resource-server + issuer-uri 한 줄
  • 리소스 서버 시작 전에 인증 서버를 먼저 기동해야 공개 키 가져올 수 있음
  • SessionCreationPolicy.STATELESS = 리소스 서버는 세션 안 만듦
  • 테스트 — HTTP Basic = .with(httpBasic("user", "pass"))
  • 테스트 — JWT = .with(jwt()) + .jwt(token -> token.subject(...).claim(...))
  • 변조된 JWT = 서명 검증 실패 → 401 거부 (테스트로 확인)
  • issuer-uri 포트 오타 = connection refused → 모든 요청 401

시리즈 다른 편

같은 시리즈의 다른 글들도 같은 친절 톤으로 묶어 정리되어 있어요.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!