HttpSecurity

어질어질하다

 

Spring Security는 설정이 대부분일 정도로 Security 설정하는 부분이 정말 많습니다. 또, 6.x.x 버전까지 오면서 Deprecated된 메소드들도 정말 많은데요 중간점검할겸 메소드들을 좀 정리해봤습니다.

1. 보안 설정

1-1 .cors()

  • CORS 설정
  • 크로스 도메인 요청 처리
http.cors(cors -> cors
    .configurationSource(corsConfigurationSource));

 

 

[CS][Spring Security] CORS에 대해서 알아보자

1. CORS란 무엇인가?CORS(Cross-Origin Resource Sharing)는 웹 브라우저에서 외부 도메인 리소스를 안전하게 요청할 수 있도록 하는 표준 규약입니다. 프론트엔드와 백엔드가 분리하는데 있어 CORS에 대해서

tmd8633.tistory.com

 

 

1-2. csrf()

  • CSRF 보호 설정
  • CSRF 토큰 처리
http.csrf(csrf -> csrf
    .ignoringAntMatchers("/api/**")
    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));

 

 

[CS][Spring Security] CSRF란?

CSRF란?CSRF Cross-Site Request Forgery의 약자로 인증된 사용자의 권한을 악용하여 해당 사용자가 의도하지 않은 요청을 웹사이트에 전송하는 공격 기법입니다. 공격자는 사용자가 이미 인증된 상태를

tmd8633.tistory.com

 

 

1-3. headers()

  • 보안 헤더 설정
  • XSS 방지, 클릭재킹 방지 등
http.headers(headers -> headers
    .defaultsDisabled()  // 기본 설정을 비활성화하고 커스텀 설정 시작
    .frameOptions(frame -> frame.deny())  // X-Frame-Options
    .xssProtection(xss -> xss.enable())   // X-XSS-Protection
    .contentTypeOptions(content -> content.disable())  // X-Content-Type-Options
    .cacheControl(cache -> cache.disable())  // Cache-Control
);

이 부분에 대해서는 아직 공부하지 못했습니다. 공부하고 정리해서 글 쓴다음에 이부분 업데이트 하겠습니다.

 

 

1-4. x509()

  • X.509 인증서 기반 인증
  • 클라이언트 인증서 인증
http.x509(x509 -> x509
    .subjectPrincipalRegex("CN=(.*?)(?:,|$)")     // CN(Common Name)을 추출하는 정규식
    .userDetailsService(userDetailsService))       // 인증서의 사용자 정보와 매핑

주로, SSL/TLS 클라이언트 인증에 사용되며 높은 수준의 보안이 필요한 B2B 환경이나 금융시스템에서 사용된다고 합니다.

 

서버간 통신(Server-to-Server)

  • 마이크로서비스 간 통신
  • API 게이트웨이 인증

IoT 디바이스 인증

  • 각 디바이스에 고유 인증서 발급
  • 디바이스 인증 관리

금융권 시스템

  • 공인인증서 기반 인증
  • 보안이 중요한 기업 시스템

 

이 부분도 자세히 몰라서 공부하고 업데이트 하겠습니다.

 

 

1-5. jee()

  • Java EE 보안 설정
  • 컨테이너 기반 인증
http.jee(jee -> jee
    .mappableRoles("USER", "ADMIN"));

 

Java EE (Enterprise Edition) 기반의 보안 설정을 구성하는 데 사용됩니다 하지만 현재는 잘 사용하지 않고 JWT, OAuth2 같은 인증 방식을 사용합니다. 그러니까 예전 어플리케이션에서 사용된다고만 알고있으면 됩니다.

 

 

1-6. requiresChannel()

  • 채널 보안 요구사항
http.requiresChannel(channel -> channel
        .requestMatchers("/secure/**").requiresSecure()     // HTTPS 필수
        .requestMatchers("/public/**").requiresInsecure()   // HTTP 허용
        .anyRequest().requiresSecure()                      // 나머지는 모두 HTTPS
    );

HTTP, HTTPS 허용에 대한 설정입니다. 사실 프로덕션 환경에서는 모든 요청에 HTTPS를 사용하는 것이 권장되기때문에 이런 설정을 보통 하지 않고 일괄적으로 HTTPS 설정을 하곤합니다.

 

 

http.requiresChannel(channel -> channel
        .anyRequest().requiresSecure() // 모든 요청에 HTTPS 필수
    );

 

 

2. 기본 인증 설정

2-1. httpBasic()

  • HTTP Basic 인증 활성화
  • 간단한 사용자명/비밀번호 기반으로 인증
http.httpBasic(basic -> basic
    .realmName("My App")
    .authenticationEntryPoint(customEntryPoint));

username:password 형식으로 Base64로 인코딩되기 때문에 보안에 취약할 수 있습니다.

http.formLogin() 과 같이 설정을 할 경우 formLogin 이 우선적용됩니다.

REST API에서 커스텀해서 사용할 수는 있으나 그냥 JWT나 쓰는게 더 안전합니다.

 

 

2-2. anonymous()

  • 익명 사용자 접근에 대한 설정
http.anonymous(anonymous -> anonymous
    .principal("guest")
    .authorities("ROLE_GUEST"));

아래 글에서 한번 다룬적이 있습니다.

 

 

[Spring Security] 스프링 시큐리티 Anonymous과 ExceptionHandling (4)

Anonymous 인증이란?Spring Security의 Anonymous 인증은 인증되지 않은 사용자(로그인하지 않은 사용자)를 처리하는 메커니즘입니다. 인증되지 않은 요청에 대해 AnonymousAuthenticationToken을 생성하여 보안

tmd8633.tistory.com

 

 

 

3. 폼 로그인

3-1. formLogin()

  • Form 기반 로그인 설정
  • 로그인 성공/실패 Handling
http.formLogin(form -> form
    .loginPage("/login")
    .defaultSuccessUrl("/home")
    .failureUrl("/login?error=true"));

 

 

3-2. logout()

  • 로그아웃 처리 설정
  • 로그아웃 후 Handling
http.logout(logout -> logout
    .logoutUrl("/logout")
    .logoutSuccessUrl("/login")
    .deleteCookies("JSESSIONID"));

 

formLogin, logout 에 대해서도 작성한 글이 있습니다.

 

[Spring Security] 스프링 시큐리티 시작하기 (로그인, 로그아웃과 권한) (2)

이전글 [Spring Security] 스프링 시큐리티 이해하기 (1)Spring SecuritySpring Security는 인증, 인가 및 일반적인 공격에 대한 보호를 제공하는 프레임워크 입니다. 서비스를 개발할때 필수적으로 구현해야

tmd8633.tistory.com

 

3-3. rememberMe()

  • 세션 만료 후에도 로그인 유지 기능
http.rememberMe(remember -> remember
    .rememberMeServices(rememberMeServices)
)
// 또는

http.rememberMe(remember -> remember
    .key("uniqueAndSecret")
    .tokenValiditySeconds(86400)
    .rememberMeParameter("remember-me")
    .rememberMeCookieName("remember-me")
);

 

 

[Spring Security] 스프링 시큐리티 RememberMe (3)

Remember-Me란?Remember-Me는 사용자가 브라우저를 닫았다가 다시 열어도 로그인 상태를 유지하는 기능입니다. 쿠키를 사용하여 구현되며, Spring Security는 두 가지 구현 방식을 제공합니다.  Simple Hash-B

tmd8633.tistory.com

로그인 폼에서 "로그인 정보 저장" 이런식으로 체크박스되어있는 그 기능에 대한 설명입니다. 아무래도 토큰 형식이다보니까 중요한 작업(결제, 정보수정 등) 에서는 재인증을 요구하는 등의 보안에 신경써야하는 기능입니다. 토큰은 DB에 저장하여 관리하는 것을 권장합니다.

 

 

3-4. passwordManagement()

 

  • 비밀번호 변경 페이지 설정
  • 비밀번호 정책 적용
http.passwordManagement(password -> password
    .changePasswordPage("/change-password")       // 비밀번호 변경 페이지
);

 

 

이 기능은 아무리 찾아봐도 잘 모르겠어서 아래 내용이 사실이 아닐 수 있습니다. 주의해서 읽어주세요

 

비밀번호 변경 페이지에 접속하게 되면 다음과 같은 헤더가 추가된다고합니다.

Change-Password: /change-password

별다른 기능은 없는것같습니다. 혹시라도 누가 이 글을 읽고 passwordManagement 에 대해서 아신다면 댓글로 알려주세요 저도 알고싶어요

 

 

4. 세션관리

4-1. sessionManagerment()

  • 세션 정책 설정
  • 동시 세션 제어 등
http.sessionManagement(session -> session
    .invalidSessionUrl("/login?invalid")
    .maximumSessions(1)
    .maxSessionsPreventsLogin(false)
    .expiredUrl("/login?expired")
);

 

 

[Spring Security] 스프링 시큐리티 SessionManagement (5)

1. Session Management란?Spring Security의 세션 관리는 사용자의 세션을 생성, 유지, 파괴하는 전반적인 프로세스를 관리합니다. 이는 보안과 사용자 경험 모두에 중요한 영향을 미칩니다. 또한 JWT이나 Se

tmd8633.tistory.com

 

 

4-2. securityContext()

  • 보안 컨텍스트 설정
  • SecurityContext 저장 방식 설정
http.securityContext(context -> context
    .requireExplicitSave(true)
    .securityContextRepository(securityContextRepository)  // 저장소 설정
);

 

SecurityContext의 관리 방식을 설정하는 메서드입니다.

 

 

Custom

@Component
public class CustomSecurityContextRepository implements SecurityContextRepository {
    
    @Override
    public SecurityContext loadContext(HttpRequestResponseHolder holder) {
        HttpServletRequest request = holder.getRequest();
        // 요청에서 SecurityContext 로드 로직
        return SecurityContextHolder.createEmptyContext();
    }
    
    @Override
    public void saveContext(SecurityContext context, 
                          HttpServletRequest request,
                          HttpServletResponse response) {
        // SecurityContext 저장 로직
    }
    
    @Override
    public boolean containsContext(HttpServletRequest request) {
        // SecurityContext 존재 여부 확인
        return false;
    }
}

 

 

세션 기반 저장소 (Default)

http.securityContext(context -> context
    .securityContextRepository(new HttpSessionSecurityContextRepository())
);

 

다중 연결 저장소

http.securityContext(context -> context
    .securityContextRepository(new DelegatingSecurityContextRepository(
        new RequestAttributeSecurityContextRepository(),
        new HttpSessionSecurityContextRepository()
    ))
);

 

 

Stateless

http.securityContext(context -> context
    .securityContextRepository(new NullSecurityContextRepository())
);

 

SecurityContext를 저장하지 않습니다. JWT 토큰 기반 인증에서 사용됩니다.

sessionManagement에서

.sessionCreationPolicy(SessionCreationPolicy.STATELESS)

설정을 하면 NullSecurityContextRepository 가 생성됩니다. 그러므로 저장소를 명시적으로 작성하지 않아도 된다는 얘기입니다.

 

// SessionManagementConfigurer.init(H http)
boolean stateless = this.isStateless();
if (securityContextRepository == null) {
    if (stateless) {
        this.sessionManagementSecurityContextRepository = new NullSecurityContextRepository();
    } else {
        // ... 생략
    }
} else {
    // ... 생략
}

// ... 생략

private boolean isStateless() {
    SessionCreationPolicy sessionPolicy = this.getSessionCreationPolicy();
    return SessionCreationPolicy.STATELESS == sessionPolicy;
}

 

요청 속성 저장소

http.securityContext(context -> context
    .securityContextRepository(new RequestAttributeSecurityContextRepository())
);

 

SecurityContext를 요청 속성에 저장하고 단일 요청범위 내에서만 유효합니다. 주로 비동기 요청 처리에서 사용한다고합니다.

사실 이 부분은 공부안해봐서 잘 모르겠습니다.

 

 

 

5. OAuth2

5-1. oauth2Login()

  • OAuth 2.0 로그인 설정
  • 소셜 로그인
http.oauth2Login(oauth2 -> oauth2
    .loginPage("/oauth2/login")
    .defaultSuccessUrl("/home")
    .userInfoEndpoint()
    .userService(customOAuth2UserService));
 

[Spring Security][OAuth2] OAuth2 Log In 소셜로그인 구현 (1)

OAuth2Spring Security의 OAuth 2.0 지원은 크게 두 가지 주요 기능으로 구성됩니다:OAuth2 Resource ServerOAuth2 ClientOAuth2 Log In추가로 OAuth2 Log In이라는 특별한 기능이 있는데, 이는 독립적으로 존재할 수 없으

tmd8633.tistory.com

 

 

5-2. oauth2Client()

  • OAuth 2.0 클라이언트 설정
  • 외부 OAuth 2.0 리소스 접근
http.oauth2Client(client -> client
    .clientRegistrationRepository(clientRegistrationRepository)
    .authorizedClientService(authorizedClientService));

 

 

 

5-3. oauth2ResourceServer()

  • OAuth 2.0 리소스 서버 설정
  • JWT 토큰 검증
http.oauth2ResourceServer(server -> server
    .accessDeniedHandler(new CustomAccessDeniedHandler())
    .authenticationEntryPoint(new CustomEntryPoint())
    .jwt(jwt -> jwt
        .jwtAuthenticationConverter(jwtAuthenticationConverter)
    )
);

 

JWT 관련해서 작성하고 있는 글이 있습니다. 업로드되는대로 업데이트 할겁니다.

 

 

6. 엔터프라이즈 보안

6-1. oidcLogout()

  • OpenID Connect 로그아웃 설정
http.oauth2Login(oauth2 -> oauth2
    .clientRegistrationRepository(clientRegistrationRepository));
    
http.oidcLogout(oidc -> oidc
    .clientRegistrationRepository(clientRegistrationRepository)
    .backChannel(logout -> logout
        .logoutUri("/api/logout")
        .logoutHandler((request, response, authentication) -> {

        })
    )

);

 

OIDC 설정은 아래 게시글에서 보시면 됩니다.

 

 

[Spring Security][OAuth2] OpenID Connect(OIDC) 구현 (2)

개요지난 글에서 OAuth2를 이용해서 로그인하는 방법을 알아보았습니다. 이번에는 OpenID Connect 를 이용해서 로그인을 구현해보도록 하겠습니다.  OIDC란?OpenID Connect(OIDC)는 OAuth 2.0 프로토콜 위에

tmd8633.tistory.com

 

 

6-2. saml2Login()

  • SAML 2.0 인증 설정
  • SSO(Single Sign On) 구현
http.saml2Login(saml2 -> saml2
    .relyingPartyRegistrationRepository(relyingPartyRegistrationRepository));

이 부분도 넘어가겠습니다 아직 여긴 공부 안했습니다.


 

7. 요청 처리

7-1. securityMatcher()

  • Security 기반 URL 접근 제어
http.securityMatcher("/**") // 해당 URL만 아래 Security 설정 적용
    .authorizeHttpRequests(request -> request
        .anyRequest().permitAll()
    )

 

REST API, WEB 환경에서 각각 Security 설정을 부여하고 싶은 경우

 

@Bean
    @Order(1)
    SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {

        http.securityMatcher("/api/**")
            .authorizeHttpRequests(request -> request
                .requestMatchers("/api/user").hasAnyRole(Role.USER.name(), Role.ADMIN.name())
                .requestMatchers("/api/admin").hasRole(Role.ADMIN.name())
                .anyRequest().permitAll()
            );

        return http.build();
    }

    @Bean
    @Order(2)
    SecurityFilterChain webSecurityFilterChain(HttpSecurity http, RememberMeServices rememberMeServices) throws Exception {

        http.securityMatcher("/**")
            .authorizeHttpRequests(request -> request
                .requestMatchers("/user/**").hasAnyRole(Role.USER.name(), Role.ADMIN.name())
                .requestMatchers("/admin/**").hasRole(Role.ADMIN.name())
                .anyRequest().permitAll()
            );

        return http.build();
    }

securityMatcher 경로가 세부적일 수록 Order 우선순위가 높아야합니다.

 

// 이러면 절대안됨!!! 
http.securityMatcher("/api/**")
    .authorizeHttpRequests(request -> request
        .requestMatchers("/api/user").hasAnyRole(Role.USER.name(), Role.ADMIN.name())
        .requestMatchers("/api/admin").hasRole(Role.ADMIN.name())
        .anyRequest().permitAll()
    )
    .securityMatcher("/**")
    .authorizeHttpRequests(request -> request
        .requestMatchers("/user/**").hasAnyRole(Role.USER.name(), Role.ADMIN.name())
        .requestMatchers("/admin/**").hasRole(Role.ADMIN.name())
        .anyRequest().permitAll()
    );

이어 붙힐 수 있다고 이렇게 붙히면 안됩니다. 반드시 SecurityFilterChain으로 연결해서 두개의 Filter로 사용해야 됩니다.

 

 

7-2. authorizeHttpRequests()

  • URL 기반 접근 제어
  • 권한별 리소스 접근 설정
http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/admin/**").hasRole("ADMIN")
    .requestMatchers("/user/**").hasRole("USER")
    .anyRequest().authenticated());

아래 게시물 권한설정 파트 보시면 됩니다.

 

 

[Spring Security] 스프링 시큐리티 시작하기 (로그인, 로그아웃과 권한) (2)

이전글 [Spring Security] 스프링 시큐리티 이해하기 (1)Spring SecuritySpring Security는 인증, 인가 및 일반적인 공격에 대한 보호를 제공하는 프레임워크 입니다. 서비스를 개발할때 필수적으로 구현해야

tmd8633.tistory.com

 

 

8. 예외처리

8-1. exceptionHandling()

  • 보안 예외 처리
  • 인증 / 인가 실패 처리
http.exceptionHandling(exception -> exception
    .authenticationEntryPoint(customAuthEntryPoint)
    .accessDeniedHandler(customAccessDeniedHandler));

 

아래 글에서 ExceptionHandling에 대해서 다뤘습니다.

 

 

[Spring Security] 스프링 시큐리티 Anonymous과 ExceptionHandling (4)

Anonymous 인증이란?Spring Security의 Anonymous 인증은 인증되지 않은 사용자(로그인하지 않은 사용자)를 처리하는 메커니즘입니다. 인증되지 않은 요청에 대해 AnonymousAuthenticationToken을 생성하여 보안

tmd8633.tistory.com

 

 

9. 기타 설정

9-1. requestCache()

  • 요청 캐시 설정
  • 인증 후 리다이렉트 요청 복원 기능
http.requestCache(cache -> cache
    .requestCache(new NullRequestCache()));

리다이렉트 요청에 대한 정보를 저장하는 곳을 설정하는 부분입니다. 설정은 securityContext() 부분과 비슷한 점이 많습니다.

 

// SessionManagementConfigurer.init(H http)

RequestCache requestCache = (RequestCache)http.getSharedObject(RequestCache.class);
if (requestCache == null && stateless) {
    http.setSharedObject(RequestCache.class, new NullRequestCache());
}

// .. 생략

SessionManagement STATELESS 설정을 하면 알아서 NullRequestCache를 넣어주는걸 볼 수 있습니다. Session 을 사용한다면 설정하지 않아도되고, API를 사용해서 Session Stateless 하려면 sessionManagement에서 STATELESS 설정만 해주면 자동으로 NullRequestCache가 설정되니까 별로 신경안써도 되는것 같습니다.

 

 

9-2. portMapper()

  • HTTP/HTTPS 포트 매핑
  • 보안 채널 전환
http.portMapper(ports -> ports
    .http(8080).mapsTo(8443)    // 8080 -> 8443
    .http(80).mapsTo(443)       // 80 -> 443
);



10. Filter

10-1. addFilter

http.addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class)  // 특정 필터 이전에 추가
    .addFilterAfter(new CustomFilter(), UsernamePasswordAuthenticationFilter.class)   // 특정 필터 이후에 추가
    .addFilterAt(new CustomFilter(), UsernamePasswordAuthenticationFilter.class);     // 특정 필터 위치에 추가

 

자신이 만든 필터를 Security Filter에 추가 시킬 수 있습니다. 그냥 addFilter(new CustomFilter()) 이렇게도 사용할 수는 있지만 특정 필터를 기준으로 Filter를 추가하는게 명확한 필터 순서를 제어할 수 있기때문에 Before, After, At을 사용하는것을 권장합니다.

 

 

마무리

후 이 글 쓰는데 오래걸렸습니다. Spring Security 에 대해서 계속 공부하고는 있는데 공식문서만 들어가기만 하면 정신이 혼미해지네요. 내용도 많고 서로 얽혀있는게 많아서 이해하면서 읽기가 쉽지가 않았습니다. 그래서 중간점검이나 할겸 조금 정리해봤는데 틀린 부분도 있을 겁니다. 그러니 걸러가면서 읽어주시기 바랍니다.

1. CORS란 무엇인가?

CORS(Cross-Origin Resource Sharing)는 웹 브라우저에서 외부 도메인 리소스를 안전하게 요청할 수 있도록 하는 표준 규약입니다. 프론트엔드와 백엔드가 분리하는데 있어 CORS에 대해서 반드시 짚고 넘어가야합니다. 그래서 온르은 CORS에 대해서 공부해보겠습니다.

 

 

 

2. CORS의 필요성

핵심은 외부로부터 리소스를 공유하는 것입니다. 요즘 웹 애플리케이션에 개발에서 백엔드와 프론트엔드를 구분하지 않고 개발하는 곳은 거의 없을 겁니다.

 

  • 프론트엔드와 백엔드의 분리
  • 마이크로서비스 아키텍처 도입
  • 외부 API 활용
  • SPA(Single Page Application) 개발 방식

이러한 상황에서 다른 출처(Origin)의 리소스를 안전하게 요청하고 사용할 수 있어야 했고, 이를 위한 표준이 바로 CORS입니다.

 

 

 

3. Same-Origin Policy

Same-Origin Policy는 웹 브라우저의 기본적인 보안 정책으로, 같은 출처에서만 리소스를 공유할 수 있도록 제한합니다.

출처(Origin)는 다음 세 가지 요소로 결정됩니다:

  • 프로토콜 (http, https)
  • 호스트 (domain)
  • 포트 번호
  • http://example.com/path1, https://example.com/path2 는 프로토콜이 다르므로 다른 출처로 간주됩니다.

 

 

4. CORS 동작 방식

CORS는 HTTP 헤더를 통해 동작합니다. 주요 헤더는 다음과 같습니다:

 

4-1. 요청 헤더

  • Origin: 요청을 보내는 출처
  • Access-Control-Request-Method: 실제 요청에서 사용할 HTTP 메서드
  • Access-Control-Request-Headers: 실제 요청에서 사용할 헤더

4-2. 응답 헤더

  • Access-Control-Allow-Origin: 허용된 출처
  • Access-Control-Allow-Methods: 허용된 HTTP 메서드
  • Access-Control-Allow-Headers: 허용된 헤더
  • Access-Control-Max-Age: 프리플라이트 요청 캐시 시간
  • Access-Control-Allow-Credentials: 인증 정보 포함 여부

 

5. CORS 요청의 종류

 

5-1. Simple Request

  • GET, HEAD, POST 중 하나의 메서드 사용
  • 허용된 헤더만 사용
  • Content-Type이 다음 중 하나:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

 

5-2. Preflight Request

Simple Request 조건을 만족하지 않는 요청의 경우, 브라우저는 실제 요청 전에 OPTIONS 메서드를 사용한 예비 요청을 보냅니다.

 

5-3. Credentialed Request

인증 정보(쿠키, HTTP 인증)를 포함한 요청입니다.

 

 

 

6. Spring Security CORS 설정

Spring Security CORS 설정에 대해서 알아보겠습니다.

 

 

UrlBasedCorsConfigurationSource apiConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();

        // 허용할 출처(Origin) 설정
        // https://api.example.com 에서 오는 요청만 허용
        configuration.setAllowedOrigins(List.of("https://api.example.com"));

        configuration.setAllowedOriginPatterns(List.of(
            "https://*.example.com",     // example.com의 모든 서브도메인 허용
            "https://*.example.*.com",   // 더 복잡한 패턴 매칭도 가능
            "http://localhost:[*]"       // 로컬호스트의 모든 포트 허용
        ));

        // 허용할 HTTP 메서드 설정
        // GET과 POST 메서드만 허용 (PUT, DELETE, PATCH 등은 차단됨)
        configuration.setAllowedMethods(List.of("GET","POST"));

        // 허용할 헤더 설정
        // 모두 허용
        configuration.setAllowedHeaders(List.of("*"));

        // 클라이언트에게 노출할 헤더
        configuration.setExposedHeaders(List.of("Authorization"));

        // allowCredentials를 true로 설정할 경우, allowedOrigins에 "*"를 사용할 수 없습니다
        configuration.setAllowCredentials(true);

        // CORS 프리플라이트 요청의 캐시 시간
        configuration.setMaxAge(3600L);

        // URL 패턴별로 CORS 설정을 적용할 수 있는 객체 생성
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        // 모든 경로("/**")에 대해 위에서 설정한 CORS 설정을 적용
        source.registerCorsConfiguration("/**", configuration);


        return source;
    }

 

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {

        http.cors(cors -> cors
            .configurationSource(apiConfigurationSource())
        );
        
        return http.build();
    }

 

6-1. Preflight 란?

configuration.setMaxAge() 에 대해서 간단하게 알아보겠습니다.

  • 브라우저는 실제 요청 전에 OPTIONS 메서드를 사용하여 preflight 요청을 보냅니다
  • 이 요청으로 해당 출처가 안전한지, 어떤 메서드와 헤더가 허용되는지 확인합니다
  • 매 요청마다 preflight를 보내면 성능 저하가 발생할 수 있습니다

6-1-1. maxAge의 역할

// 예시: preflight 응답 헤더
Access-Control-Max-Age: 3600
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type

 

  • 브라우저가 preflight 응답을 캐시하는 시간을 지정
  • 캐시 기간 동안은 동일한 요청에 대해 preflight를 다시 보내지 않음
  • 서버 부하 감소와 성능 향상에 도움

6-1-2. 브라우저별 최대 제한 시간

// 브라우저별 최대 캐시 시간이 다름
Chrome: 2시간 (7200초)
Firefox: 24시간 (86400초)
Safari: 7일

 

 

6-1-3. 장점

  • 서버 부하 감소
  • 네트워크 트래픽 감소
  • 응답 시간 개선

6-1-4. 단점

  • CORS 정책 변경 시 캐시된 정책이 즉시 적용되지 않을 수 있음
  • 브라우저마다 다른 최대 제한으로 일관성 있는 동작을 보장하기 어려움

 

 

6-2. allowCredentials와 allowedOrigins 설정

allowCredentials를 true로 설정하고 allowedOrigins에 "*"를 함께 사용할 수 없는 것은 중요한 보안상의 이유 때문입니다.

 

credentials에는 쿠키, HTTP 인증 토큰과 같은 민감한 인증 정보가 포함됩니다. 그런데 allowedOrigins에 모든 도메인("*")의 접근을 허용한다면 악의적인 웹사이트에서 사용자의 인증정보를 이용해 요청을 보낼 수 있게 됩니다.

 

 

[CS][Spring Security] CSRF란?

CSRF란?CSRF Cross-Site Request Forgery의 약자로 인증된 사용자의 권한을 악용하여 해당 사용자가 의도하지 않은 요청을 웹사이트에 전송하는 공격 기법입니다. 공격자는 사용자가 이미 인증된 상태를

tmd8633.tistory.com

이에 대해서는 CSRF에 대해서 읽어보시기 바랍니다.

 

따라서 브라우저는 이러한 보안 위험을 방지하기 위해 allowCredentials(true)와 allowedOrigins("*")의 조합을 명시적으로 금지하고 있습니다. 이는 웹 보안의 기본 원칙인 "최소 권한의 원칙"을 따르는 것이며, 실수로 인한 보안 취약점 발생을 방지합니다.

 

 

 

7. 자주 발생하는 CORS 에러와 해결 방법

7-1. No 'Access-Control-Allow-Origin' header is present

  • 원인: 서버에서 Access-Control-Allow-Origin 헤더를 설정하지 않음
  • 해결: 서버에서 적절한 CORS 설정 추가

 

7-2. Method not allowed

  • 원인: 허용되지 않은 HTTP 메서드 사용
  • 해결: allowedMethods에 필요한 메서드 추가

 

7-3. Credentials flag is true, but Access-Control-Allow-Credentials is false

  • 원인: 인증 정보를 포함한 요청에 대한 서버 설정 미비
  • 해결: allowCredentials(true) 설정 추가

 

8. 보안 관련 고려사항

8-1. Origin 설정

  • "*" 대신 구체적인 도메인 지정
  • 신뢰할 수 있는 출처만 허용

8-2. 인증 관련

  • allowCredentials(true) 사용 시 구체적인 출처 지정 필요
  • 보안에 민감한 API의 경우 더 엄격한 CORS 정책 적용

8-3. 헤더 설정

  • 필요한 헤더만 허용
  • exposedHeaders 설정 시 최소한의 헤더만 노출

8-4. 캐시 설정

  • maxAge 값을 적절히 설정하여 불필요한 프리플라이트 요청 감소

 

9. 결론

CORS는 현대 웹 개발에서 필수적인 보안 메커니즘입니다. 올바른 CORS 설정은 웹 애플리케이션의 보안과 기능성을 모두 만족시킬 수 있습니다. 각 프로젝트의 요구사항과 보안 정책에 맞게 적절한 CORS 설정을 적용하시기 바랍니다.

'일반 > CS' 카테고리의 다른 글

[CS][Spring Security] CSRF란?  (0) 2024.12.12
Maven Central Repository에 라이브러리 등록하기  (0) 2024.09.19
[CS] MVC 패턴  (0) 2024.04.17
HTTP GET과 POST 차이  (0) 2024.01.25
URI와 URL의 차이점 (Feat : URN)  (0) 2024.01.21

CSRF란?

CSRF Cross-Site Request Forgery의 약자로 인증된 사용자의 권한을 악용하여 해당 사용자가 의도하지 않은 요청을 웹사이트에 전송하는 공격 기법입니다. 공격자는 사용자가 이미 인증된 상태를 악용하여 사용자의 의도와는 무관한 작업을 수행하게 만듭니다. 다시 말해 인증된 요청과 위조된 요청을 구분하지 못하고 서버에서 요청을 처리하여 문제가 생기는 것을 말하는데요. 웹 개발자라면 반드시 알아야하는 부분입니다. 오늘은 이것에 대해서 알아보도록 하겠습니다.

 

 

 

CSRF 공격

CSRF 공격에 대해서 예시와 함께 알아보도록하겠습니다.

 

예시로 은행 웹사이트에서 로그인한 사용자로부터 돈을 이체할 수 있는 Form이 있다고 가정하겠습니다.

<form method="post" action="/transfer">
    <input type="text" name="amount"/>
    <input type="text" name="account"/>
    <input type="submit" value="Transfer"/>
</form>

HTTP 요청은 다음과 같습니다.

POST /transfer HTTP/1.1
Host: bank.jours.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded

amount=10000&account=7654

 

 

다음은 은행 웹사이트에서 로그아웃하지 않고 위조된 웹사이트를 방문한다고 가정하겠습니다. 그 웹사이트에는 다음과 같은 HTML이 있습니다.

<form method="post" action="https://bank.jours.com/transfer">
    <input type="hidden" name="amount" value="100000"/>
    <input type="hidden" name="account" value="1234"/> <!-- 공격자 계좌번호 -->
    <input type="submit" value="Win Money!"/>
</form>

 

위조된 웹사이트에서 submit을 하면 공격자 계좌로 송금이 될겁니다. 이는 위조된 웹사이트가 사용자의 쿠키를 볼 수 없지만 은행과 관련된 쿠키는 여전히 남아 요청과 함께 전송되기 때문에 발생합니다. 더욱 큰 문제는 버튼을 클릭해 submit 하지 않아도 JavaScript를 사용하여 자동화하여 제출할 수 있다는 것입니다. 그렇다면 어떻게 이 문제를 해결할 수 있을까요?

 

 

CSRF 방어

 

읽기전용 메소드

CSRF를 방어하기위해서는 읽기전용 메소드가 선행되어야 합니다.

HTTP Method중 GET, HEAD, OPTIONS, TRACE 메소드는 반드시 읽기전용 메소드가 되어야합니다.

// 잘못된 예시 - GET으로 데이터 변경
@GetMapping("/user/delete/{id}")  // ❌ 절대 하면 안 됨
public void deleteUser(@PathVariable Long id) {
    userService.deleteUser(id);
}

// 올바른 예시 - POST로 데이터 변경
@PostMapping("/user/delete/{id}")  // ✅ 올바른 방법
public void deleteUser(@PathVariable Long id) {
    userService.deleteUser(id);
}

 

 

1. Synchrozier Token Pattern

form 안에 CSRF 토큰을 넣어주는겁니다. 그러면 서버는 토큰을 조회하여 값이 일치하지 않으면 요청을 거부할 수 있게됩니다. 핵심은 쿠키는 브라우저에서 자동으로 HTTP 요청에 포함되지만 CSRF 토큰이 브라우저에서 자동으로 포함되지 않는다는 것입니다.

 

<form method="post" action="/transfer">
    <input type="text" name="amount"/>
    <input type="text" name="account"/>
    <input type="hidden" name="_csrf" value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
    <input type="submit" value="Transfer"/>
</form>

 

HTTP 요청은 다음과 같습니다.

POST /transfer HTTP/1.1
Host: bank.jours.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded

amount=100000&account=7654&_csrf=4bfd1575-3ad1-4d21-96c7-4ef2d9f86721

 

<!-- Thymeleaf 에서 @{} 를 사용하면 자동으로 CSRF 토큰이 포함됨 -->
<form th:action="@{/login}" method="post">

 

 

SameSite

쿠키에 SameSite 속성을 지정하는 것입니다. 서버는 SameSite 속성을 지정하여 외부 사이트에서 오는 쿠키를 보낼지 여부를 정할 수 있습니다.

 

 

설정값 설명
Strict
  • 가장 엄격한 설정
  • 같은 도메인의 요청에서만 쿠키 전송
  • 외부 사이트에서의 모든 요청에 쿠키를 보내지 않음
  • 보안성은 가장 높지만, 사용자 경험을 해칠 수 있음
Lax
  • TOP-level 네비게이션(주소창에 직접 입력, <a> 태그 클릭)에서는 쿠키 전송 허용
  • POST, PUT, DELETE 등의 요청에서는 쿠키 전송 제한
  • Strict보다 유연하면서도 기본적인 보안 제공
None
  • 모든 크로스 사이트 요청에 쿠키 전송 허용
  • 반드시 Secure 플래그와 함께 사용해야 함
  • HTTPS가 필수

 

 

# application.properties
# strict, lax, none 중 설정
server.servlet.session.cookie.same-site=strict

 

Spring Boot 2.6.0 이상에서는 SameSite=Lax 가 Default입니다.

 

SameSite 주의점

  • 브라우저가 SameSite를 지원하지 않을 수 있습니다. 예전 브라우저를 사용한다면 SameSite가 지원되지 않을 수 있습니다.
  • strict 설정 시  social.jours.com - email.jours.com 과의 쿠키는 전송되지않습니다.

 

 

 

REST API의 CSRF 설정

출처 : https://docs.spring.io/spring-security/reference/features/exploits/csrf.html#csrf-protection-read-only

 

REST API에서는 CSRF를 disabled 해도 괜찮다고합니다. 왜냐하면 API 요청시 인증정보(Jwt, OAuth2, Client Key 등)를 포함하여 전송하기 때문에 불필요하게 CSRF 인증정보를 저장하지 않아도 되는 것입니다.

 

 

 

Spring Security 에서의 CSRF 설정

Spring Security 에서 CSRF를 설정할 수 있습니다. 기본적으로 CSRF 토큰을 지원합니다.

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {

        http.csrf(Customizer.withDefaults());

        return http.build();
    }

 

csrf 설정 메소드

http.csrf(csrf -> {
    csrf
        // CSRF 완전 비활성화
        .disable()                     
        
        // 특정 경로 CSRF 검증 제외
        .ignoringAntMatchers("/api/**")  
        
        // RequestMatcher로 더 복잡한 조건으로 제외할 경로 설정
        .ignoringRequestMatchers(requestMatcher)  
        
        // CSRF 토큰 저장소 설정
        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
        
        // 커스텀 CSRF 토큰 저장소 설정
        .csrfTokenRepository(new CustomCsrfTokenRepository())
        
        // CSRF 토큰 생성 요청 처리 경로 설정 (기본값: "_csrf")
        .csrfTokenRequestHandler(requestHandler)
        
        // 세션 속성 이름 설정 (기본값: "CSRF_TOKEN")
        .sessionAuthenticationStrategy(sessionAuthenticationStrategy)
        
        // CSRF 토큰 필터 이전에 실행될 필터 추가
        .requireCsrfProtectionMatcher(requestMatcher)
});
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) 는 JavaScript에서 CSRF 토큰을 사용 할 수 있도록 쿠키에 저장하는 것인데, 위에서 설명했듯이 API는 csrf.disabled() 해서 사용하는 것이 더 유용할 수 있습니다.

 

 

결론

CSRF 공격은 웹 애플리케이션의 중요한 보안 위협이지만, 적절한 방어 메커니즘을 구현함으로써 효과적으로 방어할 수 있습니다. 특히 CSRF 토큰, SameSite 쿠키 설정, 그리고 적절한 헤더 검증을 조합하여 사용하는 것이 권장됩니다.

'일반 > CS' 카테고리의 다른 글

[CS][Spring Security] CORS에 대해서 알아보자  (0) 2024.12.16
Maven Central Repository에 라이브러리 등록하기  (0) 2024.09.19
[CS] MVC 패턴  (0) 2024.04.17
HTTP GET과 POST 차이  (0) 2024.01.25
URI와 URL의 차이점 (Feat : URN)  (0) 2024.01.21

+ Recent posts