Spring Security의 권한 처리 방식 비교 분석

1. 들어가며 

간단한 아웃소싱 팀 프로젝트를 진행하며 처음으로 인증/인가 역할을 맡게 되었습니다. 7일이라는 짧은 프로젝트였기에 굳이 Spring Security를 사용해야 하는가? 에 대한 고민이 있었습니다.

그럼에도 불구하고 Spring Security를 택하게 된 이유는  Security의 권한 처리 방식이 JWT만 사용한 방식과 어떤 부분이 다른지 알고 싶은 호기심과 근본적인 처리 과정을 이해하고 싶은 마음에서 출발하게 되었습니다.

이번 포스트에서 비 Spring Security 방식에서 Spring Security를 도입 후 코드 변화를 통해 권한 처리 방식의 차이점을 이해하는 것입니다.


2. 핵심 객체의 변화

2.1 기존 : Request 객체 활용

@Override
protected void doFilterInternal(
HttpServletRequest request, 
HttpServletResponse response, 
FilterChain filterChain
) throws ServletException, IOException {

    // 1. 토큰 추출
    String token = extractBearerToken(request);

    // 2. JWT NULL & 만료기간 검증
    if (token != null && jwtUtil.validateToken(token)) {

        // 2.1 사용자 정보 추출
        Long userId = jwtUtil.extractUserId(token);
        String role = jwtUtil.extractRole(token);


    request.setAttribute("userId", userId);
    request.setAttribute("role", role);

    filterChain.doFilter(request, response);
}

기존에는 HttpServletRequest 객체를 활용하여 추출한 사용자의 정보(userId)를 request 객체에 저장하였습니다. 이 객체를 사용하는 주된 이유는 데이터를 컨트롤러 계층에 전달하는 데 있습니다. 즉 객체 자체가 인가(Authorization)나 인증(Authentication)에 대한 특별한 기능이나 책임을 수행하지 않습니다.

2.2 변화 : Authentication 객체를 사용

@Override
       protected void doFilterInternal(
       HttpServletRequest request, 
       HttpServletResponse response, 
       FilterChain filterChain
       )
    throws ServletException, IOException {

          // 1. 토큰 추출
          String token = extractBearerToken(request);

          // 2. JWT NULL & 만료기간 검증
          if (token != null && jwtUtil.validateToken(token)) {

             // 2.1 사용자 정보 추출
             Long userId = jwtUtil.extractUserId(token);
             String role = jwtUtil.extractRole(token);

             // 2.2 Authentication 객체 생성 (Spring Security에서 사용되는 인증 객체)
             Authentication auth = new UsernamePasswordAuthenticationToken(
                userId,
                null, // 자격 증명. JWT 인증에서는 사용하지 않음
                List.of(new SimpleGrantedAuthority("ROLE_" + role)) // 사용자 권한 정보
             );

Spring Security는 HttpServletRequest 객체를 통해 넘어온 사용자의 정보를 추출하여 Request에 저장하는 기존 방식과 달리 Authentication 객체에 저장합니다.

이 객체는 보안을 위한 필수적인 데이터(ID 또는 고유 식별자, 사용자 권한)를 저장하는 것이 기본이지만, 필요에 따라서 부가 정보도 포함시킬 수 있습니다.

2.3 왜 Authentication에 정보를 담는가?

@PostMapping
    public ResponseEntity<ApiResponse<StoreCreateResponseDto>> createStore(
        HttpServletRequest request
        @Valid @RequestBody StoreCreateRequestWithUserDto requestDto
    ) {

        Long id = (Long) request.getAttribute("userId");
        User user = userService.getUserById(id)

        if (user.getRole() != UserRoleEnum.OWNER) {
            throw new UnauthorizedException("사장님만 가게를 등록할 수 있습니다.");
        }

        // StoreService의 createStore 메서드 호출
            StoreCreateResponseDto responseDto = storeService.createStore(
            requestDto.toStoreCreateRequestDto(), id
        );

        ApiResponse<StoreCreateResponseDto> response = ApiResponse.success(
            "가게가 성공적으로 등록되었습니다.",
            responseDto
        );

        return ResponseEntity
            .created(URI.create("/api/stores/" + responseDto.storeId()))
            .body(response);
    }

위 코드는 이번 팀 프로젝트 중 권한 검증 방식이 정해지지 않은 상태에서 구현된 것입니다. 위에서 설명했다시피 Request 객체는 인증/인가에 대한 특별한 기능이나 책임을 수행하지 않기 때문에 사용자 정보를 조회하고, 권한 검증을 위한 별도의 검증 로직을 작성해줘야 합니다.

물론 AOP 개념을 적용해서 책임 분리가 가능하지만, 보안 수준이나 유지보수성 측면에서 Spring Security에 비해 상당히 제한적입니다.

Authentication은 Spring Security가 제공하는 핵심 도구로 사용자의 인증 상태와 권한 정보를 표현하는 데이터 구조이며, 객체 자체가 인증과 권한 검증을 수행합니다.


3. Security Context

// 2.3 SecurityContext에 인증 객체 저장
SecurityContextHolder.getContext().setAuthentication(auth);

생성된 Authentication 객체는 Security Context에 저장됩니다. Security Context는 Spring Security에서 인증 상태를 관리하기 위한 전담 저장소로, Authentication 객체를 요청 범위에서 안전하게 관리합니다.

Spring Context와 역할은 다르지만, 관리하는 대상과 중앙화된 관리를 수행한다는 측면에서 유사하다고 볼 수 있습니다.


4. 권한 설정 방식의 변화

4.1 securityFilterChain

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf(csrf -> csrf.disable()) // CSRF 설정 비활성화
       .httpBasic(basic -> basic.disable())
       .formLogin(form -> form.disable())
       .exceptionHandling(exceptions -> exceptions
          .authenticationEntryPoint(new AuthenticationEntryPoint()) // 인증 실패 처리
          .accessDeniedHandler(new JwtAccessDeniedHandler()) // 권한 부족 처리
       )
       .authorizeHttpRequests(auth -> auth
          .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
          .requestMatchers("/users/login", "/users/signup").permitAll()
          .requestMatchers("/api/**").hasAnyRole("CUSTOMER", "OWNER")
          .anyRequest().authenticated() // // 나머지 요청은 인증 필요
       )
       .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

    return http.build(); // HttpSecurity 객체를 SecurityFilterChain으로 변환
}

다음 코드는 Spring Security를 사용하면서 추가된 SecurityConfig 클래스의 securityFilterChain입니다. Spring Security를 사용하면 권한 검증 방식이 이렇게 중앙화되고 구조화됩니다.

모든 HTTP 요청이 Spring Security 필터 체인을 거치며, requestMatchers()에 명시된 규칙에 따라 매칭되는 요청에 대해 권한 검증이 수행됩니다. 매칭되지 않는 요청은 anyRequest()와 같은 기본 규칙에 따라 처리됩니다.

4.2 모든 요청이 필터 체인을 통과해야 한다면 비효율적이지 않은가?

모든 요청이 필터 체인을 통과하지만 SecurityFilterChain은 규칙에 맞는 요청과 맞지 않는 요청을 빠르게 매칭해서 컨트롤러로 흘려보내기 때문에 리소스가 효율적으로 사용되는 구조에 가깝습니다.


5. 검증 방식의 변화

@PostMapping
    @PreAuthorize("hasRole('OWNER')") // OWNER 권한을 가진 사용자만 접근 가능
    @Operation(summary = "가게 생성", description = "사장님(OWNER) 권한을 가진 사용자만 가게를 생성할 수 있습니다.")
    public ResponseEntity<ApiResponse<StoreCreateResponseDto>> createStore(
        @AuthenticationPrincipal Long userId,
        @Valid @RequestBody StoreCreateRequestDto requestDto
    ) {
        StoreCreateResponseDto responseDto = storeService.createStore(requestDto, userId);
        return ResponseEntity
            .created(URI.create("/api/stores/" + responseDto.storeId()))
            .body(ApiResponse.success("가게가 성공적으로 등록되었습니다.", responseDto));
    }

위 코드는 컨트롤러에 Spring Security를 적용한 모습입니다. @PreAuthorize 어노테이션은 Authentication 객체에 저장된 사용자의 권한 정보를 기반으로, 해당 메서드에 접근 가능한 사용자를 별도의 로직없이 제한합니다.

이 검증은 필터 체인을 통과한 후, 컨트롤러 메서드에 도달하기 전에 권한 검증이 이루어지므로 불필요한 비즈니스 로직 수행을 방지합니다.


6. 마치며

Spring Security를 써보기 전에는 어려운 개념이라 생각했고, 과연 이 프로젝트 기간 안에 이해하고 적용할 수 있을까? 막연한 걱정이 있었습니다. GPT가 러닝 구간이 가파르다고 겁을 주기도 했었구요.

Spring Security 공식 문서에 기본 개념과 사용 방법이 체계적으로 정리되있어서 프로젝트에 적용하는데 큰 어려움은 없었던 것 같습니다. 실제로 변화되거나 추가되는 코드도 그리 많지 않았습니다.

앞으로의 목표가 있다면 필터 체인 처리 속도와 성능 병목이 발생할 수 있는 부분을 분석해보는 것입니다.


  1. https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html
  2. 팀 프로젝트: 딜리버리 앱 GibHub 링크