Skip to main content

03E. μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°

2023λ…„ 12μ›” 27일About 2 minJavaSpringAWScrashcoursejavajdkjdk8streamspringspringframeworkspringbootawsaws-ec2

03E. μŠ€ν”„λ§ μ‹œνλ¦¬ν‹° κ΄€λ ¨


3-05. μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°

점프 투 μŠ€ν”„λ§λΆ€νŠΈ - WikiDocs

pahkey/sbb3 - 3-05

μŠ€ν”„λ§λΆ€νŠΈλŠ” νšŒμ›κ°€μž…κ³Ό λ‘œκ·ΈμΈμ„ λ„μ™€μ£ΌλŠ” μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°(Spring Security)λ₯Ό μ‚¬μš©ν• μˆ˜ μžˆλ‹€. SBB도 μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°λ₯Ό μ‚¬μš©ν•˜μ—¬ νšŒμ›κ°€μž…κ³Ό 둜그인 κΈ°λŠ₯을 λ§Œλ“€ 것이닀. κ·Έ 전에 μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°μ— λŒ€ν•΄μ„œ κ°„λ‹¨ν•˜κ²Œ μ•Œμ•„λ³΄κ³  ν•„μš”ν•œ 섀정도 μ§„ν–‰ν•΄ 보자.


μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°λž€?

μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°λŠ” μŠ€ν”„λ§ 기반 μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ 인증과 κΆŒν•œμ„ λ‹΄λ‹Ήν•˜λŠ” μŠ€ν”„λ§μ˜ ν•˜μœ„ ν”„λ ˆμž„μ›Œν¬μ΄λ‹€.


μŠ€ν”„λ§ μ‹œνλ¦¬ν‹° μ„€μΉ˜

μŠ€ν”„λ§ μ‹œνλ¦¬ν‹° μ‚¬μš©μ„ μœ„ν•΄ λ‹€μŒκ³Ό 같이 build.gradle νŒŒμΌμ„ μˆ˜μ •ν•˜μž.

파일λͺ…: /sbb/build.gradle

// (... μƒλž΅ ...)

dependencies {
    // (... μƒλž΅ ...)
    implementation "org.springframework.boot:spring-boot-starter-security"
    implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE"
}

// (... μƒλž΅ ...)

μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°μ™€ 이와 κ΄€λ ¨λœ νƒ€μž„λ¦¬ν”„ 라이브러리λ₯Ό μ‚¬μš©ν•˜λ„λ‘ μ„€μ •ν–ˆλ‹€. "[Refresh Gradle Project]"λ₯Ό μˆ˜ν–‰ν•˜μ—¬ ν•„μš”ν•œ 라이브러리λ₯Ό μ„€μΉ˜ν•œ ν›„ λ‘œμ»¬μ„œλ²„λ„ μž¬μ‹œμž‘ ν•˜μž.

thymeleaf-extras-springsecurity6

thymeleaf-extras-springsecurity6 νŒ¨ν‚€μ§€λ₯Ό μ‚¬μš©ν•˜κΈ° μœ„ν•΄ 뒀에 3.1.1.RELEASEκ³Ό 같은 버전 정보λ₯Ό μΆ”κ°€ν–ˆλ‹€. thymeleaf-extras-springsecurity6 νŒ¨ν‚€μ§€λŠ” μŠ€ν”„λ§λΆ€νŠΈκ°€ 자체적으둜 κ΄€λ¦¬ν•˜λŠ” νŒ¨ν‚€μ§€μ΄κΈ° λ•Œλ¬Έμ— 버전 정보가 ν•„μš”μ—†μ§€λ§Œ ν˜„μž¬ μ‚¬μš©μ€‘μΈ μŠ€ν”„λ§λΆ€νŠΈ 버전인 3.0.0 λ²„μ „μ—μ„œλŠ” μœ„μ™€ 같은 버전 정보λ₯Ό μž…λ ₯ν•˜μ§€ μ•ŠμœΌλ©΄ 였λ₯˜κ°€ λ°œμƒν•œλ‹€. λ§Œμ•½ 버전 정보λ₯Ό μ œκ±°ν•˜κ³  μ‚¬μš©ν•˜λ”λΌλ„ 였λ₯˜κ°€ μ—†λ‹€λ©΄ 버전 정보 없이 μ‚¬μš©ν•˜κΈ° λ°”λž€λ‹€.


μŠ€ν”„λ§ μ‹œνλ¦¬ν‹° μ„€μ •

μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°λ₯Ό μ„€μΉ˜ν•˜κ³  λ‘œμ»¬μ„œλ²„λ₯Ό μž¬μ‹œμž‘ν•œ 후에 SBB의 질문 λͺ©λ‘ 화면에 접속해 보자. μ•„λ§ˆλ„ λ‹€μŒκ³Ό 같은 둜그인 화면이 λ‚˜νƒ€λ‚˜μ„œ 깜짝 λ†€λž„ 것이닀.

05_1
05_1

μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°λŠ” 기본적으둜 μΈμ¦λ˜μ§€ μ•Šμ€ μ‚¬μš©μžλŠ” μ„œλΉ„μŠ€λ₯Ό μ‚¬μš©ν•  수 μ—†κ²Œλ” λ˜μ–΄ μžˆλ‹€. λ”°λΌμ„œ 인증을 μœ„ν•œ 둜그인 화면이 λ‚˜νƒ€λ‚˜λŠ” 것이닀. ν•˜μ§€λ§Œ μ΄λŸ¬ν•œ κΈ°λ³Έ κΈ°λŠ₯은 SBB에 κ·ΈλŒ€λ‘œ μ μš©ν•˜κΈ°μ—λŠ” κ³€λž€ν•˜λ―€λ‘œ μ‹œνλ¦¬ν‹°μ˜ 섀정을 톡해 λ°”λ‘œ μž‘μ•„μ•Ό ν•œλ‹€.

SBBλŠ” 둜그인 없이도 κ²Œμ‹œλ¬Όμ„ μ‘°νšŒν•  수 μžˆμ–΄μ•Ό ν•œλ‹€.

λ‹€μŒκ³Ό 같이 SecurityConfig.java νŒŒμΌμ„ μž‘μ„±ν•˜μž.

파일λͺ…: /sbb/src/main/java/com/mysite/sbb/SecurityConfig.java

package com.mysite.sbb;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
                .requestMatchers(new AntPathRequestMatcher("/**")).permitAll())
        ;
        return http.build();
    }
}

@Configuration은 μŠ€ν”„λ§μ˜ ν™˜κ²½μ„€μ • νŒŒμΌμž„μ„ μ˜λ―Έν•˜λŠ” μ• λ„ˆν…Œμ΄μ…˜μ΄λ‹€. μ—¬κΈ°μ„œλŠ” μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°μ˜ 섀정을 μœ„ν•΄ μ‚¬μš©λ˜μ—ˆλ‹€. @EnableWebSecurityλŠ” λͺ¨λ“  μš”μ²­ URL이 μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°μ˜ μ œμ–΄λ₯Ό 받도둝 λ§Œλ“œλŠ” μ• λ„ˆν…Œμ΄μ…˜μ΄λ‹€.

@EnableWebSecurity μ• λ„ˆν…Œμ΄μ…˜μ„ μ‚¬μš©ν•˜λ©΄ λ‚΄λΆ€μ μœΌλ‘œ SpringSecurityFilterChain이 λ™μž‘ν•˜μ—¬ URL ν•„ν„°κ°€ μ μš©λœλ‹€.

μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°μ˜ μ„ΈλΆ€ 섀정은 SecurityFilterChain λΉˆμ„ μƒμ„±ν•˜μ—¬ μ„€μ •ν•  수 μžˆλ‹€. λ‹€μŒ λ¬Έμž₯은 λͺ¨λ“  μΈμ¦λ˜μ§€ μ•Šμ€ μš”μ²­μ„ ν—ˆλ½ν•œλ‹€λŠ” μ˜λ―Έμ΄λ‹€. λ”°λΌμ„œ λ‘œκ·ΈμΈμ„ ν•˜μ§€ μ•Šλ”λΌλ„ λͺ¨λ“  νŽ˜μ΄μ§€μ— μ ‘κ·Όν•  수 μžˆλ‹€.

http
    .authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
        .requestMatchers(new AntPathRequestMatcher("/**")).permitAll())
    ;

μ΄λ ‡κ²Œ μŠ€ν”„λ§ μ‹œνλ¦¬ν‹° μ„€μ • νŒŒμΌμ„ κ΅¬μ„±ν•˜λ©΄ 이제 질문 λͺ©λ‘, 질문 λ‹΅λ³€ λ“±μ˜ κΈ°λŠ₯을 이전과 λ™μΌν•˜κ²Œ μ‚¬μš©ν•  수 μžˆλ‹€.


H2 μ½˜μ†”

ν•˜μ§€λ§Œ μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°λ₯Ό μ μš©ν•˜λ©΄ H2 μ½˜μ†” λ‘œκ·ΈμΈμ‹œ λ‹€μŒκ³Ό 같은 403 Forbidden 였λ₯˜κ°€ λ°œμƒν•œλ‹€.

05_2
05_2

403 Forbidden 였λ₯˜κ°€ λ°œμƒν•˜λŠ” μ΄μœ λŠ” μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°λ₯Ό μ μš©ν•˜λ©΄ CSRF κΈ°λŠ₯이 λ™μž‘ν•˜κΈ° λ•Œλ¬Έμ΄λ‹€.

CSRFλž€?

CSRF(cross site request forgery)λŠ” μ›Ή μ‚¬μ΄νŠΈ 취약점 곡격을 λ°©μ§€λ₯Ό μœ„ν•΄ μ‚¬μš©ν•˜λŠ” κΈ°μˆ μ΄λ‹€. μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°κ°€ CSRF 토큰 값을 μ„Έμ…˜μ„ 톡해 λ°œν–‰ν•˜κ³  μ›Ή νŽ˜μ΄μ§€μ—μ„œλŠ” 폼 μ „μ†‘μ‹œμ— ν•΄λ‹Ή 토큰을 ν•¨κ»˜ μ „μ†‘ν•˜μ—¬ μ‹€μ œ μ›Ή νŽ˜μ΄μ§€μ—μ„œ μž‘μ„±λœ 데이터가 μ „λ‹¬λ˜λŠ”μ§€λ₯Ό κ²€μ¦ν•˜λŠ” κΈ°μˆ μ΄λ‹€.

이 였λ₯˜λ₯Ό ν•΄κ²°ν•˜κΈ° 전에 질문 등둝 화면을 μ—΄κ³  λΈŒλΌμš°μ €μ˜ μ†ŒμŠ€λ³΄κΈ° κΈ°λŠ₯을 μ΄μš©ν•˜μ—¬ 질문 등둝 ν™”λ©΄μ˜ μ†ŒμŠ€λ₯Ό μž μ‹œ 확인해 보자.

05_3
05_3

그러면 λ‹€μŒκ³Ό 같이 질문 등둝 ν™”λ©΄μ˜ μ†ŒμŠ€λ₯Ό λ³Ό 수 μžˆλ‹€.

05_4
05_4

λ‹€μŒκ³Ό 같은 input μ—˜λ¦¬λ¨ΌνŠΈκ°€ 폼(form) νƒœκ·Έ 밑에 μžλ™μœΌλ‘œ μƒμ„±λœ 것을 확인할 수 μžˆλ‹€.

<input type="hidden" name="_csrf" value="0d609fbc-b102-4b3f-aa97-0ab30c8fcfd4"/>

μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°μ— μ˜ν•΄ μœ„μ™€ 같은 CSRF 토큰이 μžλ™μœΌλ‘œ μƒμ„±λœλ‹€. 즉, μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°λŠ” μ΄λ ‡κ²Œ λ°œν–‰ν•œ CSRF ν† ν°μ˜ 값이 μ •ν™•ν•œμ§€ κ²€μ¦ν•˜λŠ” 과정을 κ±°μΉœλ‹€. (λ§Œμ•½ CSRF 값이 μ—†κ±°λ‚˜ 해컀가 μž„μ˜μ˜ CSRF 값을 κ°•μ œλ‘œ λ§Œλ“€μ–΄ μ „μ†‘ν•˜λŠ” μ•…μ˜μ μΈ URL μš”μ²­μ€ μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°μ— μ˜ν•΄ 블둝킹 될 것이닀.)

그런데 H2 μ½˜μ†”μ€ 이와 같은 CSRF 토큰을 λ°œν–‰ν•˜λŠ” κΈ°λŠ₯이 μ—†κΈ° λ•Œλ¬Έμ— μœ„μ™€ 같은 403 였λ₯˜κ°€ λ°œμƒν•˜λŠ” 것이닀.

H2 μ½˜μ†”μ€ μŠ€ν”„λ§κ³Ό μƒκ΄€μ—†λŠ” 일반 μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄λ‹€.

μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°κ°€ CSRF μ²˜λ¦¬μ‹œ H2 μ½˜μ†”μ€ μ˜ˆμ™Έλ‘œ μ²˜λ¦¬ν•  수 μžˆλ„λ‘ λ‹€μŒκ³Ό 같이 μ„€μ • νŒŒμΌμ„ μˆ˜μ •ν•˜μž.

// (... μƒλž΅ ...)


@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
                .requestMatchers(new AntPathRequestMatcher("/**")).permitAll())
            .csrf((csrf) -> csrf
                .ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**")))
        ;
        return http.build();
    }
}

/h2-console/둜 μ‹œμž‘ν•˜λŠ” URL은 CSRF 검증을 ν•˜μ§€ μ•ŠλŠ”λ‹€λŠ” 섀정을 μΆ”κ°€ν–ˆλ‹€. μ΄λ ‡κ²Œ μˆ˜μ •ν•˜κ³  λ‹€μ‹œ H2 μ½˜μ†”μ— 접속해 보자. 이제 CSRF 검증이 μ˜ˆμ™Έ μ²˜λ¦¬λ˜μ–΄ λ‘œκ·ΈμΈμ€ μˆ˜ν–‰λ˜μ§€λ§Œ λ‹€μŒμ²˜λŸΌ 화면이 깨져보인닀.

05_5
05_5

이 였λ₯˜κ°€ λ°œμƒν•˜λŠ” 원인은 H2 μ½˜μ†”μ˜ 화면이 frame ꡬ쑰둜 μž‘μ„±λ˜μ—ˆκΈ° λ•Œλ¬Έμ΄λ‹€. μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°λŠ” μ‚¬μ΄νŠΈμ˜ μ½˜ν…μΈ κ°€ λ‹€λ₯Έ μ‚¬μ΄νŠΈμ— ν¬ν•¨λ˜μ§€ μ•Šλ„λ‘ ν•˜κΈ° μœ„ν•΄ X-Frame-Options 헀더값을 μ‚¬μš©ν•˜μ—¬ 이λ₯Ό λ°©μ§€ν•œλ‹€. (clickjacking 곡격을 λ§‰κΈ°μœ„ν•΄ μ‚¬μš©ν•¨)

이 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ λ‹€μŒκ³Ό 같이 μ„€μ • νŒŒμΌμ„ μˆ˜μ •ν•˜μž.

package com.mysite.sbb;

// (... μƒλž΅ ...)
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
// (... μƒλž΅ ...)


@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
                .requestMatchers(new AntPathRequestMatcher("/**")).permitAll())
            .csrf((csrf) -> csrf
                .ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**")))
            .headers((headers) -> headers
                .addHeaderWriter(new XFrameOptionsHeaderWriter(
                    XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)))
        ;
        return http.build();
    }
}

μœ„ 처럼 URL μš”μ²­μ‹œ X-Frame-Options 헀더값을 sameorigin으둜 μ„€μ •ν•˜μ—¬ 였λ₯˜κ°€ λ°œμƒν•˜μ§€ μ•Šλ„λ‘ ν–ˆλ‹€. X-Frame-Options ν—€λ”μ˜ κ°’μœΌλ‘œ sameorigin을 μ„€μ •ν•˜λ©΄ frame에 ν¬ν•¨λœ νŽ˜μ΄μ§€κ°€ νŽ˜μ΄μ§€λ₯Ό μ œκ³΅ν•˜λŠ” μ‚¬μ΄νŠΈμ™€ λ™μΌν•œ κ²½μš°μ—λŠ” 계속 μ‚¬μš©ν•  수 μžˆλ‹€.

이제 λ‹€μ‹œ H2 μ½˜μ†”λ‘œ λ‘œκ·ΈμΈν•˜λ©΄ 정상 λ™μž‘ν•˜λŠ” 것을 확인할 수 μžˆμ„ 것이닀.