토리맘의 한글라이즈 프로젝트 logo 토리맘의 한글라이즈 프로젝트

스프링 시큐리티 공식 레퍼런스를 한글로 번역한 문서입니다.

전체 목차는 여기에 있습니다.

목차:


24.1. Cross Site Request Forgery (CSRF) for WebFlux Environments

이번 섹션에선 웹플럭스 환경에서 스프링 시큐리티로 사이트간 요청 위조(Cross Site Request Forgery, CSRF)를 방어하는 방법을 설명한다.

24.1.1. Using Spring Security CSRF Protection

스프링 시큐리티로 CSRF를 방어하는 방법은 크게 다음 단계로 나뉜다:

Use proper HTTP verbs

CSRF 공격을 방어하려면 가장 먼저 만드려는 웹사이트에서 올바른 HTTP verb를 사용해야 한다. 자세한 내용은 Safe Methods Must be Idempotent에서 다루고 있다

Configure CSRF Protection

그다음엔 어플리케이션에 스프링 시큐리티의 CSRF 방어 기능을 설정해야 한다. 스프링 시큐리티에선 CSRF 방어를 기본적으로 활성화하지만, 아마 일부 설정을 커스텀해야 할 거다. 아래는 일반적으로 커스텀하는 설정들이다.

Custom CsrfTokenRepository

기본적으로 스프링 시큐리티는 WebSessionServerCsrfTokenRepository로 CSRF 토큰을 WebSession에 저장한다. 하지만 커스텀 ServerCsrfTokenRepository를 설정하고 싶을 때도 있을 것이다. 예를 들어 자바스크립트 기반 어플리케이션을 지원하려면 쿠키에 CsrfToken을 저장해야 한다.

기본적으로 CookieServerCsrfTokenRepositoryXSRF-TOKEN이란 쿠키를 저장하고, X-XSRF-TOKEN 헤더나 _csrf 파라미터에서 토큰을 읽어들인다. 이 디폴트 값들은 AngularJS에서 따온 값이다.

자바 설정으로 CookieCsrfTokenRepository를 설정하는 방법은 다음과 같다:

Example 166. Store CSRF Token in a Cookie with Java Configuration

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .csrf(csrf -> csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()))
    return http.build();
}

이 샘플에선 cookieHttpOnly=false를 명시했다. 자바스크립트(i.e. AngularJS)에서 읽어가려면 이렇게 명시해야 한다. 자바스크립트에서 직접 쿠키를 읽어야할 필요가 없다면 보안을 위해 cookieHttpOnly=false를 생략하는 게 좋다 (이대신 new CookieServerCsrfTokenRepository()를 사용해서).

Disable CSRF Protection

CSRF 방어는 기본적으로 활성화된다. 하지만 굳이 CSRF를 방어할 필요가 없는 어플리케이션을 만든다면 간단히 비활성화할 수 있다.

아래 자바 설정은 CSRF 방어를 비활성화한다.

Example 167. Disable CSRF Java Configuration

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .csrf(csrf -> csrf.disable()))
    return http.build();
}

Include the CSRF Token

동기화 토큰 패턴으로 CSRF 공격을 방어하려면 HTTP 요청에 실제 CSRF 토큰을 추가해야 한다. 토큰은 반드시 폼 파라미터, HTTP 헤더같이 브라우저가 HTTP 요청에 자동으로 추가하지 않는 곳에 넣어야 한다.

스프링 시큐리티의 CsrfWebFilterServerWebExchange 속성에 org.springframework.security.web.server.csrf.CsrfToken이란 이름으로 Mono를 추가한다. 따라서 어떻게 뷰를 렌더링하든지, 이나 메타 태그에서 Mono<CsrfToken>으로 토큰에 접근할 수 있다.

사용하는 뷰 기술이 Mono<CsrfToken>을 간단히 구독하는 방법을 제공하지 않을 때 사용하는 일반적인 패턴은, 스프링의 @ControllerAdvice를 써서 CsrfToken을 직접 노출하는 것이다. 예를 들어 다음 코드는 스프링 시큐리티의 CsrfRequestDataValueProcessor에서 사용하는 디폴트 속성 이름(_csrf)에 CsrfToken을 담아, hidden 인풋에 CSRF 토큰이 자동으로 추가되게 만든다.

Example 168. CsrfToken as @ModelAttribute

@ControllerAdvice
public class SecurityControllerAdvice {
    @ModelAttribute
    Mono<CsrfToken> csrfToken(ServerWebExchange exchange) {
        Mono<CsrfToken> csrfToken = exchange.getAttribute(CsrfToken.class.getName());
        return csrfToken.doOnSuccess(token -> exchange.getAttributes()
                .put(CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME, token));
    }
}

다행히 타임리프는 통합 기능을 제공하므로 설정을 추가하지 않아도 된다.

Form URL Encoded

HTML 폼을 사용한다면 hidden 인풋에 CSRF 토큰을 추가해야 한다. 예를 들어 다음과 같은 HTML을 렌더링할 수 있다:

Example 169. CSRF Token HTML

<input type="hidden"
    name="_csrf"
    value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>

이제부터는 CSRF 토큰을 hidden 인풋에 추가하는 여러 가지 방법을 설명한다.

Automatic CSRF Token Inclusion

스프링 시큐리티의 CSRF 기능은 CsrfRequestDataValueProcessor로 스프링의 RequestDataValueProcessor와 통합할 수 있다. CsrfRequestDataValueProcessor를 사용하려면, Mono<CsrfToken>을 구독하고 CsrfTokenDEFAULT_CSRF_ATTR_NAME과 동일한 이름으로 속성에 추가해야 한다.

타임리프는 이를 위한 보일러플레이트를 모두 지원하며, RequestDataValueProcessor를 통합해 unsafe HTTP 메소드(i.e. post)를 사용하는 폼은 자동으로 실제 CSRF 토큰을 추가해 준다.

CsrfToken Request Attribute

다른 방법으로 요청에 실제 CSRF 토큰을 추가할 수 없다면, ServerWebExchangeorg.springframework.security.web.server.csrf.CsrfToken 속성에 Mono<CsrfToken>들어있다는 점을 활용하면 된다.

아래 타임리프 예제는 _csrf라는 이름의 속성에 CsrfToken추가했다고 가정한다.

Example 170. CSRF Token in Form with Request Attribute

<form th:action="@{/logout}"
    method="post">
<input type="submit"
    value="Log out" />
<input type="hidden"
    th:name="${_csrf.parameterName}"
    th:value="${_csrf.token}"/>
</form>
Ajax and JSON Requests

JSON을 사용한다면 HTTP 파라미터로 CSRF 토큰을 제출할 수 없다. 대신 HTTP 헤더를 활용할 수 있다.

아래 섹션에선 자바스크립트 기반 어플리케이션에서 HTTP 요청 헤더에 CSRF 토큰을 추가하는 여러 가지 방법을 설명한다.

Automatic Inclusion

간단하게 CSRF 토큰을 쿠키에 저장하도록 설정할 수 있다. CSRF 토큰을 쿠키에 저장하면 AngularJS같은 자바스크립트 프레임워크는 자동으로 HTTP 요청 헤더에 실제 CSRF 토큰을 추가한다.

Meta tags

쿠키에 CSRF 토큰을 추가하는 대신 사용할 수 있는 또 다른 패턴은 meta 태그를 사용하는 것이다. 다음과 같은 HTML이 있다면:

Example 171. CSRF meta tag HTML

<html>
<head>
    <meta name="_csrf" content="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
    <meta name="_csrf_header" content="X-CSRF-TOKEN"/>
    <!-- ... -->
</head>
<!-- ... -->

메타 태그에 CSRF 토큰을 추가하고 나면 자바스크립트 코드에서 이 메타 태그를 읽어 헤더에 CSRF 토큰을 추가할 수 있다. jQuery를 사용한다면 다음과 같이 작성할 수 있다:

Example 172. AJAX send CSRF Token

$(function () {
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    $(document).ajaxSend(function(e, xhr, options) {
        xhr.setRequestHeader(header, token);
    });
});

아래 타임리프 예제는 _csrf라는 이름의 속성에 CsrfToken추가했다고 가정한다.

Example 173. CSRF meta tag JSP

<html>
<head>
    <meta name="_csrf" th:content="${_csrf.token}"/>
    <!-- default header name is X-CSRF-TOKEN -->
    <meta name="_csrf_header" th:content="${_csrf.headerName}"/>
    <!-- ... -->
</head>
<!-- ... -->

24.1.2. CSRF Considerations

CSRF 공격을 방어하려면 특별히 고려해야 할 몇 가지가 있다. 이번 섹션에선 웹플럭스 환경에서 주의할 사항을 설명한다. 일반적인 주의사항은 CSRF Considerations를 참고하라.

Logging In

로그인을 위조하지 못하게 막으려면 로그인 요청을 CSRF 공격으로부터 보호해야 한다. 스프링 시큐리티는 웹플럭스에서 CSRF 로그인 공격을 방어할 수 있는 기능을 지원한다.

Logging Out

로그아웃을 위조하지 못하게 막으려면 로그아웃 요청을 CSRF 공격으로부터 보호해야 한다. 기본적으로 스프링 시큐리티의 LogoutWebFilter는 HTTP POST 요청만 처리한다. CSRF 토큰을 검사하므로 악의적으로 다른 사용자의 로그아웃을 위조하지 못한다.

로그아웃을 구현하는 가장 쉬운 방법은 폼을 사용하는 것이다. 링크가 꼭 있어야 한다면 자바스크립트로 POST 요청을 수행하는 링크를 만들면 된다 (hidden 폼으로). 자바스크립트가 비활성화된 브라우저에선 POST 요청을 수행하는 로그아웃 컨펌 페이지로 사용자를 이동시키는 링크를 만들 수 있다.

로그아웃에서 HTTP GET을 꼭 써야 한다면 그럴 순 있지만, 일반적으로 권장하는 방법이 아니란 점을 알아둬라. 예를 들어 아래 자바 설정은 /logout URL의 모든 HTTP 메소드 요청에서 로그아웃을 수행한다.

Example 174. Log out with HTTP GET

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .logout(logout -> logout.requiresLogout(new PathPatternParserServerWebExchangeMatcher("/logout")))
    return http.build();
}

CSRF and Session Timeouts

기본적으로 스프링 시큐리티에선 CSRF 토큰을 WebSession에 저장한다. 따라서 세션이 만료되면 검증할 CSRF 토큰이 없는 상황이 발생할 수 있다.

세션 타임아웃을 해결하는 일반적인 방법은 앞에서 이미 설명했다. 이번에는 웹플럭스에서 CSRF 타임아웃에 대응하는 방법을 설명한다.

간단하게 CSRF 토큰을 쿠키에 저장하도록 바꿀 수 있다. 자세한 내용은 Custom CsrfTokenRepository 섹션을 참고하라.

Multipart (file upload)

CSRF 공격으로부터 멀티파트 요청을 (파일 업로드) 보호하려고 할 땐 닭이 먼저인가, 달걀이 먼저인가하는 문제와 맞닥뜨릴 수 있다고 앞에서 이미 언급했었다. 이번 섹션에선 웹플럭스 어플리케이션에서 bodyurl에 CSRF 토큰을 추가하는 방법을 설명한다.

스프링에서 멀티파트 폼을 사용하는 방법은 스프링 레퍼런스의 Multipart Data 섹션에서 자세히 설명한다.

Place CSRF Token in the Body

CSRF 토큰을 바디에 두면 생길 수 있는 상황은 앞에서 이미 설명했다.

웹플럭스 어플리케이션에선 다음과 같이 설정할 수 있다:

Example 175. Enable obtaining CSRF token from multipart/form-data

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .csrf(csrf -> csrf.tokenFromMultipartDataEnabled(true))
    return http.build();
}
Include CSRF Token in URL

CSRF 토큰을 URL에 두면 생길 수 있는 상황은 앞에서 이미 설명했다. ServerHttpRequest 요청 속성CsrfToken이 들어있으므로, 이를 사용해서 action을 만들 수 있다. 다음은 타임리프를 사용한 예시이다.

Example 176. CSRF Token in Action

<form method="post"
    th:action="@{/upload(${_csrf.parameterName}=${_csrf.token})}"
    enctype="multipart/form-data">

HiddenHttpMethodFilter

HTTP 메소드 재정의는 앞에서 이미 설명했다.

스프링 웹플럭스 어플리케이션에선 HiddenHttpMethodFilter를 사용해서 HTTP 메소드를 재정의한다.


24.2. Security HTTP Response Headers

보안 HTTP 응답 헤더는 웹 어플리케이션을 보호할 때 사용하는 헤더다. 이번 섹션에선 웹플럭스 기반 어플리케이션에서 사용할 수 있는 보안 HTTP 응답 헤더를 설명한다.

24.2.1. Default Security Headers

스프링 시큐리티는 기본적인 보안을 위한 HTTP 응답 헤더의 디폴트 셋을 제공한다. 모든 헤더가 다 좋은 관행이라고는 하지만, 그렇다고 해서 모든 클라이언트가 다 헤더를 사용하는 것은 아니므로 추가적인 테스트가 필요하다.

특정 헤더를 커스텀할 수도 있다. 예를 들어 X-Frame-Options에선 SAMEORIGIN을 지정하돼, 다른 헤더는 모두 디폴트 값을 사용하고 싶을 수도 있다.

아래처럼 자바 설정으로 쉽게 변경할 수 있다:

Example 177. Customize Default Security Headers with Java Configuration

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .headers(headers -> headers
            .frameOptions(frameOptions -> frameOptions
                .mode(Mode.SAMEORIGIN)
            )
        );
    return http.build();
}

디폴트 값을 사용하는 대신 사용할 헤더만 명시하고 싶다면 디폴트 설정을 비활성화할 수 있다. 다음은 자바 설정을 사용한 예시이다:

Example 178. Disable HTTP Security Response Headers

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .headers(headers -> headers.disable());
    return http.build();
}

24.2.2. Cache Control

스프링 시큐리티는 디폴트로 Cache Control 헤더를 추가한다.

하지만 특정 응답을 캐시하고 싶다면 어플리케이션에서 원하는 곳에서 ServerHttpResponse에 헤더를 추가해 스프링 시큐리티의 헤더 셋을 재정의하면 된다. CSS, 자바스크립트, 이미지 등을 적절히 캐시해야 할 때 유용하다.

스프링 웹플럭스를 사용한다면 설정으로 캐시를 관리할 수 있다. 자세한 방법은 스프링 레퍼런스 문서의 스태틱 리소스 섹션을 참고해라.

필요하다면 스프링 시큐리티의 cache control HTTP 응답 헤더를 비활성화할 수 있다.

Example 179. Cache Control Disabled

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .headers(headers -> headers
            .cache(cache -> cache.disable())
        );
    return http.build();
}

24.2.3. Content Type Options

스프링 시큐리티는 디폴트로 Content-Type 헤더를 추가한다. 하지만 자바 설정으로 비활성화할 수 있다:

Example 180. Content Type Options Disabled with Java Configuration

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .headers(headers -> headers
            .contentTypeOptions(contentTypeOptions -> contentTypeOptions.disable())
        );
    return http.build();
}

24.2.4. HTTP Strict Transport Security (HSTS)

스프링 시큐리티는 디폴트로 Strict Transport Security 헤더를 추가한다. 하지만 헤더 내용을 직접 커스텀할 수도 있다. 예를 들어 다음 예제는 자바 설정으로 사용할 HSTS를 지정한다:

Example 181. Strict Transport Security with Java Configuration

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .headers(headers -> headers
            .hsts(hsts -> hsts
                .includeSubdomains(true)
                .preload(true)
                .maxAge(Duration.ofDays(365))
            )
        );
    return http.build();
}

24.2.5. X-Frame-Options

스프링 시큐리티는 기본적으로 X-Frame-Options를 사용해서 아이프레임 내부 렌더링을 비활성화한다.

다음 코드는 자바 설정으로 프레임 옵션을 SAMEORIGIN으로 커스텀한다:

Example 182. X-Frame-Options: SAMEORIGIN

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .headers(headers -> headers
            .frameOptions(frameOptions -> frameOptions
                .mode(SAMEORIGIN)
            )
        );
    return http.build();
}

24.2.6. X-XSS-Protection

스프링 시큐리티는 기본적으로 X-XSS-Protection 헤더를 사용해서 브라우저에게 reflected XSS 공격을 막도록 지시한다. 다음 자바 설정으로 X-XSS-Protection을 비활성화할 수도 있다:

Example 183. X-XSS-Protection Customization

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .headers(headers -> headers
            .xssProtection(xssProtection -> xssProtection.disable())
        );
    return http.build();
}

24.2.7. Content Security Policy (CSP)

컨텐츠 보안 정책은 어플리케이션에 따라 다르므로 디폴트라는 게 불가능하며, 스프링 시큐리티는 디폴트로 관련 헤더를 추가하지 않는다. 보안 정책을 운영하거나 보호 중인 리소스를 모니터링하려면 어플리케이션에서 직접 지정해야 한다.

예를 들어 다음과 같은 보안 정책을 사용한다면:

Example 184. Content Security Policy Example

Content-Security-Policy: script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/

아래에 있는 자바 설정으로 CSP 헤더를 활성화할수 있다:

Example 185. Content Security Policy

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .headers(headers -> headers
            .contentSecurityPolicy(policy -> policy
                .policyDirectives("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/")
            )
        );
    return http.build();
}

CSP report-only 헤더를 사용하려면 아래 자바 설정을 사용해라:

Example 186. Content Security Policy Report Only

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .headers(headers -> headers
            .contentSecurityPolicy(policy -> policy
                .policyDirectives("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/")
                .reportOnly()
            )
        );
    return http.build();
}

24.2.8. Referrer Policy

스프링 시큐리티는 디폴트로 Referrer Policy 헤더를 추가하지 않는다. 아래와 같은 자바 설정으로 Referrer Policy 헤더를 추가할 수 있다:

Example 187. Referrer Policy Java Configuration

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .headers(headers -> headers
            .referrerPolicy(referrer -> referrer
                .policy(ReferrerPolicy.SAME_ORIGIN)
            )
        );
    return http.build();
}

24.2.9. Feature Policy

스프링 시큐리티는 디폴트로 Feature Policy 헤더를 추가하지 않는다. 다음 Feature-Policy 헤더는:

Example 188. Feature-Policy Example

Feature-Policy: geolocation 'self'

아래 자바 설정으로 활성화할 수 있다:

Example 189. Feature-Policy Java Configuration

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .headers(headers -> headers
            .featurePolicy("geolocation 'self'")
        );
    return http.build();
}

24.2.10. Clear Site Data

스프링 시큐리티는 디폴트로 Clear-Site-Data 헤더를 추가하지 않는다. 아래 Clear-Site-Data 헤더는:

Example 190. Clear-Site-Data Example

Clear-Site-Data: "cache", "cookies"

아래 자바 설정을 사용해서 로그아웃 시 전송할 수 있다:

Example 191. Clear-Site-Data Java Configuration

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    ServerLogoutHandler securityContext = new SecurityContextServerLogoutHandler();
    ClearSiteDataServerHttpHeadersWriter writer = new ClearSiteDataServerHttpHeadersWriter(CACHE, COOKIES);
    ServerLogoutHandler clearSiteData = new HeaderWriterServerLogoutHandler(writer);
    DelegatingServerLogoutHandler logoutHandler = new DelegatingServerLogoutHandler(securityContext, clearSiteData);

    http
        // ...
        .logout()
            .logoutHandler(logoutHandler);
    return http.build();
}

24.3. HTTP

모든 HTTP 통신은 TLS로 보호해야 한다.

아래에선 웹플럭스 어플리케이션에서 HTTPS를 사용할 때 활용할만한 기능을 설명한다.

24.3.1. Redirect to HTTPS

스프링 시큐리티는 클라이언트가 HTTPS가 아닌 HTTP로 보낸 요청을 HTTPS로 리다이렉트하게 설정할 수 있다.

예를 들어 아래 자바 설정은 모든 HTTP 요청을 HTTPS로 리다이렉트한다:

Example 192. Redirect to HTTPS

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .redirectToHttps(withDefaults());
    return http.build();
}

이 설정은 if문으로 감싸서 프로덕션 레벨에서만 활성화하도록 만들 수 있다. 또는 프로덕션에서만 사용하는 요청 프로퍼티를 찾아 활성화할 수도 있다. 예를 들어 프로덕션 환경에서 X-Forwarded-Proto라는 헤더를 추가한다면 아래 자바 코드를 사용할 수 있다:

Example 193. Redirect to HTTPS when X-Forwarded

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .redirectToHttps(redirect -> redirect
            .httpsRedirectWhen(e -> e.getRequest().getHeaders().containsKey("X-Forwarded-Proto"))
        );
    return http.build();
}

24.3.2. Strict Transport Security

스프링 시큐리티는 Strict Transport Security를 지원하며 디폴트로 활성화돼 있다.

24.3.3. Proxy Server Configuration

스프링 시큐리티는 프록시 서버와의 통합을 지원한다.


전체 목차는 여기에 있습니다.

<< >>