포스트

2023-01-16-TIL

2023-01-16-TIL

Today I Learned

TinyMCE 에디터의 이미지 업로드를 Spring Boot 서버와 연결하는 방법, 그리고 Spring Security를 쓰는 환경에서 CORS preflight 요청이 막히는 문제와 CORS 설정 위치를 정리했다.


TinyMCE and Spring Boot

TinyMCE 6 문서에 따르면 에디터에 넣은 로컬 이미지는 images_upload_url에 지정한 서버 주소로 HTTP POST 요청 하나에 이미지 하나씩 전송된다. 서버의 업로드 핸들러는 이미지를 파일 시스템, CDN, DB, 자산 관리 시스템 등에 저장하고, 저장 위치를 담은 location 속성이 있는 JSON 객체를 돌려줘야 한다. 에디터는 이 값으로 본문의 <img src>를 바꾼다.

Spring Boot 쪽에서는 MultipartFile을 받는 컨트롤러를 만들고 {"location": "https://.../image.png"} 형태로 응답하면 된다. 문서는 에디터가 붙이는 임시 파일명(blobid0-....png 등)이 겹치면 덮어쓰일 수 있다고 경고하므로, 서버에서 고유한 파일명을 새로 부여하는 편이 안전하다. 직접 구현하지 않으려면 Tiny의 클라우드 파일 관리 서비스인 Tiny Drive와 Java 스타터를 쓰는 선택지도 있다.

1
2
3
4
5
@PostMapping("/editor/images")
public Map<String, String> upload(@RequestParam("file") MultipartFile file) {
    String url = storage.save(file); // 고유한 이름으로 저장
    return Map.of("location", url);
}
  • https://catchdream.tistory.com/152
  • https://blog.csdn.net/haha_201510/article/details/107110473?spm=a2c6h.12873639.article-detail.7.41995799nWypD0&%253E
  • https://learncodewithdurgesh.com/course/spring-boot-tutorial-in-hindi/333
  • https://github.com/tinymce/tinydrive-java-spring-starter
  • https://www.tiny.cloud/docs/tinymce/6/tinydrive-java/#
  • https://www.tiny.cloud/docs/configure/file-image-upload/
  • https://www.tiny.cloud/docs/general-configuration-guide/upload-images/
  • https://www.tiny.cloud/docs/tinymce/6/upload-images/
  • https://www.tiny.cloud/docs/tinymce/6/file-image-upload/
  • https://www.tiny.cloud/docs/demo/local-upload/#
  • https://www.tiny.cloud/blog/tinymce-image-upload-tiny-drive/
  • https://www.tiny.cloud/docs/tinymce/6/upload-images/
  • https://stackoverflow.com/questions/44694579/how-to-upload-images-in-spring-mvc-from-tinymce-editor
  • https://careydevelopment.us/blog/adding-images-in-a-wysiwyg-editor-with-tinymce-thymeleaf-and-spring-boot
  • https://github.com/careydevelopment/tinymce-image-guide

Spring Security and Preflight

브라우저는 다른 출처로 커스텀 헤더나 PUT, DELETE 같은 요청을 보내기 전에 OPTIONS 메서드로 preflight 요청을 보낸다. Spring Security 문서는 CORS를 Spring Security보다 먼저 처리해야 한다고 설명한다. preflight 요청에는 쿠키(JSESSIONID 등)가 담기지 않기 때문에, Spring Security가 먼저 처리하면 인증되지 않은 요청으로 보고 거부한다. 그 결과 브라우저 콘솔에는 401이나 CORS 오류로 나타난다.

해결책은 Spring Security 설정에서 http.cors()를 켜고 CorsConfigurationSource 빈을 제공하는 것이다. 그러면 Spring Security가 CorsFilter를 자기 필터 체인 앞쪽에 두어 preflight를 먼저 처리한다. OPTIONS 요청을 모두 permitAll()로 여는 방법도 쓰이지만, CORS 응답 헤더까지 맞춰 주지는 않으므로 근본적인 해결은 아니다.

1
2
3
4
5
6
7
8
9
@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://example.com"));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
}
  • https://www.baeldung.com/spring-security-cors-preflight
  • https://kkyu-coder.tistory.com/184
  • https://velog.io/@xonic789/CORS-preflight-%EC%9A%94%EC%B2%AD%EC%97%90-%EB%8C%80%ED%95%B4-401-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0Spring-security-config
  • https://evan-moon.github.io/2020/05/21/about-cors/
  • https://oddpoet.net/blog/2017/04/27/cors-with-spring-security/
  • https://stackoverflow.com/questions/36809528/spring-boot-cors-filter-cors-preflight-channel-did-not-succeed
  • https://docs.spring.io/spring-security/reference/reactive/integrations/cors.html
  • https://gowoonsori.com/error/springsecurity-cors/

WebMvcConfigurer

WebMvcConfigurer는 Spring MVC의 기본 설정을 유지하면서 필요한 부분만 바꾸는 콜백 인터페이스다. 인터셉터 등록(addInterceptors), 리소스 핸들러, 메시지 컨버터, 그리고 CORS 매핑(addCorsMappings)을 여기서 설정한다. 다만 addCorsMappings는 Spring MVC의 DispatcherServlet 단계에서 적용되므로, 그 앞의 서블릿 필터인 Spring Security가 요청을 먼저 거부하면 소용이 없다. Spring Security를 쓴다면 앞 절처럼 Security 쪽에서 CORS를 처리해야 하는 이유다.

  • https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.html
  • https://stackoverflow.com/questions/63426010/cors-spring-security-filter-vs-webmvcconfigurer-addcorsmappings
  • https://spring.io/blog/2015/06/08/cors-support-in-spring-framework
  • https://goodgid.github.io/Spring-WebMvcConfigurer/

WebMvcConfigurer vs WebSecurityConfigurerAdapter

둘은 설정 대상이 다르다. WebMvcConfigurer는 Spring MVC를, WebSecurityConfigurerAdapter는 Spring Security의 필터 체인을 설정한다. Spring 블로그에 따르면 WebSecurityConfigurerAdapter는 Spring Security 5.7.0-M2에서 deprecated 되었고, 상속 대신 SecurityFilterChain을 빈으로 등록하는 컴포넌트 기반 설정이 권장된다.

1
2
3
4
5
6
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.cors(Customizer.withDefaults())
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
    return http.build();
}
  • https://stackoverflow.com/questions/53894649/difference-between-webmvcconfigurer-and-websecurityconfigureradapter
  • https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter

CORS in Spring Boot

Spring Boot에서 CORS를 설정하는 방법은 세 단계로 정리된다.

  • 컨트롤러나 메서드 단위: @CrossOrigin
  • 애플리케이션 전역(Spring MVC): WebMvcConfigurer.addCorsMappings()
  • Spring Security를 쓰는 경우: http.cors()와 CorsConfigurationSource 빈

허용 출처를 *로 열면서 allowCredentials(true)를 함께 쓸 수 없다는 점은 자주 걸리는 부분이다. 쿠키를 포함한 요청을 허용하려면 출처를 명시하거나 allowedOriginPatterns를 써야 한다.

  • https://medium.com/techpanel/guide-to-cors-in-spring-boot-e76d317b9b36
  • https://rajendraprasadpadma.medium.com/what-the-cors-ft-spring-boot-spring-security-562f24d705c9
  • https://www.javadevjournal.com/spring-security/spring-security-cors-filter/
  • https://reflectoring.io/spring-cors/
  • https://yg1110.tistory.com/3
  • https://2step-hyun.tistory.com/112
  • https://vvshinevv.tistory.com/62
  • https://docs.spring.io/spring-security/site/docs/4.2.x/reference/html/cors.html
  • https://github.com/spring-projects/spring-boot/issues/24997
  • https://www.baeldung.com/cs/cors-preflight-requests
  • https://coding-groot.tistory.com/91
이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다