swagger 란 Restful API 의 문서화, 테스트 도구인데, 시각적으로 테스트할 수 있는 UI 를 제공하고 있기 때문에아주 유용하지. swagger 가 적용되면 대충 이런 모습의 UI 화면을 사용할 수가 있어.

이번 포스트에서는 API 테스트를 목적으로 diary 프로젝트에 swagger 를 연동해보려고 해.
swagger 와 연동하기 위해서는 build.gradle 에 dependency 를 추가해 주어야 해.
// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
swagger UI 화면은 http://{ipaddress}:{port}/swagger-ui.html 주소로 이동하면 확인할 수 있어. (자동으로 http://{ipaddress}:{port}/swagger-ui/index.html 파일로 redirect 가 되더군)
그런데, diary 프로젝트는 Spring Security 를 사용하고 있기 때문에 위 주소로 이동하면 /login 화면으로 redirect 되어버리니까, SecurityConfig 에서 swagger 관련 URL 을 허용하도록 추가해 주어야 해.
SecurityConfig.java
...
@Configuration
@EnableWebSecurity
public class SecurityConfig {
...
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/process_login"
, "/signup"
, "/swagger-ui.html"
, "/swagger-ui/**"
).permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/process_login")
.usernameParameter("email")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login")
.invalidateHttpSession(true)
)
.build();
}
...
}
여기까지만 진행해놓고 http://localhost:8080/swagger-ui.html 주소로 이동하면 이 글 맨 앞에서 본 샘플 페이지를 볼 수가 있어. 아직 swagger 관련해서 아무런 설정도 하지 않았기 때문이지.
그런데 놀라운 일 한가지를 보여줄께. SecurityConfig 클래스의 filterChain 에서 /v3/api-docs/** 항목을 허용 목록에 추가해볼께.
SecurityConfig.java
...
@Configuration
@EnableWebSecurity
public class SecurityConfig {
...
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/process_login"
, "/signup"
, "/swagger-ui.html"
, "/swagger-ui/**"
, "/v3/api-docs/**"
).permitAll()
.anyRequest().authenticated()
)
...
}
이 상태에서 실행해서 http://localhost:8080/swagger-ui/index.html 주소로 이동해보면 내가 작성한 컨트롤러 클래스중에서 @RestController 애노테이션이 붙어있는 DiaryController 클래스에 대한 API 목록이 따악 뜨는거야.

이제 Swagger UI 를 이용해서 API 를 테스트해볼 수가 있을 것 같네. 테스트를 한번 해볼까? 위 그림에서 GET method 로 작성된 /diary/{id} 항목을 클릭하면 테스트 데이터를 입력할 수 있는 UI 가 펼쳐지지.

오른쪽의 Try it out 버튼을 클릭하면 입력 UI 가 활성화가 돼. id 항목란에 1 이라는 값을 입력하고 아래의 Execute 버튼을 클릭해볼께.

아래쪽에 이 호출에 대한 응답 결과가 표시되는데, Code 는 200(성공), Response body 내용으로는 로그인폼 화면의 소스가 표시되고 있지. Spring Security 를 적용했기 때문에 로그인되지 않은 상태에서 redirect 되는 로그인폼의 소스가 리턴된 것이지.
그렇다면 swagger UI 에서 로그인이 요구되는 API 를 테스트하기 위해서는 로그인 과정을 끼워넣어야 하는데, 이 과정을 끼워넣으려면 어떻게 해야 할까?
SpringSecurity 설정을 조금 변경을 해야 할 필요가 있어.
SpringSecurity.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/process_login"
, "/signup"
, "/swagger-ui.html"
, "/swagger-ui/**"
, "/v3/api-docs/**"
).permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/process_login")
.usernameParameter("email")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login")
.invalidateHttpSession(true)
)
.httpBasic(Customizer.withDefaults())// swagger 에서는 필수로 기록해야 한다.
.build();
}
...
}
Spring Security 가 제공하는 여러가지 인증 방식중에서 폼 로그인 방식은 httpBasic 방식과 동일한 개념이야. formLogin 메소드를 호출한 것은 로그인 폼을 보여줄지에 대한 설정인 것이고, httpBasic 메소드 호출은 생략된 것이라고 보면 되는거지.
잠깐 Postman 을 실행시켜서 Auth 탭의 구성 요소를 볼까?

Type 항목의 드롭다운 버튼을 클릭하면, 여러가지 항목들이 나오는데, 그 중 Basic Auth 가 있지. 이걸 선택하면 로그인 폼에 입력하는 값인 Username 과 Password 를 입력할 수 있는 양식이 나와.

여기에 로그인용 데이터를 채우고 API 를 테스트하게 되면 로그인 과정을 거친 후에 요청이 전송되게 되지.
이제 프로그램을 빌드해서 실행시키고 swagger UI 를 호출한 다음에 GET method 인 /diary/all API 를 테스트해보자.

Execute 버튼을 클릭하면 웹브라우저에 로그인 데이터를 입력받기 위한 UI 가 나와.

여기에 로그인 데이터를 입력하고 로그인을 하게 되면 로그인된 상태에서의 API 호출 결과가 표시되지.

일단 로그인 과정을 한번 거치면 다른 API 호출시에도 로그인한 정보를 계속 사용하게 돼.

그런데, PUT, POST, DELETE method API 는 정상적인 로그인정보를 입력해도 API 호출은 되지 않고, 계속 로그인 폼을 띄우는 이상한 행동을 하네.
이유는 csrf 토큰이 처리되지 않기 때문인데, SpringSecurity 클래스에서 이것과 관련된 속성을 아래와 같이 추가했어.
...
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.csrf((csrf) -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/process_login"
, "/signup"
, "/swagger-ui.html"
, "/swagger-ui/**"
, "/v3/api-docs/**"
).permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/process_login")
.usernameParameter("email")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login")
.invalidateHttpSession(true)
)
.httpBasic(Customizer.withDefaults())// swagger 에서는 필수로 기록해야 한다.
.build();
}
...
그리고 swagger 에서 csrf 토큰이 사용가능하도록 설정을 추가해 주어야 해.
application.yml
springdoc:
swagger-ui:
csrf:
enabled: true
이 상태에서 프로그램을 실행시켜서 POST method API 를 테스트해보면 아래 그림에 표시한 것처럼 X-XSRF-TOKEN 이라는 이름으로 csrf 토큰값이 헤더에 포함되어 전달되지.

DELETE method API 도 로그인처리 후에 잘 실행이 되고 있는데, PUT method 는 아래와 같이 오류가 발생하네.

그런데, 고유값 26 의 데이터는 성공적으로 변경되었어. 즉, 위 오류는 PUT method 핸들러에서 redirect 를 하고 있는데, 이것에 대한 처리가 불가능해서 발생하는 오류야.
DiaryController.java
...
// Update
@PutMapping(value = "/diary/{id}")
public RedirectView UpdateDiary(@PathVariable("id") Integer id, String diary_date, String diary_content) {
System.out.println("id=" + id + ", date=" + diary_date + ", content=" + diary_content);
diaryService.UpdateDiary(id, diary_date, diary_content);
return new RedirectView("/");
}
...
일기내용을 표시한 화면에서 내용을 수정한 후에 Home 화면으로 이동시키기 위해서 작성한 redirect 로직이 GET 방식으로 전달되다보니 생기는 오류인데, 이 방법을 해결해보려고 많은 검색을 했음에도 불구하고 마땅한 해결방법을 찾질 못했네.
일단 PUT method 로 데이터를 update 하는 로직이 정상적으로 수행되고 있으니 그냥 놔둘께.