메서드 기반 인증 관리와 uri

Regular Kim·2025년 6월 6일
0

Reelvy

목록 보기
7/11

기존에는 /v1/{domain}/{action} 형식의 uri를 사용했다. api 서버를 제작 중이므로 메서드 기반으로 인증과정을 처리하기로 결정했다. api를 만들 때마다 securityConfig 에서 해당 api 를 public 으로 열어두는 과정이 귀찮았기 때문이다. 여튼 도입을 했지만 불편한 부분이 있었다. 바로 api가 인증이 필요한 api인지, 아니면 인증이 필요없는지 기억하기가 어려웠다. 그래서 리팩토링을 진행했다.

SecurityConfig

개선 전

개선 전 구조 : /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 구조 개선과 와일드카드(*) 사용으로 로직이 단순해졌다.

VideoController

개선 전

  
@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 에너테이션으로 표시되어있다.

개선 후

인증 필요한 api 컨트롤러

@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 에너테이션을 적용해서 이후 메서드 모두 인증과정이 필요하도록 했다.

인증 불필요한 api 컨트롤러

@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들이 인증 과정이 필요 없음을 보여준다.

@User

@Target({ElementType.METHOD, ElementType.TYPE})  
@Retention(RetentionPolicy.RUNTIME)  
@PreAuthorize("hasRole('USER')")  
public @interface User {  
}

해당 에너테이션은 어려운 구조가 아니다. 스프링 시큐리티의 @PreAuthorize 에너테이션을 한 번 래핑하는 에너테이션일 뿐이다.

profile
What doesn't kill you, makes you stronger

0개의 댓글