기존에는 /v1/{domain}/{action}
형식의 uri를 사용했다. api 서버를 제작 중이므로 메서드 기반으로 인증과정을 처리하기로 결정했다. api를 만들 때마다 securityConfig 에서 해당 api 를 public 으로 열어두는 과정이 귀찮았기 때문이다. 여튼 도입을 했지만 불편한 부분이 있었다. 바로 api가 인증이 필요한 api인지, 아니면 인증이 필요없는지 기억하기가 어려웠다. 그래서 리팩토링을 진행했다.
개선 전 구조 : /v1/{domain}/{actoin}
기존에는 유저 도메인에서 사용할 수 있는 api들이 모두 이런식으로 정의되어 있었다. SecurityConfig의 SecurityFilterChain 설정이 쉽지 않았다. 어떤 api를 permit 설정해야하는지 매번 확인해야만 했다.
http.authorizeHttpRequests(auth ->
auth
.requestMatchers(
new AntPathRequestMatcher("/favicon.ico"),
new AntPathRequestMatcher("/error"),
new AntPathRequestMatcher("/h2-console/**"),
new AntPathRequestMatcher("/docs/**"),
new AntPathRequestMatcher("/app/uploads/**"),
new AntPathRequestMatcher("/images/**")
).permitAll()
.requestMatchers(
new AntPathRequestMatcher("/v1/users/**"),
new AntPathRequestMatcher("/v1/videos/**")
).permitAll()
.requestMatchers(HttpMethod.GET, "/v1/comments/**").permitAll()
.requestMatchers("/v1/comments/**").authenticated()
.anyRequest().authenticated()
);
게다가 comment 도메인의 api는 GET 요청 전체는 permit 이면서 다른 요청은 인증을 받아야한다는 설정이 되어있다.
통일성 없는 모습이 보기 싫어 개선하기로 했다.
개선 후 구조 : /v1/{domain}/{permitted?}/{action}
http.authorizeHttpRequests(auth ->
auth
.requestMatchers(
new AntPathRequestMatcher("/favicon.ico"),
new AntPathRequestMatcher("/error"),
new AntPathRequestMatcher("/h2-console/**"),
new AntPathRequestMatcher("/docs/**"),
new AntPathRequestMatcher("/app/uploads/**"),
new AntPathRequestMatcher("/images/**")
).permitAll()
.requestMatchers(
new AntPathRequestMatcher("/v1/*/public/**")
).permitAll()
.anyRequest().authenticated()
);
특정 도메인의 api 주소가 public 이라면 (ex : /v1/users/public/login
) 인증을 거치지 않도록 개선 되었다. 기존에는 도메인마다, api 동작마다 인증이 필요한지 지정해 줘야 했지만 uri 구조 개선과 와일드카드(*
) 사용으로 로직이 단순해졌다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/videos")
public class VideoController {
private final VideoService videoService;
@GetMapping
public ResponseEntity<List<VideoResponse>> getHomeVideos() {
return ResponseEntity.ok(videoService.getHomeVideos());
}
@GetMapping("/{videoId}/info")
public ResponseEntity<VideoResponse> getVideo(@PathVariable Long videoId,
@AuthenticationPrincipal UserDetails userDetails) {
return ResponseEntity.ok(videoService.getVideo(videoId, userDetails));
}
@User
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<VideoResponse> uploadVideo(@ModelAttribute @Valid VideoUploadRequest videoUploadRequest,
@AuthenticationPrincipal UserDetails userDetails) {
return ResponseEntity.status(HttpStatus.CREATED).body(videoService.uploadVideo(videoUploadRequest, userDetails));
}
@GetMapping(value = "/{videoId}/stream")
public ResponseEntity<StreamingResponseBody> getVideoStream(@PathVariable Long videoId,
HttpServletRequest request) {
VideoStreamingResponse videoStreamingResponse = videoService.stream(videoId, request);
return ResponseEntity.status(videoStreamingResponse.getStatus()).headers(videoStreamingResponse.getHeaders()).body(videoStreamingResponse.getStreamingResponseBody());
}
@User
@PatchMapping("/{videoId}/info")
public ResponseEntity<VideoResponse> updateVideo(@PathVariable Long videoId,
@Valid @RequestBody VideoUpdateRequest videoUpdateRequestDto,
@AuthenticationPrincipal UserDetails userDetails) {
return ResponseEntity.ok(videoService.updateVideoInfo(videoId, videoUpdateRequestDto, userDetails));
}
@User
@PatchMapping("/{videoId}/status")
public ResponseEntity<VideoResponse> changeVideoStatus(@PathVariable Long videoId,
@Valid @RequestBody VideoStatusChangeRequest videoStatusChangeRequest,
@AuthenticationPrincipal UserDetails userDetails) {
return ResponseEntity.ok(videoService.changeVideoStatus(videoId, videoStatusChangeRequest, userDetails));
}
@User
@PatchMapping("/status")
public ResponseEntity<Void> changeVideosStatus(@RequestBody VideosStatusChangeRequest videosStatusChangeRequest,
@AuthenticationPrincipal UserDetails userDetails) {
videoService.changeVideosStatus(videosStatusChangeRequest, userDetails);
return ResponseEntity.ok().build();
}
@GetMapping("/{username}")
public ResponseEntity<List<VideoResponse>> getVideosOf(@PathVariable String username) {
return ResponseEntity.ok(videoService.getVideosOf(username));
}
}
개선 전에는 인증이 필요한 api와 인증이 필요없는 api가 뒤섞인 구조였다. 인증이 필요한 api라면 @User
에너테이션으로 표시되어있다.
@User
@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/videos")
public class VideoPermittedController {
private final VideoService videoService;
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<VideoResponse> uploadVideo(@ModelAttribute @Valid VideoUploadRequest videoUploadRequest,
@AuthenticationPrincipal UserDetails userDetails) {
return ResponseEntity.status(HttpStatus.CREATED).body(videoService.uploadVideo(videoUploadRequest, userDetails));
}
@PatchMapping("/{videoId}/info")
public ResponseEntity<VideoResponse> updateVideo(@PathVariable Long videoId,
@Valid @RequestBody VideoUpdateRequest videoUpdateRequestDto,
@AuthenticationPrincipal UserDetails userDetails) {
return ResponseEntity.ok(videoService.updateVideoInfo(videoId, videoUpdateRequestDto, userDetails));
}
@PatchMapping("/{videoId}/status")
public ResponseEntity<VideoResponse> changeVideoStatus(@PathVariable Long videoId,
@Valid @RequestBody VideoStatusChangeRequest videoStatusChangeRequest,
@AuthenticationPrincipal UserDetails userDetails) {
return ResponseEntity.ok(videoService.changeVideoStatus(videoId, videoStatusChangeRequest, userDetails));
}
@PatchMapping("/status")
public ResponseEntity<Void> changeVideosStatus(@RequestBody VideosStatusChangeRequest videosStatusChangeRequest,
@AuthenticationPrincipal UserDetails userDetails) {
videoService.changeVideosStatus(videosStatusChangeRequest, userDetails);
return ResponseEntity.ok().build();
}
}
인증이 필요한 api만 따로 모아둔 컨트롤러를 새로 만들었다. 그리고 메서드 선언부에 @User
에너테이션을 적용해서 이후 메서드 모두 인증과정이 필요하도록 했다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/videos/public")
public class VideoPublicController {
private final VideoService videoService;
@GetMapping
public ResponseEntity<List<VideoResponse>> getHomeVideos() {
return ResponseEntity.ok(videoService.getHomeVideos());
}
@GetMapping("/{videoId}/info")
public ResponseEntity<VideoResponse> getVideo(@PathVariable Long videoId,
@AuthenticationPrincipal UserDetails userDetails) {
return ResponseEntity.ok(videoService.getVideo(videoId, userDetails));
}
@GetMapping(value = "/{videoId}/stream")
public ResponseEntity<StreamingResponseBody> getVideoStream(@PathVariable Long videoId,
HttpServletRequest request) {
VideoStreamingResponse videoStreamingResponse = videoService.stream(videoId, request);
return ResponseEntity.status(videoStreamingResponse.getStatus()).headers(videoStreamingResponse.getHeaders()).body(videoStreamingResponse.getStreamingResponseBody());
}
@GetMapping("/{username}")
public ResponseEntity<List<VideoResponse>> getVideosOf(@PathVariable String username) {
return ResponseEntity.ok(videoService.getVideosOf(username));
}
}
인증이 필요 없는 api 모음 컨트롤러이다. 최상단 @RequestMapping 에서 public 이라고 표시하여 작성된 api들이 인증 과정이 필요 없음을 보여준다.
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('USER')")
public @interface User {
}
해당 에너테이션은 어려운 구조가 아니다. 스프링 시큐리티의 @PreAuthorize
에너테이션을 한 번 래핑하는 에너테이션일 뿐이다.