CSRF 보호와 CORS 적용

10.1. 애플리케이션에 CSRF(사이트 간 요청 위조) 보호 적용

  • 스프링 시큐리티가 기본적으로 활성화 하기 때문에 POST 요청을 할 수 없다.

10.1.1. 스프링 시큐리티의 CSRF 보호가 작동하는 방식

  • CSRF

    • 사용자가 웹 애플리케이션에 로그인 했다고 가정한다.

    • 공격자에게 속아서 작업 중인 같은 애플리케이션에서 작업을 실행하는 스크립트가 포함된 페이지를 연다.

    • 스크립트는 이제 사용자를 가장하고 사용자 대신 작업을 수행한다.

  • CSRF 보호

    • 프런트엔드만 번경 작업을 수행할 수 있게 보장한다.

  • 작동 방식

    • 변경 작업 수행 전에 적어도 한 번은 GET 요청을 해야 한다.

    • 이때 애플리케이션이 고유한 토큰을 생성한다.

    • 이제 애플리케이션은 헤더에 이 고유값이 들어있을 때만 변경 작업을 수행한다.

  • CsrfFilter

    • 요청을 가로채고 GET, HEAD, TRACE, OPTIONS는 모두 허용, 다른 요청은 토큰이 포함된 헤더가 있는지 확인한다.

    • 헤더가 없거나 잘못된 토큰 값이 포함된 경우 요청을 거부하고 403으로 설정한다.

  • CsrfTokenRepository

    • 토큰 값을 관리한다.

    • 토큰을 HTTP 세션에 저장하고 랜덤 UUID로 토큰을 생성한다.

    • 직접 구현해서 커스터마이징 할 수 있다.

public class CsrfTokenLogger implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        CsrfToken o = (CsrfToken) servletRequest.getAttribute("_csrf");

        filterChain.doFilter(servletRequest, servletResponse);
    }
}

@Configuration
public class ProjectConfig {

    ...

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.addFilterAfter(new CsrfTokenLogger(), CsrfFilter.class)
                .authorizeHttpRequests(auth -> auth.anyRequest().permitAll());

        return http.build();
    }
}
  • 백엔드는 응답에 CSRF 토큰을 담아 프론트가 전달할 수 있게 한다.

10.1.2. 실제 시나리오에서 CSRF 보호 사용

  • CSRF 토큰은 같은 서버가 프론트와 백엔드를 담당하는 단순한 아키텍처에서 잘 작동한다.

  • 백엔드와 독립적일 때에는 잘 동작하지 않는다.

    • 11~15장에서 해결 방법을 설명한다.

10.1.3. CSRF 보호 맞춤 구성


@Configuration
public class ProjectConfig {

    ...

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(c -> {
                    c.ignoringRequestMatchers("/ciao");
                    RegexRequestMatcher matcher = new RegexRequestMatcher(".*[0-9].*", HttpMethod.POST.name());
                    c.ignoringRequestMatchers(matcher);
                })
                .authorizeHttpRequests(auth -> auth.anyRequest().permitAll());

        return http.build();
    }
}
  • CSRF 보호 메커니즘에서 제외할 경로를 나타낼 수 있다.

  • MvcRequestMatcher, RegexRequestMatcher도 함께 사용할 수 있다.


@Entity
public class Token {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String identifier;

    private String token;

    public void setToken(String token) {
        this.token = token;
    }

    public void setIdentifier(String identifier) {
        this.identifier = identifier;
    }

    public String getToken() {
        return this.token;
    }
}
  • 세션에 CSRF 토큰을 저장하는 것은 수평적 확장이 필요한 애플리케이션에는 적합하지 않다.

  • 데이터베이스에 저장하고 싶다면 CsrfTokenRepository로 토큰을 관리할 수 있다.

10.2. CORS(교차 출처 리소스 공유) 이용

10.2.1. CORS 작동 방식

  • 다른 도메인으로 엔드 포인트를 호출하면 거부된다.

  • CORS를 이용하면 도메인을 허용하고 세부 정보를 지정할 수 있다.

  • 제한을 가하기보다 도메인 호출의 엄격한 제약 조건을 완화하도록 도와주는 기능이다.

  • 브라우저는 어떤 요청을 허용해야 하는지 테스트 하기 위해 HTTP OPTIONS 방식으로 prefilght 요청을 하기도 한다.

주요 헤더

  • Access-Control-Allow-Origin

    • 도메인의 리소스에 접근할 수 있는 외부 도메인을 지정한다.

  • Access-Control-Allow-Methods

    • 다른 도메인에 대해 접근을 허용하지만 특정 HTTP 방식만 허용하고 싶을 때 지정할 수 있다.

  • Access-Control-Allow-Headers

    • 특정 요청에 이용할 수 있는 헤더에 제한을 추가한다.

10.2.2. @CrossOrigin 애너테이션으로 CORS 정책 적용


@Controller
public class MainPageController {

    @Autowired
    private ProductService productService;

    @GetMapping("/main")
    @CrossOrigin("http://localhost:8080")
    public String main(Authentication a, Model model) {
        model.addAttribute("username", a.getName());
        model.addAttribute("products", productService.findAll());
        return "main.html";
    }
}
  • 각 엔드포인트에 맞게 손쉽게 CORS를 구성할 수 있는 방법이다.

  • 여러 출처를 배열로 받을 수 있다.

  • *를 쓸 수 있지만 DDoS 공격에 취약해질 수 있으므로 되도록 허용하지 않는 것이 좋다.

10.2.3. CorsConfigurer로 CORS 적용


@Configuration
public class ProjectConfig {

    ...

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomCsrfTokenRepository customCsrfTokenRepository) throws Exception {
        http.cors(c -> {
            CorsConfigurationSource source = request -> {
                CorsConfiguration config = new CorsConfiguration();
                config.setAllowedOrigins(List.of());
                config.setAllowedMethods(List.of());
                return config;
            };
            c.configurationSource(source);
        });
        
        ...

        return http.build();
    }
}
  • 한 곳에서 관리하는 방법도 있다.

  • 복잡해질 수 있으므로 코드를 다른 클래스로 나누는것이 좋다.

Last updated

Was this helpful?