<< 요약 >>
- 문제 상황
• EC2 서버에 배포 후 http://{ip}:8080/ 에 접속했을 때 403 Forbidden 에러 발생
• EC2 인바운드 규칙 등 네트워크 설정은 정상이나, 스프링 시큐리티 때문에 권한 문제가 의심됨
- 원인
• 프로젝트 내에 SecurityFilterChain이 여러 개 존재
• Spring Security는 여러 개의 SecurityFilterChain 중 가장 먼저 매칭되는 체인의 설정을 우선 적용
• 특정 경로만 인증을 요구하게 하고 싶었으나, 두 번째 체인에서도 / 경로를 인증 필요로 설정해버려서 충돌 발생
• @Order 애노테이션을 지정하지 않으면, 빈(Bean)의 생성 순서로 필터 체인 우선순위가 결정되어 예측 불가한 문제 야기
- 해결 방법
1) 필터 체인별로 @Order 명시
• @Order(1) → WebSocket 보안을 담당할 SecurityFilterChain
• /ws/**, /topic/**, /app/**, /api/notification/** 등 특정 경로만 매칭
• 해당 경로들은 permitAll()로 설정
• @Order(2) → 일반 웹 보안을 담당할 SecurityFilterChain
• 그 외 경로에 대해 인증 로직 수행
2) securityMatcher() 사용
• @Order(1) 체인: securityMatcher("/ws/**"...)로 명확히 적용 대상 경로 지정
• 불필요하게 루트 경로("/")까지 걸리지 않도록 설정
- 결과
• 각 체인이 담당하는 경로가 명확해짐
• 루트 경로 등의 접속 시 더 이상 403 에러가 발생하지 않음
• 성공적으로 배포 완료
------------------------------------------------------------------------
(해당 ip)에 대한 액세스가 거부됨
이 페이지를 볼 수 있는 권한이 없습니다.
HTTP ERROR 403
일단 배포를 하고 서버가 잘 돌아가고 있는 것도 확인했다.
웹 브라우저에서 http://(해당 ip):8080/ 에 접속을 시도 했고, 위와 같은 에러를 맞이했다.
우선은 혹시라도 ec2 인바운드에서 포트가 안뚫려 있다거나 그런 문제일까봐 확인했다.
뭐 당연히 잘 열어뒀었다..
사실 어떻게 접근해야할지 조금 막막했는데..
권한 문제다보니 local에서 테스트할 때 굉장히 나를 괴롭혔었던 spring security가 떠올랐다.
우선 코드는 아래와 같이 작성했다.
private static final String[] WHITELIST = {"/", "/login", "/loginHome", "/signUp", "/renew", "/loginSuccess",
"/login/oauth2/code/**", "/oauth2/signUp", "/error", "/js/**", // Swagger UI & Docs
"/swagger-ui", "/v3/api-docs", "/swagger-ui/index.html", "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**"
,"/api/**", "/WebSocketTest.html","/ws/**","/swagger-ui"};
WHITELIST라는 문자열 배열을 만들었고, 이 배열로 어플리케이션에서 인증이나 권한이 필요하지 않은 URL 경로들을 지정해주었다.
일단 로컬에서 구현하다가 인증과 관련된 문제들이 발생할 때 마다 하나하나 필요한 것을 이것저것 넣다보니 많아졌다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.disable()) // CORS 설정: 필요에 따라 enable()로 변경
.csrf(csrf -> csrf.disable()) // CSRF 비활성화
.authorizeHttpRequests(authz -> authz
.requestMatchers(WHITELIST).permitAll()
.anyRequest().authenticated() // 나머지 경로는 인증 요구
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(formLogin -> formLogin.disable())
.httpBasic(httpBasic -> httpBasic.disable())
.logout(logout -> logout.logoutSuccessUrl("/"))
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo.userService(oAuth2UserService))
.successHandler(customOAuth2LoginSuccessHandler())
.failureHandler(customOAuth2LoginFailureHandler())
);
}
FilterChain 메서드에서 중간에 WHITELIST에 정의된 경로에 대해 인증없이 접근을 허용하도록 했다.
그리고 anyRequest().authenticated()로 나머지 경로는 인증을 요구하도록 했다.
팀프로젝트를 하면서 다른 백엔드 팀원 분이 짠 코드를 확인했더니..
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// CSRF 비활성화 (WebSocket 요청은 CSRF 보호가 필요 없음)
.csrf(csrf -> csrf.disable())
// 요청에 대한 권한 설정
.authorizeHttpRequests(auth -> auth
.requestMatchers("/ws/**","/topic/**", "/app/**","/WebSocketTest.html","/api/notification/**").permitAll() // WebSocket 엔드포인트는 인증 없이 접근 허용
.anyRequest().authenticated() // 그 외 요청은 인증 필요
);
return http.build();
}
SecurityFilterChain이 또 있었다.
여기서는 "/" 경로로 들어오는 것에 대한 인증을 요구하고 있었고, 설마 여기서 문제가 발생하지 않았을까 라고 생각이 들었다.
Spring security는 여러 개의 SecurityFilterChain이 있을 경우, 매칭되는 첫 번째체인(또는 Order가 낮은 체인)으로 보안 로직을 수행한다.
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/ws/**", "/topic/**", "/app/**","/WebSocketTest.html","/api/notification/**").permitAll()
.anyRequest().authenticated()
);
위는 WebSocketSecurityConfig 코드의 일부분이었는데,
해당 체인에 securityMatcher()나 매칭 순서(@Order)를 지정하지 않으면,
"/" 과 같은 홈 URL도 이 체인에 적용될 수 있다고 한다.
즉, 이말은 @Order Annotation이 없는 경우,
Spring Security는 필터 체인의 우선순위를 빈의 등록 순서에 따라 결정한다.
근데 이 등록순서는 항상 명확하게 보장되지 않으므로, 환경에 따라 다소 예측 불가능한 결과를 초래할 수 있는 것!
-@Order 애너테이션이 명시된 경우:
• 낮은 숫자가 높은 우선순위를 가집니다. (@Order(1)이 @Order(2)보다 우선 적용)
- @Order가 명시되지 않은 경우:
• 빈이 생성된 순서대로 체인이 등록됩니다. 이 순서는 일반적으로 @Configuration 클래스의 로드 순서와 관련이 있지만, 보장되지 않습니다.
<References>
https://docs.spring.io/spring-security/reference/servlet/configuration/java.html
Java Configuration :: Spring Security
Spring Security’s Java configuration does not expose every property of every object that it configures. This simplifies the configuration for a majority of users. After all, if every property were exposed, users could use standard bean configuration. Whi
docs.spring.io
11. The Security Filter Chain
Spring Security’s web infrastructure is based entirely on standard servlet filters. It doesn’t use servlets or any other servlet-based frameworks (such as Spring MVC) internally, so it has no strong links to any particular web technology. It deals in H
docs.spring.io
@Configuration
@Order(1)
public class WebSocketSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 이 체인에서는 "/ws/**" 등 특정 경로만 보안 필터가 동작
.securityMatcher("/ws/**", "/topic/**", "/app/**", "/api/notification/**", "/WebSocketTest.html")
// CSRF 비활성화 (WebSocket 요청은 CSRF 보호가 필요 없음)
.csrf(csrf -> csrf.disable())
// 요청에 대한 권한 설정
.authorizeHttpRequests(auth -> auth
.requestMatchers("/ws/**","/topic/**", "/app/**","/WebSocketTest.html","/api/notification/**").permitAll() // WebSocket 엔드포인트는 인증 없이 접근 허용
.anyRequest().authenticated() // 그 외 요청은 인증 필요
);
return http.build();
}
}
따라서 securityMatcher을 통해 위의 체인에서는 "/ws/**" 등 특정 경로만 보안 필터가 동작하도록 했다.
@Order annotation 도 달아주었고,
당연히 내 SecurityFilterChain에도 @Order annotation을 달아주었다.
@Order(2)
public class WebSecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
private final OAuth2UserServiceImpl oAuth2UserService;
private final JwtService jwtService;
바꾸고 났더니?
결론적으로 해결완료! 403 에러가 더이상 뜨지 않았고, 결국 배포를 완료했다.
'지식 나눔' 카테고리의 다른 글
| Cloudflare Worker 기반 GitHub → Discord 알림 시스템 구축 (0) | 2025.11.19 |
|---|---|
| [Vue] 오픈소스 이슈 제보 후기 (3) | 2025.07.24 |
| [Github Actions & Docker] unable to authenticate, attempted methods - 해결 (0) | 2025.01.20 |
| [Github Actions & Docker] error username and password required 해결 (0) | 2025.01.20 |
| Postman을 활용한 카카오톡 로그인 테스트 - Oauth2(Bad client credentials) (4) | 2024.01.28 |