๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ’ป ํ”„๋กœ์ ํŠธ/๐Ÿ› ๏ธ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

[ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ… ] 403 Forbidden ์—๋Ÿฌ์™€ SecurityContextHolder ๋ฌธ์ œ ํ•ด๊ฒฐํ•˜๊ธฐ

by carrot0911 2025. 1. 27.

Spring Security๋ฅผ ์ด์šฉํ•ด์„œ ๊ถŒํ•œ๋ณ„ URL ์ ‘๊ทผ์„ ์„ค์ •ํ•˜๋˜ ์ค‘, 403 Forbidden ์—๋Ÿฌ๋ฅผ ๋งˆ์ฃผํ–ˆ๋‹ค.


๋ฌธ์ œ ์ƒํ™ฉ

๋‹ค์Œ๊ณผ ๊ฐ™์ด /admin/** ์™€ /users/** ์— ๋Œ€ํ•œ ๊ถŒํ•œ์„ ์„ค์ •ํ•œ ์ƒํƒœ์—์„œ ADMIN ๊ณ„์ •์œผ๋กœ ์ ‘๊ทผํ•  ๋•Œ 403 Forbidden ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/users/**").hasRole("USER")

๋ฌธ์ œ ์›์ธ ๋ถ„์„

JwtFilter์—์„œ ์ธ์ฆ(Authentication)๊ณผ ์ธ๊ฐ€(Authorization)๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ฉด์„œ ๋ฐœ์ƒํ–ˆ๋‹ค.
Spring Security๊ฐ€ ๋‚ด๋ถ€์ ์œผ๋กœ SecurityContextHolder๋ฅผ ์ด์šฉํ•ด ์ธ์ฆ ์ •๋ณด๋ฅผ ๊ด€๋ฆฌํ•˜๋Š”๋ฐ, JwtFilter์—์„œ SecurityContextHolder์— ์ธ์ฆ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š์•„์„œ ๊ถŒํ•œ ์ฒ˜๋ฆฌ๊ฐ€ ์ œ๋Œ€๋กœ ์ด๋ฃจ์–ด์ง€์ง€ ์•Š์•˜๋‹ค.
๊ฒฐ๊ณผ์ ์œผ๋กœ JwtFilter์—์„œ ์ธ์ฆ์„ ์ง„ํ–‰ํ–ˆ์Œ์—๋„ Spring Security์—์„œ ์ธ์ฆ ์ •๋ณด๋ฅผ ์ฐพ์ง€ ๋ชปํ•ด 403 Forbidden ์—๋Ÿฌ๊ฐ€ ๋ฐ˜ํ™˜๋œ ๊ฒƒ์ด๋‹ค.


ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

SpringContextHolder์— ์ธ์ฆ ์ •๋ณด ์ €์žฅํ•˜๊ธฐ

JwtFilter์—์„œ JWT ํ† ํฐ์„ ํŒŒ์‹ฑํ•˜์—ฌ ์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ๊ณผ ๊ถŒํ•œ ์ •๋ณด๋ฅผ ์ถ”์ถœํ•œ ๋’ค, ์ด๋ฅผ SecurityContextHolder์— ์ €์žฅํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ๋‹ค.

String email = jwtUtil.extractEmail(jwt); // JWT์—์„œ ์ด๋ฉ”์ผ ์ถ”์ถœ
String userRole = jwtUtil.extractRoles(jwt); // JWT์—์„œ ์‚ฌ์šฉ์ž ๊ถŒํ•œ ์ถ”์ถœ
UserRole role = UserRole.of(userRole); // ๊ถŒํ•œ ๋ณ€ํ™˜

// ๊ถŒํ•œ ๋ฆฌ์ŠคํŠธ ์ƒ์„ฑ
List<SimpleGrantedAuthority> authorities = List.of(new SimpleGrantedAuthority(role.name()));

// SecurityContextHolder์— ์ธ์ฆ ์ •๋ณด ์ €์žฅ
SecurityContextHolder.getContext().setAuthentication(
    new UsernamePasswordAuthenticationToken(email, null, authorities)
);

JwtFilter์—์„œ ์ธ๊ฐ€ ๋กœ์ง ์ œ๊ฑฐ

Spring Security์—์„œ ๊ถŒํ•œ(Authorization)์„ ๊ด€๋ฆฌํ•˜๋„๋ก ์„ค๊ณ„ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— JwtFilter์—์„œ ์ธ๊ฐ€๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋กœ์ง์„ ์ œ๊ฑฐํ–ˆ๋‹ค.
์ด๋ฅผ ํ†ตํ•ด ์ธ์ฆ๊ณผ ์ธ๊ฐ€ ๋กœ์ง์ด ์„œ๋กœ ์ถฉ๋Œํ•˜์ง€ ์•Š๋„๋ก ๊ตฌ์กฐ๋ฅผ ๋‹จ์ˆœํ™”ํ–ˆ๋‹ค.

hasRole ๋Œ€์‹  hasAuthority ์‚ฌ์šฉ

Spring Security์—์„œ ๊ถŒํ•œ์„ ํ™•์ธํ•  ๋•Œ๋Š” ROLE_ ์ ‘๋‘์‚ฌ๊ฐ€ ํฌํ•จ๋œ ๊ฐ’์„ ์ด์šฉํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, "ADMIN" ์—ญํ• ์„ ํ™•์ธํ•˜๋ ค๋ฉด "ROLE_ADMIN" ๊ถŒํ•œ์ด ์กด์žฌํ•ด์•ผ ํ•œ๋‹ค.
๋”ฐ๋ผ์„œ hasRole("ADMIN") ๋Œ€์‹  hasAuthority("ADMIN")์„ ์‚ฌ์šฉํ•ด์•ผ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•œ๋‹ค.


์ตœ์ข… ์ฝ”๋“œ

JwtFilter

๋”๋ณด๊ธฐ
@Slf4j
@RequiredArgsConstructor
@Component
public class JwtFilter implements Filter {

    private final JwtUtil jwtUtil;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        String url = httpRequest.getRequestURI();

        if (url.startsWith("/auth")) {
            chain.doFilter(request, response);
            return;
        }

        String bearerJwt = httpRequest.getHeader("Authorization");

        if (bearerJwt == null) {
            // ํ† ํฐ์ด ์—†๋Š” ๊ฒฝ์šฐ 400์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
            httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT ํ† ํฐ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.");
            return;
        }

        String jwt = jwtUtil.substringToken(bearerJwt);

        try {
            // JWT ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์™€ claims ์ถ”์ถœ
            Claims claims = jwtUtil.extractClaims(jwt);
            if (claims == null) {
                httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "์ž˜๋ชป๋œ JWT ํ† ํฐ์ž…๋‹ˆ๋‹ค.");
                return;
            }

            // ์ธ๊ฐ€ -> ์–ด๋–ป๊ฒŒ ํ• ๊ฒƒ์ธ๊ฐ€..? ๊ณ ๋ฏผ์„ ํ•ด๋ณด๊ณ 
            String email = jwtUtil.extractEmail(jwt);
            String userRole = jwtUtil.extractRoles(jwt);
            UserRole role = UserRole.of(userRole);

            List<SimpleGrantedAuthority> authorities = List.of(new SimpleGrantedAuthority(role.name()));

            SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(email, null, authorities));

            chain.doFilter(request, response);
        } catch (SecurityException | MalformedJwtException e) {
            log.error("Invalid JWT signature, ์œ ํšจํ•˜์ง€ ์•Š๋Š” JWT ์„œ๋ช… ์ž…๋‹ˆ๋‹ค.", e);
            httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "์œ ํšจํ•˜์ง€ ์•Š๋Š” JWT ์„œ๋ช…์ž…๋‹ˆ๋‹ค.");
        } catch (ExpiredJwtException e) {
            log.error("Expired JWT token, ๋งŒ๋ฃŒ๋œ JWT token ์ž…๋‹ˆ๋‹ค.", e);
            httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "๋งŒ๋ฃŒ๋œ JWT ํ† ํฐ์ž…๋‹ˆ๋‹ค.");
        } catch (UnsupportedJwtException e) {
            log.error("Unsupported JWT token, ์ง€์›๋˜์ง€ ์•Š๋Š” JWT ํ† ํฐ ์ž…๋‹ˆ๋‹ค.", e);
            httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "์ง€์›๋˜์ง€ ์•Š๋Š” JWT ํ† ํฐ์ž…๋‹ˆ๋‹ค.");
        } catch (Exception e) {
            log.error("Internal server error", e);
            httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

Spring Security

๋”๋ณด๊ธฐ
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtFilter jwtFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(AbstractHttpConfigurer::disable)
            .httpBasic(AbstractHttpConfigurer::disable)
            .formLogin(AbstractHttpConfigurer::disable)
            .addFilterBefore(jwtFilter, SecurityContextHolderAwareRequestFilter.class)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll()
                .requestMatchers("/todos/**").permitAll()
                .requestMatchers("/admin/**").hasAuthority("ADMIN")
                .requestMatchers("/users/**").hasAuthority("USER")
                .anyRequest().authenticated()
            )
            .build();
    }
}

๊ฒฐ๋ก 

SecurityContextHolder๋ฅผ ํ™œ์šฉํ•˜์—ฌ Spring Security์— ์ธ์ฆ ์ •๋ณด๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ „๋‹ฌํ•ด์•ผ ํ•œ๋‹ค.
์ธ์ฆ(Authentication)๊ณผ ์ธ๊ฐ€(Authorization) ์—ญํ• ์„ ๋ช…ํ™•ํžˆ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€๋ฆฌํ•œ๋‹ค.

  • JwtFilter: ์ธ์ฆ ์ •๋ณด ์ถ”์ถœ ๋ฐ ์ €์žฅ
  • Spring Security: ๊ถŒํ•œ ๊ด€๋ฆฌ ๋ฐ ์ ‘๊ทผ ์ œ์–ด

๊ถŒํ•œ ๊ฒ€์‚ฌ ์‹œ hasRole ๋Œ€์‹  hasAuthority๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด๊ฒฐํ•œ๋‹ค.