λ³Έ 글은 μŠ€ν”„λ§ λΆ€νŠΈ ν”„λ‘œμ νŠΈλ‘œ λ§Œλ“€μ–΄μ§„ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ μŠ€ν”„λ§ μ‹œνλ¦¬ν‹° λͺ¨λ“ˆμ„ μ μš©ν•˜λŠ” κ²½μš°μ— λ³΄μ•ˆ 섀정에 λŒ€ν•΄ μ •λ¦¬ν•œ 것 μž…λ‹ˆλ‹€. 일반적으둜 μ˜€ν•΄ν•˜κ³  μžˆκ±°λ‚˜ 신경쓰지 μ•ŠλŠ” 뢀뢄에 λŒ€ν•΄μ„œ λ‹€λ£¨κ³ μž ν•©λ‹ˆλ‹€.

더 μ•ˆμ „ν•œ CSRF 토큰 μ„€μ •

λ¦¬μ•‘νŠΈλ‚˜ 뷰와 같은 ν”„λ‘ νŠΈμ—”λ“œ 개발 ν™˜κ²½μ΄μ–΄λ„ μ„œλ²„ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ CSRF 토큰을 ν”„λ‘ νŠΈμ—”λ“œλ‘œ μ „λ‹¬ν•˜κΈ° μœ„ν•΄ μΏ ν‚€λ₯Ό μ΄μš©ν•  ν•„μš”λŠ” μ—†λ‹€. μžλ°”μŠ€ν¬λ¦½νŠΈμ—μ„œ 쿠킀에 μ €μž₯된 XSRF-TOKEN 값을 κ°€μ Έμ˜¬ 수 μžˆλ„λ‘ httpOnly 섀정을 λΉ„ν™œμ„±ν™”ν•˜μ§€ 말자. 이것에 λŒ€μ•ˆμœΌλ‘œ CSRF 토큰을 μ œκ³΅ν•˜λŠ” APIλ₯Ό λ§Œλ“€μ–΄μ„œ μ‘λ‹΅ν•˜λ©΄ λœλ‹€.

WebSecurityConfig.java
@AllArgsConstructor @Configuration @EnableWebSecurity public class WebSecurityConfig { private final ServerProperties serverProperties; @Bean public CsrfTokenRepository csrfTokenRepository() { Session.Cookie cookie = serverProperties.getServlet().getSession().getCookie(); CookieCsrfTokenRepository tokenRepository = new CookieCsrfTokenRepository(); tokenRepository.setCookieCustomizer(c -> c.secure(cookie.getSecure()) .path(cookie.getPath()) .httpOnly(cookie.getHttpOnly()) .sameSite(cookie.getSameSite().attributeValue()) .maxAge(Duration.ofMinutes(30))); return tokenRepository; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.csrfTokenRepository(csrfTokenRepository())); return http.build(); } }
CsrfController.java
@AllArgsConstructor @RestController public class CsrfController { private final CsrfTokenRepository csrfTokenRepository; @RequestMapping("/csrf") public CsrfToken csrf(HttpServletRequest request, HttpServletResponse response) { return csrfTokenRepository.loadDeferredToken(request, response).get(); } }

μžλ°”μŠ€ν¬λ¦½νŠΈ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μœ„ν•œ 더 λ‹€μ–‘ν•œ 방법은 곡식 λ¬Έμ„œλ₯Ό μ°Έκ³ ν•˜μ„Έμš”.

μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ³΄μ•ˆμ—μ„œ 더 μ€‘μš”ν•œ 것은 둜그인 및 λ‘œκ·Έμ•„μ›ƒ μš”μ²­μ— λŒ€ν•΄ CSRFλ₯Ό μ‚¬μš©ν•΄μ„œ 둜그인 μ‹œλ„λ₯Ό μœ„μ‘°ν•˜μ§€ λͺ»ν•˜λ„둝 ν•˜λŠ” 것이닀. μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°μ˜ κΈ°λ³Έ 폼 λ‘œκ·ΈμΈμ΄λ‚˜ HTTP 베이직 인증이 μ•„λ‹ˆλΌ λ³„λ„μ˜ APIλ₯Ό μž‘μ„±ν•œλ‹€λ©΄ λ°˜λ“œμ‹œ CSRF 토큰이 μ μš©λ˜λŠ”μ§€ κ²€μ¦ν•˜λ„λ‘ ν•˜μž.

λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ λ³΄μ•ˆ

μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°λŠ” 기본적으둜 λ³΄μ•ˆμ„ μœ„ν•œ 응닡 헀더λ₯Ό μΆ”κ°€ν•΄μ€€λ‹€. λ‹€λ§Œ, HSTS의 κ²½μš°λŠ” μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μžμ²΄κ°€ HTTPS ν”„λ‘œν† μ½œλ‘œ μ‹€ν–‰λ˜μ—ˆμ„λ•Œ ν™œμ„±ν™”λœλ‹€. λ”°λΌμ„œ, μ—”μ§„μ—‘μŠ€μ™€ 같은 μ›Ή μ„œλ²„λ‚˜ λ‘œλ“œλ°ΈλŸ°μ„œλ₯Ό 톡해 λ¦¬λ²„μŠ€ ν”„λ‘μ‹œλ₯Ό κ΅¬μ„±ν•˜λŠ” κ²½μš°μ—λŠ” HTTP 포트λ₯Ό μ‚¬μš©ν•΄μ„œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μ‹€ν–‰ν•˜λ―€λ‘œ μ—”μ§„μ—‘μŠ€μ™€ 같은 μ›Ή μ„œλ²„μ—μ„œ HSTS 헀더λ₯Ό μ‘λ‹΅ν•˜λ„λ‘ ν•˜μž.

nginx.conf
server { listen 443 ssl http2; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; location /api/ { limit_except GET POST PUT DELETE OPTIONS { deny all; } } }

λ¦¬μ•‘νŠΈ λ˜λŠ” 뷰의 λΉŒλ“œ 에셋을 μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ λ°°ν¬ν•˜μ§€ μ•ŠλŠ”λ‹€λ©΄ μ›Ή μ„œλ²„μ—μ„œ λ³΄μ•ˆ 응닡 헀더λ₯Ό λ™μΌν•˜κ²Œ μ „λ‹¬λ˜λ„λ‘ κ΅¬μ„±ν•΄μ•Όν•©λ‹ˆλ‹€.

μ›Ήμ†ŒμΌ“ λ³΄μ•ˆ

μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°λŠ” 기본적으둜 HTTP 톡신에 λŒ€ν•œ λ³΄μ•ˆ 섀정을 μ œκ³΅ν•œλ‹€. λ§Œμ•½, μ• ν”Œλ¦¬μΌ€μ΄μ…˜ κΈ°λŠ₯ μš”κ΅¬μ‚¬ν•­μ„ μœ„ν•΄ μ›Ήμ†ŒμΌ“ ν”„λ‘œν† μ½œμ„ μ‚¬μš©ν•œλ‹€λ©΄ μ›Ήμ†ŒμΌ“μ— λŒ€ν•œ λ³΄μ•ˆμ— λŒ€ν•΄ λ³„λ„λ‘œ μ²΄ν¬ν•΄μ•Όν•œλ‹€. μŠ€ν”„λ§ μ‹œνλ¦¬ν‹° λͺ¨λ“ˆμ„ μ‚¬μš©ν•œλ‹€λ©΄ 일반적인 μ›Ήμ†ŒμΌ“ κ΅¬ν˜„λ³΄λ‹€λŠ” STOMP λ°©μ‹μ˜ μ›Ήμ†ŒμΌ“ 연결을 κ΅¬μ„±ν•˜λŠ” 것이 μ’‹λ‹€. CSRF을 μ μš©ν•˜κ±°λ‚˜ SockJSλ₯Ό μœ„ν•œ iFrame μ˜΅μ…˜μ„ μ²΄ν¬ν•˜λ„λ‘ ν•˜μž.

WebSocketSecurity.java
@Configuration @EnableWebSocketSecurity public class WebSocketSecurity { @Bean public AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) { // NOTE: Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]: Access Denied messages .nullDestMatcher().authenticated() .simpDestMatchers("/app/**").hasRole("USER") .simpSubscribeDestMatchers("/user/queue/error").authenticated() .simpSubscribeDestMatchers("/user/**", "/topic/hello").hasRole("USER") .anyMessage().denyAll(); return messages.build(); } }

SockJS의 μ›Ήμ†ŒμΌ“ 연결을 μœ„ν•΄ 사전에 μš”μ²­ν•˜λŠ” /info μ—”λ“œν¬μΈνŠΈλŠ” 일반적인 HTTP ν†΅μ‹ μž„μ— μ£Όμ˜ν•˜λ„λ‘ ν•΄μ•Όν•©λ‹ˆλ‹€.

μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ³΄μ•ˆ μ κ²€μœΌλ‘œ 인해 λ‹€μ‹œ ν•œλ²ˆ ν•™μŠ΅ν•˜λŠ” 것은 μ•ˆ 비밀이닀.