[Spring Boot Tutorial] 13. OpenAPI 3.0를 이용한 REST API 문서 만들기 (Swagger v3)

2020. 6. 18. 11:28Spring/Spring Boot Tutorial

반응형

OAS 3.0

  1. OpenAPI 3.0
    1. OAS 란?
    2. Swagger 란?
    3. springdoc-openapi
  2. 설정
    1. 의존성 라이브러리 추가
    2. 프로퍼티 설정 (operations-sorter, display-query-params-without-oauth2)
    3. security 설정
    4. Swagger Info 설정
  3. Swagger-ui 실행
    1. swagger-ui.html 이동 버튼 생성
    2. /swagger-ui/index.html
    3. Swagger-ui 실행 및 csrf token 문제점
    4. csrf ignoringAntMatchers 설정 추가

1. OpenAPI 3.0

1.1. OAS 란?

OAS(OpenAPI Specification)는 RESTful 웹서비스를 약속된 규칙에 따라 약속된 규칙에 맞게 API 스펙을 json과 yaml 형식으로 표현합니다.

92

※ springdoc.api-docs.path 에설정한 url로 접속하면 위와같이 json형식의 api 스펙을 확인할 수 있습니다.
yaml 파일은 위의 url에서 확장자 .yaml을 붙이면 다운로드 됩니다.(http://localhost:8989/api-docs.yaml)

Spring Boot에서는 springdoc-openapi 라이브러리를 추가하는 것으로 OAS 3를 사용할 수 있는데, 생성한 api에 별도의 설정을 추가할 경우 웹 애플리케이션의 시작과 동시에 자동으로 문서화 합니다.


1.2. Swagger 란?

OpenAPI 에서 빼놓을 수 없는 기능이 바로 Swagger입니다.

Swagger는 위에서 생성된 api-docs 를 기반으로 api를 HTML 문서화 해줍니다.
Swagger는 OpenAPI 스펙을 맞춘 api-docs를 이용하여 html 페이지를 만들어주는 오픈소스 프레임워크로, RESTful API 의 설계 및 문서화에 매우 도움을 줍니다.

91

Swagger-ui 페이지 화면


1.3. springdoc-openapi

springfox 라이브러리는 OpenAPI 3.0을 지원하고 있지 않습니다.

springfox를 사용하고 있던 기존 Spring Boot 애플리케이션에서 OpenAPI 3.0을 지원하기 위해서는 io.springfox 그룹의 'springfox-swagger2, springfox-swagger-ui' 라이브러리를 제거하고 springdoc-openapi dependency를 추가하면 됩니다.

springdoc-openapi 라이브러리는 아래의 항목들을 지원합니다.

  • OpenAPI 3
  • Spring Boot 1, Spring Boot 2
  • JSR-303 (ex. @NotNull, @Min, @Max, @Size)
  • Swagger-ui
  • OAuth 2

2. 설정

2.1. 의존성 라이브러리 추가

pom.xml에 springdoc-openapi 을 추가합니다.

<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-ui</artifactId>
  <version>1.4.1</version>
</dependency>

springdoc-openapi-ui 에는 swagger.core.v3 가 포함되어있습니다.
따라서 springdoc-openapi-ui 라이브러리를 추가한 후 HTML 형식의 API 테스트 페이지인 Swagger-ui 페이지를 이용할 수 있습니다.


2.2. 프로퍼티 설정

springdoc:
  version: '@project.version@'
  api-docs:
    path: /api-docs
  default-consumes-media-type: application/json
  default-produces-media-type: application/json
  swagger-ui:
    operations-sorter: alpha
    tags-sorter: alpha
    path: /swagger-ui.html
    disable-swagger-default-url: true
    display-query-params-without-oauth2: true
  paths-to-match:
  - /api/v1/**
  - /test/**

Spring Boot에서 OpenAPI 3을 이용하여 Swagger 3 를 구성하기 위해 간단한 프로퍼티 설정을 했습니다.
request/response의 Media Type 기본 값을 application/json
api 및 태그 정렬 기준을 알파벳 오름차순으로
그리고 json/yaml로 자동 생성될 OpenAPI 3.0 규격에 맞춘 api 스펙문서 경로(api-docs.path)를 설정했습니다.

더 자세한 정보는 아래를 참고하면 됩니다.


  • springdoc
    • api-docs.path
      • 기본값 : /v3/api-docs
      • spring boot 웹 애플리케이션의 api를 OpenAPI 3을 이용하여 json 형식화 한것의 경로
    • default-consumes-media-type
      • 기본값 : application/json
      • request media type 의 기본 값
    • default-produces-media-type
      • 기본값 : */*
      • response media type 의 기본 값
    • swagger-ui.operations-sorter
      • 기본값 : 컨트롤러 내에서 정의한 api 메서드 순
      • 태그 내 각 api의 정렬 기준
      • alpha(알파벳 오름차순), method(http method 순)
    • swagger-ui.tags-sorter
      • 태그 정렬 기준
    • swagger-ui.path
      • 기본 값 : /swagger-ui.html
      • Swagger HTML 문서 경로
    • swagger-ui.disable-swagger-default-url
      • swagger-ui default url인 petstore html 문서 비활성화 여부
      • v1.4.1 이상 버전부터 지원합니다.
    • swagger-ui.display-query-params-without-oauth2
      • 기본 값 : false
      • json화 된 config파일 대신 파라미터를 이용하여 swagger-ui에 접근하도록 합니다.
      • api-docs(/api-docs) 및 swagger-ui.configUrl(/api-docs/swagger-config)를 두번씩 호출하는 것을 방지합니다.
      • v1.4.1 이상 버전부터 지원합니다.
    • paths-to-match
      • OpenAPI 3 로 문서화할 api path 리스트

※ 더 자세한 설정정보를 알고 싶다면 springdoc-openapi를 참고해주세요.


2.2.1. operations-sorter 설정에 따른 api 출력화면

85

springdoc.swagger-ui.operations-sorter : alpha


86

springdoc.swagger-ui.operations-sorter : method
DELETE → GET → PATCH → POST → PUT 순서


2.2.2. display-query-params-without-oauth2 설정에 따른 리소스 호출 횟수

87

swagger-ui.display-query-params-without-oauth2 : false
/api-docs/api-docs/swagger-config를 2번씩 호출합니다.

88

swagger-ui.display-query-params-without-oauth2 : true
/api-docs/swagger-config 를 호출하지 않으며, 파라미터를 이용하여 swagger-ui html문서를 호출합니다.


2.3. security 설정

@Override
protected void configure(HttpSecurity http) throws Exception {
  http.authorizeRequests()
    .antMatchers("/", "/login", "/join", "/api/v1/**", "/test/**").permitAll()
    .antMatchers("/v/users").hasRole("ADMIN")
    .antMatchers("/v", "/v/**").hasRole("VIEW")
    .antMatchers("/swagger-ui.html", "/swagger-ui/**", "/api-docs", "/api-docs/**").hasRole("VIEW")
    .anyRequest().authenticated()
    .....
    ;
}

demo 웹 애플리케이션에 swagger 페이지 접속에 관한 권한을 설정합니다.
저의 경우 ROLE_VIEW 권한을 가진 사용자만 접근할 수 있도록 .hasRole('VIEW') 설정을 했습니다.


※ Spring Security 설정 더 알고 싶다면 아래의 페이지에서 참고해주세요.
7. JavaConfig 설정으로 Spring Security 커스터마이징
9. JDBC 기반 Spring Security 인증&인가


2.4. Swagger Info 설정

OpenAPI 의 Info 설정을 별도하지 않을 경우 위와 같은 모양으로 Swagger-ui 페이지가 아래와 같이 자동 설정됩니다.

89

Swagger-ui 기본 화면


OpenAPI bean 을 이용하여 Swagger-ui를 구성하는 메시지를 수정해봅시다.

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;

@Component
public class OpenApiConfig {

  @Bean
  public OpenAPI openAPI(@Value("${springdoc.version}") String appVersion) {
    Info info = new Info().title("Demo API").version(appVersion)
            .description("Spring Boot를 이용한 Demo 웹 애플리케이션 API입니다.")
            .termsOfService("http://swagger.io/terms/")
            .contact(new Contact().name("jini").url("https://blog.jiniworld.me/").email("jini@jiniworld.me"))
            .license(new License().name("Apache License Version 2.0").url("http://www.apache.org/licenses/LICENSE-2.0"));

    return new OpenAPI()
            .components(new Components())
            .info(info);
  }

}

90

OpenAPI 빈을 설정한 후의 Swagger-ui 화면입니다.


3. Swagger-ui 실행

3.1. swagger-ui.html 이동 버튼 생성

<header>
  <h1>
    <a th:href="@{/v}"><img th:src="@{/static/img/like.png}" width="50" height="auto" alt="demo page" id="btn_home" /></a>
  </h1>
  <ul>
    <li sec:authorize="isAuthenticated()">
      <span sec:authentication="principal.username"></span> 님 반가워요!
    </li>
    <li><a th:href="@{/swagger-ui.html}" target="_blank"><img th:src="@{/static/img/swagger.png}" width="20" alt="swagger-ui page" id="btn-swagger" /></a></li>
    <li><form id="logoutFrm" th:action="@{/logout}" method="post" style="display: inline-block;">
        <a href="#" onclick="document.getElementById('logoutFrm').submit()" data-toggle="tooltip" data-placement="logout" title="Logout"><i class="fa fa-power-off"></i></a>
      </form></li>
  </ul>
</header>

demo 의 view 화면 header에 swagger-ui 페이지 이동 a 태그를 추가했습니다.

95

화면


3.2. /swagger-ui/index.html

springdoc.swagger-ui.path에 정의한 경로(default: swagger-ui.html)로 접속할 Swagger 관련 설정값을 이용하여 Swagger-ui HTML 페이지로 Redirect 됩니다.

궁극적인 Swagger-ui 도착지 주소는 /swagger-ui/index.html 이지만, 이 페이지는 API 명세를 만들기 위해 api-docs에 대한 파라미터를 필수적으로 입력받아야 생성됩니다.

만일, 파라미터를 설정하지 않을 경우에는 아래와 같은 petstore 페이지가 출력됩니다. 파라미터 미설정시 표현할 defaultUrl 설정을 끄고 싶다면 springdoc.swagger-ui.disable-swagger-default-url 속성을 true로 설정하면 됩니다.

96

query-param 을 url로 표출할 것인지에 대한 설정에 따라 url에서 이용되는 필수 파라미터가 달라집니다.
이에 대한 설정은 springdoc.swagger-ui.display-query-params-without-oauth2 에서 행해집니다.
이 값이 true 일 경우에는 Swagger-ui 를 구성하는데 필요한 설정값들을 파라미터로 표출하고,
false 일 경우에는 configUrl만 받습니다.

  • false
    • /swagger-ui/index.html?configUrl=/api-docs/swagger-config
  • true
    • /swagger-ui/index.html?operationsSorter=method&tagsSorter=alpha&url=/api-docs

프로퍼티 설정 참고


3.3. 실행

GET api를 실행한 결과 화면입니다.
200 status와 ResponseBody가 정상적으로 출력됨을 확인할 수 있네요!

93


이번에는 PATCH api를 실행해보았습니다.
그런데 이게 웬일!?

94

2020-06-18 00:52:41.785 -DEBUG [  http-nio-8989-exec-6] o.s.web.servlet.DispatcherServlet        : "FORWARD" dispatch for PATCH "/err/denied-page", parameters={}
2020-06-18 00:52:41.786 -DEBUG [  http-nio-8989-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to public java.lang.String me.jiniworld.demo.controllers.views.LoginController.accessDenied()
2020-06-18 00:52:41.787 -DEBUG [  http-nio-8989-exec-6] o.s.w.s.v.ContentNegotiatingViewResolver : View remains unresolved given [application/json]
2020-06-18 00:52:41.789 -DEBUG [  http-nio-8989-exec-6] o.s.web.servlet.DispatcherServlet        : Exiting from "FORWARD" dispatch, status 403

console에 출력된 에러 로그

403 Forbidden Error가 나왔습니다. 두둥..
바로 api이용에 관한 권한 문제로 인한 에러 현상인데요.
이는, 이전 시간에 Spring Security에 설정했었던 csrf-token이 설정되어있지 않아 발생되는 문제입니다.

Spring Security 설정 시, 웹 보안 취약점 해결을 위해 csrf token을 활성화 했었습니다.

csrf token을 활성화 시킨 후, postman에서도 이전에 잘 동작하던 PATCH/POST/PUT/DELETE method api가 403 Forbidden 에러를 반환했었고, 이에 대한 해결책으로 header에 X-XSRF-TOKEN를 설정했었습니다.
[Spring Security] postman에서 csrf token 이용하기를 참고해주세요.

Spring Security가 적용되어야하는 Thymeleaf 템플릿에서 이용되는 api외의 모든 api들을 csrf 제외시키도록 설정을 추가하면 됩니다.

7. JavaConfig 설정으로 Spring Security 커스터마이징의 url 접근 권한 설정에서 permitAll 설정했던 api들이 csrf 제외시킬 대상입니다.


3.4. csrf ignoringAntMatchers 설정 추가2021.09.15 수정

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	private final static String[] allowedUrls = {"/", "/login", "/join", "/api/v1/**", "/test/**", "/token"};...

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
		.authorizeRequests()
			.antMatchers(allowedUrls).permitAll()
			.antMatchers("/v/users").hasRole("ADMIN")
			.antMatchers("/v", "/v/**").hasRole("VIEW")
			.antMatchers("/swagger-ui.html", "/swagger-ui/**").hasRole("VIEW")
			.anyRequest().authenticated()
		.and()
			.formLogin().loginPage("/login").defaultSuccessUrl("/v", true)
			.usernameParameter("email").passwordParameter("password")
		.and()
			.logout().invalidateHttpSession(true).deleteCookies("JSESSIONID")
		.and().exceptionHandling().accessDeniedHandler(webAccessDeniedHandler)		
		.and()
			.authenticationProvider(authenticationProvider())
		.csrf()
			.ignoringAntMatchers(allowedUrls)
			.requireCsrfProtectionMatcher(new CsrfRequireMatcher())
			.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
	}

	static class CsrfRequireMatcher implements RequestMatcher {
	    private static final Pattern ALLOWED_METHODS = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$");

	    @Override
	    public boolean matches(HttpServletRequest request) {
	        if (ALLOWED_METHODS.matcher(request.getMethod()).matches())
	        	return false;
	        return true;
	    }
	}
}

csrf() 설정에서 csrf 토큰을 요구하지 않는 antMatchers를 설정하고, 그 외에도 단순 조회 기능을 제공하는 GET, HEAD, TRACE, OPTIONS method의 경우 CORS 처리를 하지않도록 requireCsrfProtectionMatcher 설정을 추가했습니다.

requireCsrfProtectionMatcher 설정을 마친 후 다시 PATCH api를 실행해봅시다.

97

이번에는 204 No Content. 정상적으로 api 실행이 되었음을 확인할 수 있습다.


+++ keyword

  • io.swagger.core.v3 설정하기
  • OpenAPI 3.0 + Swagger
  • Spring Boot에 OAS3 설정하기
반응형
  • 프로필사진
    traeper2021.01.26 14:37

    OpenAPI 관련 삽질중이었는데 도움이 많이 되었습니다.
    감사합니다. :)

  • 프로필사진
    빵더쿠2021.05.11 16:39

    지니얌

    "springfox 라이브러리는 OpenAPI 3.0을 지원하고 있지 않습니다."

    -> 이 코멘트 springfox-boot-starter 를 사용한 3.0.0 버전도 마찬가지로 해당하는 사항일까?

    [GitHub - springfox/springfox: README.md]
    https://github.com/springfox/springfox#springfox

    -> @EnableOpenApi 따위가 보임!

    [Made open api 3.0.0 the default · springfox/springfox-demos | Examples repository]
    https://github.com/springfox/springfox-demos/commit/ee64c326cb45f140ef17c3f0ad620b359a54c302

    -> 단순히 Docket 빈을 생성하지 않으므로 OAS_30 적용되는 예제 소스 커밋 인데,
    OpenAPI 3.0 스펙을 맞춘 api-docs 제작을 지원하고 있는 것 아닌가?


    [Maven Repository: io.springfox » springfox-boot-starter]
    https://mvnrepository.com/artifact/io.springfox/springfox-boot-starter

    -> 이게 최근의 간단한 스타터 방식!

    • 프로필사진
      Favicon of https://blog.jiniworld.me BlogIcon jiniya222021.05.20 16:23 신고

      그렇네요! 코멘트 감사합니다!
      글을 올릴 당시와 2020년 7월에 `springfox-boot-starter` 가 올라왔었네요!!
      해당 부분은 추가해서 다시 올리겠습니다. 좋은 정보 감사해요!

  • 프로필사진
    샘숭22021.05.11 17:14

    2-2-2) display-query-params-without-oauth2 설정에 따른 리소스 호출 횟수

    -> 바로 밑에 첨부한 짤은 어디서 보는 거에요? (제 크롬 브라우저 네트워크 탭에는 이런 탭의 형태가 아닌데...)

    • 프로필사진
      Favicon of https://blog.jiniworld.me BlogIcon jiniya222021.09.15 15:09 신고

      fiddler 4 라는 툴을 이용하여 조회했습니다.
      https://www.telerik.com/fiddler