이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • 인증된 사용자 정보는 어떤 방식으로 URL 접근 규칙과 비교되는가?
  • authorizeHttpRequests, requestMatchers, hasRole, hasAuthority를 읽는 기준은 무엇인가?
  • 401과 403, role prefix, matcher 순서 문제를 어떻게 추적해야 하는가?

개요

인증은 사용자가 누구인지 확인하는 단계다. 인가는 그 사용자가 이 요청을 수행할 수 있는지 판단하는 단계다. Spring Security의 요청 인가는 주로 AuthorizationFilterAuthorizationManager 흐름에서 처리된다.

AuthorizationFilter는 현재 요청과 SecurityContextHolder에서 가져온 Authentication을 바탕으로 권한 규칙을 평가한다. 규칙에 통과하면 필터 체인을 계속 진행하고, 거부되면 예외 처리 흐름으로 넘어간다. 기본적으로 Security FilterChain 뒤쪽에서 실행되므로, 직접 추가한 필터가 이 앞에 있는지 뒤에 있는지도 접근 제어 관점에서 중요하다.

요청 권한 규칙

@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
            .requestMatchers(HttpMethod.GET, "/articles/**").permitAll()
            .requestMatchers("/admin/**").hasRole("ADMIN")
            .requestMatchers("/api/orders/**").hasAuthority("SCOPE_order:write")
            .anyRequest().authenticated());
    return http.build();
}

규칙은 순서대로 읽어야 한다. 먼저 매칭되는 규칙이 요청에 적용된다. 넓은 anyRequest를 먼저 두면 뒤의 구체 규칙은 의미가 없어질 수 있다.

hasRole("ADMIN")은 일반적으로 ROLE_ADMIN authority를 기대한다. 이미 저장된 권한 문자열이 ADMIN인지 ROLE_ADMIN인지, JWT scope가 SCOPE_ prefix로 변환되는지 확인해야 한다.

정적 리소스나 공개 endpoint는 가능하면 filter chain을 완전히 건너뛰는 ignoring보다 permitAll을 먼저 검토한다. Spring Security 6에서는 authorization rule이 필요할 때만 세션을 조회하는 방향이라, public endpoint를 filter chain에 남겨 두어 보안 헤더 같은 처리를 유지하는 이점이 있다.

다만 permitAll은 인증/인가 판단을 통과시킨다는 뜻이지 요청이 모든 보안 필터를 건너뛴다는 뜻은 아니다. 예를 들어 브라우저 기반 POST endpoint가 permitAll이어도 CSRF 보호가 켜져 있으면 token이 없어서 403이 날 수 있다. 공개 endpoint의 실패를 볼 때는 인가 규칙과 CSRF, CORS, 인증 필터를 분리해서 확인해야 한다.

401과 403

401은 인증 정보가 없거나 인증에 실패했다는 뜻에 가깝다. AuthenticationEntryPoint가 응답을 만든다. JWT가 없거나 만료되었거나 잘못된 Bearer prefix를 사용하면 401이 날 수 있다.

403은 인증은 되었지만 권한이 부족하다는 뜻이다. AccessDeniedHandler가 응답을 만든다. 필요한 authority가 없거나 CSRF token이 누락된 경우에도 403을 볼 수 있다.

둘을 섞어 보면 디버깅이 어렵다. 먼저 현재 요청에 Authentication이 만들어졌는지 확인하고, 그 다음 authorities와 matcher 규칙을 비교한다.

AuthorizationFilter는 dispatcher type에도 영향을 받을 수 있다. ERROR나 FORWARD dispatch가 다시 인가 대상이 되어 예상치 못한 403을 만들면 dispatcherTypeMatchers로 허용할지 검토한다.

Method Security와의 관계

URL 권한은 HTTP 요청 경로와 method 기준의 보안이다. Method Security는 Service 메서드 실행 권한을 제어한다. 둘은 경쟁 관계가 아니라 서로 다른 경계다.

예를 들어 /admin/reports URL 자체는 ADMIN만 접근 가능하게 하고, 특정 리포트 다운로드 메서드는 도메인 객체 소유자나 관리자만 허용하도록 method security를 둘 수 있다. 단, 같은 규칙을 양쪽에 무분별하게 중복하면 변경 비용이 커진다.

실전 팁

  • 보안 규칙은 좁은 matcher에서 넓은 matcher 순서로 둔다.
  • hasRole을 쓸 때 실제 GrantedAuthority에 ROLE_ prefix가 있는지 확인한다.
  • JWT scope는 Spring Security에서 SCOPE_ prefix authority로 매핑될 수 있음을 기억한다.
  • 403은 현재 Authentication과 authorities를 로그나 테스트로 확인한다.
  • ERROR/FORWARD dispatch에서 보안 규칙이 다시 적용되는지 확인한다.
  • 인증 실패 응답과 권한 실패 응답은 각각 entry point와 access denied handler에서 일관되게 설계한다.

위험 신호!

  • anyRequest().permitAll()이 구체 보안 규칙보다 앞에 있다.
  • role 저장 값과 hasRole 기대 prefix가 맞지 않는다.
  • 401과 403을 모두 같은 API 오류 코드로 내려준다.
  • URL 보안과 Method Security에 같은 규칙이 중복되어 서로 어긋난다.
  • public resource를 무조건 ignoring 처리해 보안 헤더나 공통 Security filter 처리를 놓친다.
  • permitAll이면 CSRF도 자동으로 통과한다고 오해한다.
  • JWT scope 변환 규칙을 모르고 authority 이름을 추측한다.

확인 질문

확인 질문

  • AuthorizationFilter는 무엇을 비교하는가?
    • 현재 Authentication과 HttpServletRequest에 적용되는 authorization rule을 비교한다.
  • hasRole("ADMIN")을 사용할 때 흔한 함정은 무엇인가?
    • 실제 authority 문자열의 ROLE_ prefix와 기대값이 맞지 않아 403이 발생하는 것이다.
  • 401과 403을 디버깅하는 순서는 무엇인가?
    • 먼저 Authentication이 생성되었는지 확인하고, 그 다음 authorities와 matcher 규칙을 확인한다.

참고 문서