Swagger token 설정

Geun Nam Park·6일 전

Spring Boot

목록 보기
8/9

개요

프로젝트를 진행하면서 swagger로 API 테스트를 많이 사용한다. JWT(JSON Web Token) 인증이 필요한 엔드포인트는 추가 설정 없이는 401 Unautherized 에러를 반환한다.

{
	"message": "인증이 필요합니다."
}

이번 글에서는 Swagger UI에서 JWT 토큰을 입력하고 모든 API 요청을 자동으로 Authorization 헤더를 포함시키는 방법에 대해 알아보자.


JWT 인증 요청 구조

JWT를 사용하는 API에서는 일반적으로 다음과 같은 형식으로 Access Token 을 전달한다.
Authorization Bearer {ACCESS_TOKEN}

각각의 의미

  • Authorizaion: 인증 정보를 전달하는 HTTP 헤더
  • Bearer: Bearer Token 인증 방식
  • {ACCESS_TOKEN} 로그인 후 서버로부터 발급받은 JWT

Bearer 인증이란
Bearer의 사전적 의미 : 소유자

과거 금융권에서 쓰이던 '무기명 채권(Bearer Bond)'에서 개념이 유래했다.무기명 채권은 증서에 주인 이름이 적혀있지 않는다. 즉, "이 종이를 들고 있는(Bearing) 사람이 곧 주인"이며, 은행은 가져온 사람에게 돈을 지급한다.

이를 프로그래밍 보안에도 가져와, 서버는 토큰을 들고 온 클라이언트가 '진짜 로그인한 그 사람'인지 재차 확인하지 않는다. "이 토큰을 소지한(Bearer) 사람을 권한을 가진 자로 인정한다."는 의미에서 Bearer 토큰이라는 이름이 붙었다.

Bearer 토큰을 사용하기 이전에는 OAuth 1.0 방식을 사용하였다.
OAuth 1.0 방식은 '토큰만 보내면 탈취당했을 때 위험하다'라는 생각에, 요청을 보낼 때마다 HTTP 메서드, URL 주소, 보내는 시간, 토큰 등을 복잡한 암호화 알고리즘으로 섞어 '매번 새로운 디지털 서명(Signature)'을 만들어 같이 보내야 했다. 이를 개발자가 구현하기가 매우 어려웠고 오류도 잦았다.

Bearer은 OAuth 2.0 방식으로, 네트워크 전체를 HTTPS(SSL/TLS)로 암호화하는 것이 표준이 되었다. 네트워크 파이프 자체가 암호화되었으니, 요청마다 서명을 계산할 필요가 없어졌고, 발급받은 Bearer 토큰 문자열을 헤더에 그대로 실어 보내는 것으로 단순화시켰다.


Swagger JWT 설정 코드

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {
	private static final String SECURITY_SCHEME_NAME = "Bearer Authentication";
    
    @Bean
    public OpenAPI openAPI() {
    return new OpenAPI()
    	.info(apiInfo())
        .addSecurityItem(
        	new SecurityRequirement()
            	.addList(SECURITY_SCHEME_NAME)
            )
        	.components(
                new Components()
                    .addSecuritySchemes(
                        SECURITY_SCHEME_NAME,
                        createSecurityScheme()
                )
           );
     }
            
    private Info apiInfo() {
        return new Info()
        	.title("Couplead API")
            .description("Couplead 서비스 API 문서")
            .version("1.0.0");
    }
    
    private SecurityScheme createSecurityScheme() {
    	return new SecurityScheme()
        	.name(SECURITY_SCHEME_NAME)
          	.type(SecurityScheme.Type.HTTP)
            .scheme("bearer") .bearerFormat("JWT")
            .in(SecurityScheme.In.HEADER);
    }
}

설정 코드 설명

SecurityScheme

new SecurityScheme() 
	.type(SecurityScheme.Type.HTTP)
    .scheme("bearer")
    .bearerFormat("JWT")

Swagger에 다음 내용을 알려준다.

  • HTTP 인증 방식을 사용한다.
  • Bearer Token을 사용한다.
  • 토큰 형식은 JWT다.
  • 인증 정보는 HTTP 헤더에 전달한다.

SecurityRequirement

new SecurityRequirement()
	.addList(SECURITY_SCHEME_NAME)

등록한 JWT 인증 방식을 API 문서 전체에 적용한다.
이 설정이 적용되면 Swagger UI 상단에 Authorize 버튼이 표시되고, 각 API 오른쪽에는 자물쇠 아이콘이 나타난다.

주의

.addList(SECURITY_SCHEME_NAME)

.addSecuritySchemes(
        SECURITY_SCHEME_NAME,
        createSecurityScheme()
)

해당 두 부분에서 사용하는 이름을 반드시 같아야 한다.
같지 않을 경우, Swagger가 보안 요구사항과 보안 스키마를 연결하지 못할 수 있다.


Spring Security에서 Swagger 경로 허용

Swagger 설정을 추가했어도, Spring Security에서 Swagger 관련 URL을 차단하기 때문에 SecurityConfig에서 Swagger 경로를 인증 없이 접근할 수 있도록 허용해야 한다.

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
	private final JwtAuthenticationFilter jwtAuthenticationFilter;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private static final String[] SWAGGER_WHITELIST = {
    	"/swagger-ui/**",
        "/swagger-ui.html",
        "/v3/api-docs/**"
    };
    
    @Bean
    public SecurityFilterChain securityFilterChain(
    	HttpSecurity http
    ) throws Exception {
    	http .csrf(csrf -> csrf.disable())
        	.sessionManagement(
            	session -> session.sessionCreationPolicy(
                	SessionCreationPolicy.STATELESS
                )
            )
         	.authorizeHttpRequests(auth -> auth
            	.requestMatchers(SWAGGER_WHITELIST).permitAll()
                .requestMatchers(
                	"/api/auth/login",
                    "/api/auth/signup",
                    "/api/auth/refresh"
                ).permitAll()
                .requestMatchers(
                	HttpMethod.GET,
                    "/uploads/**"
                ).permitAll()
                	.anyRequest()
                    .authenticated()
            )
            .exceptionHandling(exception ->
               	exception.authenticationEntryPoint(
                    jwtAuthenticationEntryPoint
               	)
            )
                
            .addFilterBefore(
                jwtAuthenticationFilter,
                UsernamePasswordAuthenticationFilter.class
            );
        return http.build();
    }
}

이 중에 핵심 설정은 아래 부분이다.

.requestMatchers(
	"/swagger-ui/**",
	"/swagger-ui.html",
	"/v3/api-docs/**"
).permitAll()

swagger-ui/**: Swagger UI 화면과 관련 리소스
/swagger-ui.html: Swagger UI 리다이렉트 경로
/v3/api-docs/**: OpenAPI JSON 문서


인증이 필요없는 API 표시

로그인이나 회원가입 등은 JWT가 없어도 호출할 수 있어야 한다.
Swagger 문서에서도 해당 API에 자물쇠 표시가 되지 않게 하려면 @SecurityRequirements를 사용할 수 있다.

import com.example.couplead.auth.dto.LoginRequest;
import com.example.couplead.auth.dto.TokenResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirements;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class AuthController {
	private final AuthService authService;
    
    @Operation(
    	summary = "로그인",
        description = "이메일과 비밀번호로 로그인하고 JWT를 발급받습니다."
    )
    
    @SecurityRequirements
    @PostMapping("/login")
    public ResponseEntity<TokenResponse> login(
    	@RequestBody LoginRequest request
    ) {
    	TokenResponse response = authService.login(request);
        return ResponseEntity.ok(response);
    }
}

해당 어노테이션은 전역으로 적용된 보안 요구사항을 해당 API에서 제거한다.(Swagger 문서 표시만 해당, 실제 접근 허용 여부는 별도로 설정해야한다.)


특정 API 에서만 JWT 인증 적용

전역으로 인증 적용을 하고싶지 않다면, OpenAPI 설정에서 SecurityRequirement를 제거할 수 있다.

@Bean
public OpenAPI openAPI() {
	return new OpenAPI()
    	.info(apiInfo())
        .components(
        	new Components()
            	.addSecuritySchemes(
                	SECURITY_SCHEME_NAME,
                    createSecurityScheme()
                )
        );
}

Swagger UI에서 JWT 인증 테스트

  1. 로그인 API 실행
    Swagger UI에 접속http://localhost:8080/swagger-ui/index.html하여, 로그인 API를 선택한 뒤 Try it out을 클릭한다.
{
  "email": "test@example.com",
  "password": "password1234!@#$"
}

로그인 정보를 입력 후 Execute를 클릭하면 서버에서 토큰을 반환한다.

{
  "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiJ9..."
}

여기서 swagger 인증에 사용하는 token은 accessToken이다.

  1. Authorize 버튼
    Swagger UI 우측 상단에 Authorize 버튼을 클릭한다.
Bearer Authentication (http, Bearer)
Value:

위와 같이 값을 입력하는 창이 나타난다.

  1. Access Token을 입력
    해당 입력창에는 Bearer 을 제외하고 토큰 값만을 넣는다.
    (scheme("bearer")로 설정했기 때문에 Swagger UI가 요청을 보낼 때 자동으로 Bearer를 붙인다.)

이후에는 인증이 필요한 API는 정상적으로 반환하게 된다.


Swagger 관련 application.yml 설정

필요하다면 Swagger 경로와 정렬 방식을 설정할 수 있다.

springdoc:
  api-docs:
    path: /v3/api-docs

  swagger-ui:
    path: /swagger-ui.html
    operations-sorter: method
    tags-sorter: alpha
    display-request-duration: true

설정 내용은 다음과 같다.

api-docs.path: OpenAPI JSON 문서 경로
swagger-ui.path:Swagger UI 접근 경로
operations-sorter: API를 HTTP Method 기준으로 정렬
tags-sorter: 태그를 알파벳순으로 정렬
display-request-duration: 요청 처리 시간을 화면에 표시


자주 일어나는 문제들

Authorize 버튼 없음
SecuritySchemeSecurityRequirement를 작성하지 않았는지 확인한다.
전역 인증을 적용했다면 두 설정이 모두 필요하다.

혹은

특정 API만 적용한다면 해당 API에 다음 어노테이션이 있어야 한다.
@SecurityRequirement(name = "Bearer Authentication")

Swagger UI 접속 시 401 에러
Spring Security에서 Swagger 경로를 허용했는지 확인한다.

.requestMatchers(
	"/swagger-ui/**",
    "/swagger-ui.html",
    "/v3/api-docs/**"
).permitAll()

API 목록이 보이지 않는다.
/v3/api-docs/**가 차단된 경우일 것으로, 브라우저에서 주소를 확인해본다.
http://localhost:8080/v3/api-docs
해당 주소에서 401 또는 403이 반환된다면 SecurityConfig의 허용 경로를 확인해야 한다.

토큰을 입력했지만 계속 401 발생
다음 항목을 차례대로 확인한다.

Access Token이 만료되지 않았는가?
Refresh Token을 잘못 입력하지 않았는가?
Bearer 문자열을 중복해서 입력하지 않았는가?
JWT 필터가 Authorization 헤더를 읽고 있는가?
JWT 필터가 Spring Security 필터 체인에 등록되어 있는가?
토큰의 서명 검증에 사용하는 Secret Key가 발급 시점과 동일한가?
토큰에서 사용자 정보를 정상적으로 추출하는가?
필터를 제대로 등록했는가?

.addFilterBefore(
	jwtAuthenticationFilter,
    UsernamePasswordAuthenticationFilter.class
)

토큰을 입력했는데, Authrization 헤더가 없다.
SecurityScheme의 타입과 스키마를 확인한다.

.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")

OpenAPI 3에서는 Bearer 인증을 단순한 API Key가 아니라 HTTP Bearer 방식으로 정의하는 것이 표준적인 설정이다.

0개의 댓글