ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring REST API에 Swagger 3.0 적용하기
    Project 2023. 7. 2. 20:27

     

    Swagger를 적용하면 postman처럼 body에 요청 데이터를 넣어 응답을 테스트 할 수 있다.

    그리고 개발한 REST API들의 명세를 한눈에 파악할 수 있어 프론트가 없는 API만 개발했을 경우에 커뮤니케이션에 용이하다.

     

     

    1. 의존성 추가 후 접속테스트 하기 

    implementation 'io.springfox:springfox-boot-starter:3.0.0'

    만약 swagger2 로 적용하고 싶은 경우에는 해당 의존성을 추가한다

    implementation 'io.springfox:springfox-swagger2:2.9.2'
    implementation 'io.springfox:springfox-swagger-ui:2.9.2'

     

    1-1. /swagger-ui/** 경로로 들어오는 요청 허용하기

    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class AuthenticationConfig extends WebSecurityConfigurerAdapter {
    
    	// 생략
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.cors().and().csrf().disable()
    			.authorizeRequests()
    			.antMatchers("/", "/auth/**", "/api/*/members/register", "/api/*/members/login", "/swagger-ui/**").permitAll()
    			.antMatchers("/api/**").authenticated()
    		// 생략
    		;
    	}
        
    }

    Spring security를 사용하는 경우 구현한 configure 메소드에 /swagger-ui/** 경로를 추가하여 허용하도록 설정한다. spring security를 적용하지 않았으면 해당 단계는 넘어가도 된다. 

     

     

    1-2. http://localhost:8080/swagger-ui/ 경로로 접속 확인

     

    끝에 슬래쉬 까지 붙여야한다. 컨트롤러를 클릭하면 엔드포인트 별로 응답을 테스트 할 수 있다. 

    만약 swagger 2.x의 경우 http://localhost:8080/swagger-ui.html로 접속 경로가 다르니 주의하자.

     

     

     

    2. Swagger 페이지에 Authorize 버튼 활성화 하기

    현재 구현한 register, login기능을 제외한 기능들은 요청시 JWT 토큰 값을 넣어 함께 요청해야한다. 

    postman으로 테스트 할 때는 login 요청 시 응답으로 받은 토큰 값을 직접 복사하여 Authorization 헤더에 Bearer token값을 넣어줬었다.

    Swagger 페이지에서도 Bearer {Token} 값을 Authorization 헤더에 넣어보자.

    먼저, Authorization 버튼을 활성화 시켜야한다. 

     

     

    2-1. Authorize 추가를 위한 Config 작성

    package com.postype.sns.configuration;
    
    import java.util.List;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import springfox.documentation.builders.ApiInfoBuilder;
    import springfox.documentation.builders.PathSelectors;
    import springfox.documentation.builders.RequestHandlerSelectors;
    import springfox.documentation.service.ApiInfo;
    import springfox.documentation.service.ApiKey;
    import springfox.documentation.service.AuthorizationScope;
    import springfox.documentation.service.Contact;
    import springfox.documentation.service.SecurityReference;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spi.service.contexts.SecurityContext;
    import springfox.documentation.spring.web.plugins.Docket;
    
    @Configuration
    public class SwaggerConfig {
    
    	@Bean
    	public Docket testDocket() {
    		return new Docket(DocumentationType.OAS_30)// OpenAPi spec 3.0으로 설정
    			.useDefaultResponseMessages(false)
    			.securityContexts(List.of(this.securityContext())) // SecurityContext 설정
    			.securitySchemes(List.of(this.apiKey())) // ApiKey 설정
    			.apiInfo(this.getApiInfo())// Api 정보 저장 
    			.select()
                //컨트롤러가 있는 패키지 경로 설정
    			.apis(RequestHandlerSelectors.basePackage("com.postype.sns.application.controller"))
                .paths(PathSelectors.ant("/api/**"))
    			.build();
    	}
    
    	private ApiInfo getApiInfo() {
    		return new ApiInfoBuilder()
    			.title("API")
    			.description("postype API")
    			.contact(new Contact("yooyouny", "https://yooyouny.tistory.com/", "jhjin2u@gmail.com"))
    			.version("1.0")
    			.build();
    	}
    
    	// JWT SecurityContext 구성
    	private SecurityContext securityContext() {
    		return SecurityContext.builder()
    			.securityReferences(defaultAuth())
    			.build();
    	}
    
    	private List<SecurityReference> defaultAuth() {
    		AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
    		AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
    		authorizationScopes[0] = authorizationScope;
    		return List.of(new SecurityReference("Authorization", authorizationScopes));
    	}
    
    	// ApiKey 정의
    	private ApiKey apiKey() {
    		return new ApiKey("Authorization", "Authorization", "header");
    	}
    }

     

    현재 내 패키지 경로는 아래와 같다. 

    com.postype.sns.application 안에 있는 4개의 컨트롤러를 모두 swagger 페이지에서 테스트 하도록 디파인 하고싶기 때문에 basebackage 경로로 지정한다.

     

    그리고 모든 컨트롤러는 클래스 단에 @RequestMapping("/api/~~~") 경로로 설정되어있기 때문에 paths도 맞춰서 작성해줬다.

    paths의 슬래쉬를 잘못넣었거나 basepackage 이름에 오타가 있는 등 경로가 잘못설정되면 no operations defined in spec! 과 같은 에러가 발생하니 주의해야한다. 해당 에러가 발생했다면 패키지 명과 path를 확인하길 바란다.

     

    ApiKey 부분도 나와 같이 Authorization 헤더 안에 Authorization 값을 "Bearer {token}"로 처리하는 경우 위와 같이 작성해야만 한다. Bearer 다음에 띄어쓰기가 있는 것을 주의하자. 

     

     

    빌드 후 재실행 하면 OAS3 마크 활성화, Authorize 버튼 활성화, 원하는 컨트롤러들이 디파인 되어있는 것을 확인할 수 있다. 

     

     

    2-2 Authorize 적용 시 주의점 

    /login 테스트 시 정상적인 응답이라면 token값을 얻을텐데 이 토큰값만 그대로 Authorize value에 넣으면 안된다. 

     

    토큰값 앞에 "Bearer "문자열을 직접 추가해줘야 한다. 이게 귀찮아서 value에 자동으로 prefix를 추가하는 기능에 대해 열심히 찾아보았지만 해결하지 못했다.. 방법이 있다면 댓글 부탁드린다...

     

     

    3. @Operation으로 엔드포인트 별 명세 작성하기 

    @Operation(summary = "포스트 발행", description = "로그인한 멤버가 title, body, price를 작성하여 포스트를 발행합니다.")
    @PostMapping
    public Response<Void> create(@RequestBody PostCreateRequest request, @AuthenticationPrincipal MemberDto memberDto){
    	createPostUseCase.execute(request.getTitle(), request.getBody(), memberDto, request.getPrice());
    	return Response.success();
    }

    swagger.v3이 제공하는 @Operation을 활용하여 각 엔드포인트 별 명세를 작성할 수 있다. 

     

    가독성이 좋아졌다. 현재 Authorize로 로그인을 한 상태라 자물쇠가 풀려있다. @ApiParam과 @ApiResponse로 파라미터와 응답에도 명세 추가가 가능하나 필자는 엔트포인트에만 설명을 작성했다. 

     

     

    4. Swagger페이지에서 특정 파라미터 숨기기

     

    해당 파라미터들은 memberDto에 정의된 파라미터들로 Swagger페이지에서는 보여질 필요가 없는 필드들이다.

     

    나는 JWT token을 만들때 claims에 memberId를 넣어놓고 필터에서 memberId로 member를 찾아 UsernamePasswordAuthenticationToken 값을 생성한다. 

     

    이렇게 필터에서 한번의 멤버 조회를 하기 때문에 굳이 컨트롤러에서 login한 유저를 한번 더 조회할 필요가 없다. 

    따라서 로그인 한 유저만 접근할 수 있는 엔드포인트일 경우 해당 파라미터에 @AuthenticationPrincipal 를 붙여 principal에 저장된 member를 dto에 자동으로 매핑시킨다. 때문에 굳이 Swagger페이지에서 파라미터로 입력받지 않아도 된다. 

     

    특정 파라미터들을 제외시키려면 해당 파라미터 앞에 @ApiIgnore를 붙이면 된다. 

     

    @Operation(summary = "포스트 발행", description = "로그인한 멤버가 title, body, price를 작성하여 포스트를 발행합니다.")
    @PostMapping
    public Response<Void> create(@RequestBody PostCreateRequest request, @ApiIgnore @AuthenticationPrincipal MemberDto memberDto){
    	createPostUseCase.execute(request.getTitle(), request.getBody(), memberDto, request.getPrice());
    	return Response.success();
    }

     

    파라미터로 Pageable이 들어오는 경우 @PageableDefault 등을 활용해 파라미터로 굳이 받지 않아도 되는 경우에도 해당 어노테이션을 붙이면 된다.

     

    불필요한 파라미터는 사라지고 입력받을 PostCreateRequest 필드들만 보이는 상태다. 

     

     

     

Designed by Tistory.