μ€νλ§ λΆνΈ 보μ μ€μ
λ³Έ κΈμ μ€νλ§ λΆνΈ νλ‘μ νΈλ‘ λ§λ€μ΄μ§ μ ν리μΌμ΄μ μμ μ€νλ§ μνλ¦¬ν° λͺ¨λμ μ μ©νλ κ²½μ°μ 보μ μ€μ μ λν΄ μ 리ν κ² μ λλ€. μΌλ°μ μΌλ‘ μ€ν΄νκ³ μκ±°λ μ κ²½μ°μ§ μλ λΆλΆμ λν΄μ λ€λ£¨κ³ μ ν©λλ€.
λ μμ ν 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.confserver { 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 ν΅μ μμ μ£Όμνλλ‘ ν΄μΌν©λλ€.
μ ν리μΌμ΄μ 보μ μ κ²μΌλ‘ μΈν΄ λ€μ νλ² νμ΅νλ κ²μ μ λΉλ°μ΄λ€.