<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>우직하게</title>
    <link>https://greatwhite.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 16 Apr 2026 21:51:19 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>greatwhite</managingEditor>
    <image>
      <title>우직하게</title>
      <url>https://tistory1.daumcdn.net/tistory/6637793/attach/514f7f43441b44f2bbc325baa56bff04</url>
      <link>https://greatwhite.tistory.com</link>
    </image>
    <item>
      <title>Mermaid ERD 문법</title>
      <link>https://greatwhite.tistory.com/43</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mermaid.js.org/syntax/entityRelationshipDiagram.html&quot;&gt;Entity Relationship Diagrams | Mermaid&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;br /&gt;엔티티와 관계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;엔티티1&amp;gt; [&amp;lt;관계&amp;gt; &amp;lt;엔티티2&amp;gt; : &amp;lt;관계 이름&amp;gt;]&lt;/code&gt; 으로 구성됨.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔티티1 : 엔티티 명이며 영문자나 &lt;code&gt;_&lt;/code&gt;로 시작해야 하며, 숫자와 &lt;code&gt;-&lt;/code&gt; 를 포함할 수 있다.&lt;/li&gt;
&lt;li&gt;관계 : 두 엔티티 간의 상호 관계를 나타낸다.&lt;/li&gt;
&lt;li&gt;엔티티2 : 다른 엔티티명.&lt;/li&gt;
&lt;li&gt;관계 이름 : &lt;b&gt;엔티티1의 관점&lt;/b&gt;에서의 관계를 나타낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;HOUSE ||--|{ ROOM : contains&lt;/code&gt; 를 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제는 &amp;ldquo;집은 하나 이상의 방을 포함할 수 있고, 방은 반드시 하나의 집에만 포함될 수 있다&amp;rdquo;로 해석된다. 위의 관계명은 엔티티1의 관점인 것을 확인할 수 있다. 엔티티2의 관점에서 봤을 때 동등 라벨은(?)은 추론하기 매우 쉽다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 문법의 &lt;code&gt;&amp;lt;엔티티1&amp;gt;&lt;/code&gt; 부분만 필수이다. 이를 통해 관계가 없는 엔티티를 보여줄 수 있는데 다이어그램을 반복적으로 생성할 때 유용하다. 만약 뒷 부분 중 아무 부분이라도 넣게 된다면 모든 부분을 작성해야 한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;관계 문법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 문장의 관계 부분은 크게 3개로 나뉠 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔티티2의 관점에서 엔티티1의 cardinality&lt;/li&gt;
&lt;li&gt;관계가 &amp;lsquo;자식&amp;rsquo; 엔티티에 대해 신원을 부여하는지 여부&lt;/li&gt;
&lt;li&gt;엔티티1의 관점에서 엔티티2의 cardinality&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cardinality는 다른 엔티티의 요소가 해당 엔티티와 얼마나 연관될 수 있는지를 설명하는 속성이다. 위에서 다룬 예시에서는 집이 하나 이상의 방과 연관될 수 있었던 반면에 방은 하나의 집에만 연관될 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 cardinality 표시자는 두가지 문자로 구성된다. 엔티티와 가까운 쪽이 최대 수를 나타내며, 다른한 쪽은 최소 수를 나타낸다.&lt;/p&gt;
&lt;table style=&quot;height: 100px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;엔티티1 기준&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;엔티티2 기준&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;|o&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;o|&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;없거나 최대 하나&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;||&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;||&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;반드시 하나&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;code&gt;}o&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;code&gt;o{&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;없거나 하나 이상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;}|&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;|{&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;하나 이상&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mermaid ERD에서는 &lt;b&gt;多쪽에&lt;/b&gt; limit을 제공하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;br /&gt;식별 관계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 테이블의 키를 자식 테이블이 기본 키로 사용하는가? &amp;rArr; 식별(O) VS 비식별(X)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계는 식별 또는 비식별 관계로 분류될 수 있고 이는 실선과 점선으로 표시된다. 이는 해당 엔티티가 다른 엔티티 없이는 존재할 수 없는 종속적일 경우에 해당된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 사람들이 자동차를 운전할 때 보험에 가입하는 회사에서는 &lt;code&gt;NAMED-DRIVER&lt;/code&gt; 에 대한 데이터를 저장해야할 수 있다. 모델링 단계에서는 먼저 하나의 자동차를 많은 사람들이 탈 수 있고 사람이 많은 자동차를 운전할 수 있다는 것을 관찰하는 것에서부터 시작할 수 있다. 두 개체는 서로 없이 존재 할 수 있으므로 이는 비식별관계이고 Mermaid에서는 아래와 같이 표현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;PERSON }|..|{ CAR : &quot;driver&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위 표현식에서 &lt;code&gt;..&lt;/code&gt; 은 두 엔티티간의 관계를 표현할 때 점선으로 나타난다. 하지만 이 다대다 관계가 두 개의 일대다 관계로 나누면 &lt;code&gt;NAMED-DRIVER&lt;/code&gt;는 사람과 차 둘 다 없이는 존재하지 못하는 식별관계인 것을 알 수 있다. 그렇기 때문에 &lt;code&gt;--&lt;/code&gt;을 이용해 실선으로 표시하게 된다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;erDiagram
    CAR ||--o{ NAMED-DRIVER : allows
    PERSON ||--o{ NAMED-DRIVER : is&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-12-02 오후 9.08.25.png&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;277&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YU6nK/btsK47stwTZ/FG1zwKjRuV6EgixdGaab1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YU6nK/btsK47stwTZ/FG1zwKjRuV6EgixdGaab1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YU6nK/btsK47stwTZ/FG1zwKjRuV6EgixdGaab1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYU6nK%2FbtsK47stwTZ%2FFG1zwKjRuV6EgixdGaab1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;336&quot; height=&quot;277&quot; data-filename=&quot;스크린샷 2024-12-02 오후 9.08.25.png&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;277&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;br /&gt;속성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성은 &lt;code&gt;{}&lt;/code&gt; 안에 여러 &lt;code&gt;[자료형] [이름]&lt;/code&gt; 쌍으로 구성될 수 있다. 속성은 엔티티의 범위 안에 표시된다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;erDiagram
    CAR ||--o{ NAMED-DRIVER : allows
    CAR {
        string registrationNumber
        string make
        string model
    }
    PERSON ||--o{ NAMED-DRIVER : is
    PERSON {
        string firstName
        string lastName
        int age
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-12-02 오후 9.08.53.png&quot; data-origin-width=&quot;357&quot; data-origin-height=&quot;301&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dh9KTD/btsK4PTchAF/Jw7nwqesBvbIYVvdnIIc4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dh9KTD/btsK4PTchAF/Jw7nwqesBvbIYVvdnIIc4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dh9KTD/btsK4PTchAF/Jw7nwqesBvbIYVvdnIIc4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdh9KTD%2FbtsK4PTchAF%2FJw7nwqesBvbIYVvdnIIc4k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;357&quot; height=&quot;301&quot; data-filename=&quot;스크린샷 2024-12-02 오후 9.08.53.png&quot; data-origin-width=&quot;357&quot; data-origin-height=&quot;301&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자료형은 반드시 영문자로 시작해야 하며 숫자, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;_&lt;/code&gt; ,&lt;code&gt;()&lt;/code&gt;, &lt;code&gt;[]&lt;/code&gt; 을 포함할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름은 자료형과 비슷하나 기본 키임을 표시하기 위해 &lt;code&gt;*&lt;/code&gt;로 시작할 수 있다. 그 외에는 아무런 제한이 없으며, 유효한 데이터 유형의 암묵적인 집합도 존재하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;엔티티 명 약칭(&lt;i&gt;v10.5.0+&lt;/i&gt;)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;[]&lt;/code&gt;을 이용해 약칭을 추가할 수 있다. 만약 아래와 같이 약칭을 추가하면 약칭이 엔티티명으로 표시된다. 띄어쓰기를 사용해야 한다면 &lt;code&gt;&quot; &quot;&lt;/code&gt;를 활용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;erDiagram
p[Person] {
    string firstName
    string lastName
}

a[&quot;Customer Account&quot;] {
    string email
}
p ||--o| a : has&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-12-02 오후 9.09.25.png&quot; data-origin-width=&quot;357&quot; data-origin-height=&quot;301&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2VMB1/btsK42Y1Z3H/YaPuGzscRfNzuj69ZgOSE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2VMB1/btsK42Y1Z3H/YaPuGzscRfNzuj69ZgOSE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2VMB1/btsK42Y1Z3H/YaPuGzscRfNzuj69ZgOSE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2VMB1%2FbtsK42Y1Z3H%2FYaPuGzscRfNzuj69ZgOSE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;357&quot; height=&quot;301&quot; data-filename=&quot;스크린샷 2024-12-02 오후 9.09.25.png&quot; data-origin-width=&quot;357&quot; data-origin-height=&quot;301&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;속성 키와 비고&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성은 키 또는 비고가 있을 수 있다. 키는 주요 키, 외래 키, 고유키가 있고 각각 &lt;code&gt;PK&lt;/code&gt;, &lt;code&gt;FK&lt;/code&gt;, &lt;code&gt;UK&lt;/code&gt; 로 표시한다. 하나의 컬럼에 대해 여러 키 제약을 명시하고 싶다면 쉼표(&lt;code&gt;,&lt;/code&gt;) 로 이어 적는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비고는 큰따옴표(&lt;code&gt;&quot;&lt;/code&gt;)를 사용해 처음과 끝을 감싼다. &lt;b&gt;비고는 내부에 큰따옴표를 포함할 수 없다&lt;/b&gt;.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;erDiagram
    CAR ||--o{ NAMED-DRIVER : allows
    CAR {
        string registrationNumber PK
        string make
        string model
        string[] parts
    }
    PERSON ||--o{ NAMED-DRIVER : is
    PERSON {
        string driversLicense PK &quot;The license #&quot;
        string(99) firstName &quot;Only 99 characters are allowed&quot;
        string lastName
        string phone UK
        int age
    }
    NAMED-DRIVER {
        string carRegistrationNumber PK, FK
        string driverLicence PK, FK
    }
    MANUFACTURER only one to zero or more CAR : makes&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-12-02 오후 9.10.33.png&quot; data-origin-width=&quot;595&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HnsMI/btsK4sD2c9y/wwQD3MkrM3aryGwngrAJm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HnsMI/btsK4sD2c9y/wwQD3MkrM3aryGwngrAJm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HnsMI/btsK4sD2c9y/wwQD3MkrM3aryGwngrAJm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHnsMI%2FbtsK4sD2c9y%2FwwQD3MkrM3aryGwngrAJm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;595&quot; height=&quot;504&quot; data-filename=&quot;스크린샷 2024-12-02 오후 9.10.33.png&quot; data-origin-width=&quot;595&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계 이름에 두 단어 이상 사용하고 싶다면 큰따옴표(&lt;code&gt;&amp;rdquo;&lt;/code&gt;)로 감쌀 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계 이름을 사용하고 싶지 않다면 &lt;code&gt;&quot;&quot;&lt;/code&gt;와 같이 큰따옴표로 감싸진 빈 문자를 사용하면 된다.&lt;/p&gt;</description>
      <category>Today I Learned</category>
      <category>erd 문법</category>
      <category>Mermaid</category>
      <author>greatwhite</author>
      <guid isPermaLink="true">https://greatwhite.tistory.com/43</guid>
      <comments>https://greatwhite.tistory.com/43#entry43comment</comments>
      <pubDate>Mon, 2 Dec 2024 21:04:57 +0900</pubDate>
    </item>
    <item>
      <title>14284 : 간선 이어가기 2 - Python</title>
      <link>https://greatwhite.tistory.com/42</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/14284&quot;&gt;14284 : 간선 이어가기 2&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;접근&lt;/h3&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 단순하게 간선과 가중치가 나오길래 최소 신장 트리를 생각했었다. 하지만, 모든 정점을 연결하는 것이 아닌 특정 정점 두 개만 연결시켜야 했고 최소신장트리로는 풀 수 없음을 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음으로 생각한 것은 유니온 파인드인데, 이것도 두 정점을 연결했을 때 최소 비용임을 보장할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 생각한 것은 다익스트라였다. 모든 정점을 다 고려해도 두 정점 간의 최소 비용으로 연결을 보장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간선 연결 과정에서 최악의 형태는 1자형태로 쭉 뻗은 그래프 형태가 되는데, 그렇게 되면 간선이 N - 1개가 된다. 그렇다면 최대 100,000개의 간선에는 중복이 있을 수 밖에 없다. 따라서, 그래프 저장시 가중치가 더 낮은 간선 취하면 될 듯 싶었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀 때는 중복되는 간선이 많을 수 있어 배열방식으로 최솟값 갱신하는 방식이 더 빠를 것이라고 생각했는데, 의외로 리스트에 모든 간선 다 저장하는 방식이 더 빨랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 추측해보면, 배열 방식을 사용했을 때는 각 정점마다 N - 1개의 간선을 모두 확인하고 넘어가기 때문인 것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 코드&lt;/h3&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배열 방식&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;import sys
from heapq import heappop, heappush

# print = sys.stdout.write
input = sys.stdin.readline

# 반드시 아래 두 라인 주석 처리 후 제출
f = open(&quot;input.txt&quot;, &quot;rt&quot;)
input = f.readline
# 반드시 위 두 라인 주석 처리 후 제출
INF = float(&quot;inf&quot;)

N, M = map(int, input().split())
graph = [[INF] * (N + 1) for _ in range(N + 1)]
for i in range(1, N + 1):
    graph[i][i] = 0

for _ in range(M):
    a, b, c = map(int, input().split())
    graph[a][b] = min(graph[a][b], c)
    graph[b][a] = min(graph[b][a], c)

S, T = map(int, input().split())

def main():
    print(dijkstra())

def dijkstra():
    dist = [INF] * (N + 1)
    dist[S] = 0

    pq = [(0, S)]
    while pq:
        cost, to = heappop(pq)
        if to == T:
            return dist[T]

        for next in range(1, N + 1):
            if next == to or graph[to][next] == INF:
                continue

            next_cost = cost + graph[to][next]
            if next_cost &amp;gt;= dist[next]:
                continue
            dist[next] = next_cost
            heappush(pq, (next_cost, next))

if __name__ == &quot;__main__&quot;:
    main()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리스트 방식&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;import sys
from heapq import heappop, heappush

# print = sys.stdout.write
input = sys.stdin.readline

# 반드시 아래 두 라인 주석 처리 후 제출
f = open(&quot;input.txt&quot;, &quot;rt&quot;)
input = f.readline
# 반드시 위 두 라인 주석 처리 후 제출
INF = float(&quot;inf&quot;)

N, M = map(int, input().split())
graph = [[] for _ in range(N + 1)]

for _ in range(M):
    a, b, c = map(int, input().split())
    graph[a].append((b, c))
    graph[b].append((a, c))

S, T = map(int, input().split())

def main():
    print(dijkstra())

def dijkstra():
    dist = [INF] * (N + 1)
    dist[S] = 0

    pq = [(0, S)]
    while pq:
        current_cost, current = heappop(pq)
        if current == T:
            return dist[T]

        for next, next_cost in graph[current]:
            total = current_cost + next_cost
            if total &amp;gt;= dist[next]:
                continue

            dist[next] = total
            heappush(pq, (total, next))

if __name__ == &quot;__main__&quot;:
    main()&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Problem Solving/백준</category>
      <category>다익스트라</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>greatwhite</author>
      <guid isPermaLink="true">https://greatwhite.tistory.com/42</guid>
      <comments>https://greatwhite.tistory.com/42#entry42comment</comments>
      <pubDate>Tue, 12 Nov 2024 11:59:34 +0900</pubDate>
    </item>
    <item>
      <title>2011 : 암호코드 - Python</title>
      <link>https://greatwhite.tistory.com/41</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;접근&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 도전했다가 실패했었던 문제.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;2&lt;/th&gt;
&lt;th&gt;3&lt;/th&gt;
&lt;th&gt;4&lt;/th&gt;
&lt;th&gt;5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;BE, Y&lt;/td&gt;
&lt;td&gt;BEA, YA&lt;/td&gt;
&lt;td&gt;BEAA, YAA, BEK, YK&lt;/td&gt;
&lt;td&gt;BEAAD, YAAD, BEKD, YKD, BEAN, YAN&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 1&amp;le; code[i - 1] * 10 + code[i] &amp;le; 26라면, dp[i] = dp[i - 2] + d[i - 1]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니라면 dp[i] = dp[i - 1]&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 분들 풀이를 참고하니 좀 더 간단한 방법도 있었다. 특정 조건을 만족할 때만, 값을 더해주는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 현재 자리가 0이 아니라면 일단 문자로 만들 수 있다. 따라서, 바로 직전에 만든 문자열에 문자 하나를 추가하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 식으로 나타내면 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;for idx in range(1, len(code)):
    if code[idx] &amp;gt; 0:
            dp[idx] += dp[idx - 1]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 두 글자를 합쳤을 때 10과 26의 사이에 있다면 문자를 만들 수 있다. 따라서, 두 글자 전에 만든 문자열에 문자를 하나 추가하면 된다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;for idx in range(1, len(code)):
    if 10 &amp;lt;= code[idx - 1] * 10 + code[idx] &amp;lt;= 26:
            dp[idx] += dp[idx - 2]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 나올 수 있는 경우의 수는 총 4가지이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;현재 자리수가 0이 아니고, 직전 자리수와 합쳐서 문자를 만들 수 있는 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 때, &lt;code&gt;dp[idx] = dp[idx - 2] + dp[idx - 1]&lt;/code&gt; 이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;현재 자리수가 0이고, 직전 자리수와 합치면 문자를 만들 수 있는 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 때, &lt;code&gt;dp[idx] = dp[idx - 2]&lt;/code&gt;가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;현재 자리수가 0이 아니지만 직전 자리수와 합쳐도 문자를 만들 수 없는 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 때는, &lt;code&gt;dp[idx] = dp[idx - 1]&lt;/code&gt; 이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;현재 자리수가 0이고, 직전 자리수와 합쳐도 문자를 만들 수 없는 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 경우 &lt;code&gt;dp[idx]&lt;/code&gt;는 0에서 갱신되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 1000과 같이 해독할 수 없는 코드에는 아래와 같은 dp테이블이 생성된다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;0&lt;/th&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;2&lt;/th&gt;
&lt;th&gt;3&lt;/th&gt;
&lt;th&gt;4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 코드&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1차 풀이&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;import sys

# print = sys.stdout.write
input = sys.stdin.readline

# 반드시 아래 두 라인 주석 처리 후 제출
f = open(&quot;input.txt&quot;, &quot;rt&quot;)
input = f.readline
# 반드시 위 두 라인 주석 처리 후 제출
MOD = 1_000_000
code = &quot;0&quot; + input().rstrip()
dp = [0] * (len(code) + 1)
# 점화식 처리하기 위한 장치
dp[0] = 1

def main():
    # 순서상 문자열의 두 번째
    # dp의 첫 번째
    # dp[1] = 1

    for idx in range(1, len(code)):
        prev, current = int(code[idx - 1]), int(code[idx])
        if current == 0:
            # 두 부분을 합쳐도 문자 성립이 안되면
            if prev * 10 + current &amp;gt; 26 or prev * 10 + current == 0:
                print(0)
                return
            # 두 부분을 합치면 문자 생성은 가능
            dp[idx] = dp[idx - 2] % MOD
            continue

        # 현재 숫자만 문자로 변환 가능
        if prev * 10 + current &amp;gt; 26 or prev * 10 + current &amp;lt; 10:
            dp[idx] = dp[idx - 1] % MOD
            continue

        # 모두 가능
        dp[idx] = (dp[idx - 2] + dp[idx - 1]) % MOD

    print(dp[len(code)] % MOD)

if __name__ == &quot;__main__&quot;:
    main()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;풀이 참조 후 개선&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;import sys

# print = sys.stdout.write
input = sys.stdin.readline

# 반드시 아래 두 라인 주석 처리 후 제출
f = open(&quot;input.txt&quot;, &quot;rt&quot;)
input = f.readline
# 반드시 위 두 라인 주석 처리 후 제출
MOD = 1_000_000
code = [0] + list(map(int, input().rstrip()))
dp = [0] * (len(code) + 1)
# 점화식 처리하기 위한 장치
dp[0] = 1

def main():
    if code[1] == 0:
        print(0)
        return

    for idx in range(1, len(code)):
        # 길이를 늘려줬으므로 첫 번째 자리부터 시작가능
        if code[idx] &amp;gt; 0:
            dp[idx] += dp[idx - 1]

        temp = code[idx - 1] * 10 + code[idx]
        if 10 &amp;lt;= temp &amp;lt;= 26:
            dp[idx] += dp[idx - 2]
        dp[idx] %= MOD

    # 강제로 한 글자 늘렸으니 -1 해줘야함
    print(dp[len(code) - 1] % MOD)

if __name__ == &quot;__main__&quot;:
    main()&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Problem Solving/백준</category>
      <category>dp</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>greatwhite</author>
      <guid isPermaLink="true">https://greatwhite.tistory.com/41</guid>
      <comments>https://greatwhite.tistory.com/41#entry41comment</comments>
      <pubDate>Mon, 11 Nov 2024 15:00:48 +0900</pubDate>
    </item>
    <item>
      <title>Python 코딩 테스트 준비[update - 24.11.08]</title>
      <link>https://greatwhite.tistory.com/33</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;빠른 입출력&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://computer-science-student.tistory.com/749&quot;&gt;[파이썬, Python] 빠른 입출력&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;import sys

# 빠른 입력
input = sys.stdin.readline
data = input().rstrip()

# 빠른 출력
print = sys.stdout.write
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문자열 포매팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hyjykelly.tistory.com/65&quot;&gt;[Python] 문자열 포맷팅하는 3가지 방법&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# 문자열 포매팅
s = f&quot;문자열 {값}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정렬&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bio-info.tistory.com/134#google_vignette&quot;&gt;[Python] f-string 포맷팅2 (2,8,16 진수, 1000단위 쉼표, 정렬, 문자채우기)&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;print(f&quot;{bin(b)[2:]:&amp;gt;04s}&quot;) # 오른쪽 정렬 4칸 0 채우기&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;삼항 연산자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blockdmask.tistory.com/551&quot;&gt;[python] 파이썬 삼항 연산자 (if ~ else ~)&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;[True] 일때 값 if condition else [False]일 때 값&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;람다식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dojang.io/mod/page/view.php?id=2359&quot;&gt;파이썬 코딩 도장: 32.1 람다 표현식으로 함수 만들기&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;lambda x : x + 10 # 매개변수 O
lambda: 0 # 매개변수 X&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다중 조건 정렬&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://imdona.tistory.com/14&quot;&gt;[python] List(2) list sorting : 다중 조건 정렬 &amp;amp; 백준 1181번[단어 정렬]&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;lectures.sort(key = lambda lecture : (lecture[0], lecture[1])&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;힙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://littlefoxdiary.tistory.com/3&quot;&gt;[Python] 힙 자료구조 / 힙큐(heapq) / 파이썬에서 heapq 모듈 사용하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kjhoon0330.tistory.com/entry/Python-heapq-%EB%AA%A8%EB%93%88&quot;&gt;[Python] heapq 모듈&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 최소힙의 형태로 정렬되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙 함수&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;heapq.heappush(heap, item)&lt;/code&gt; : item을 heap에 저장&lt;/li&gt;
&lt;li&gt;&lt;code&gt;heapq.heappop(heap)&lt;/code&gt; : 가장 작은 원소를 pop &amp;amp; 리턴. 비어있는 경우 IndexError 호출&lt;/li&gt;
&lt;li&gt;&lt;code&gt;heapq.heapify(x)&lt;/code&gt; : 리스트 x를 heap으로 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;import heapq

pq = []
heapq.heappush(heap, 50)
heapq.heappush(heap, 10)
heapq.heappush(heap, 20)

heap2 = [50, 10, 20]
heap.heapify(heap2)

# 최솟값 확인
print(pq[0])

# 응용 최대 힙
maxHeap = []
heapq.heappush(heap, (-item, item))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;pass, continue, break&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://chancoding.tistory.com/7&quot;&gt;[Python] pass, continue, break 차이점 알아보기&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문자열 반복&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dojang.io/mod/page/view.php?id=1213&quot;&gt;COS Pro 2급 파이썬: 7.3 문자열을 연결하고 반복하기&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 문자열 반복
'str' * 3&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;유니코드 &amp;harr; 문자 변환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://co1nam.tistory.com/94&quot;&gt;Python string을 char로 바꾸는 법 feat. 대소문자 변환&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;s = &quot;123123&quot;
for s in str:
    # 문자을 아스키 코드로 변환
    print(ord(s))

# 아스키 값을 문자로 변환
chr(65)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;defaultdict&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dongdongfather.tistory.com/69&quot;&gt;[파이썬 기초] 유사 딕셔너리 defaultdict() 활용법&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;from collections import defaultdict

# dict와 달리 키 값이 없으면 defaultdict의 callable 매개변수가 동작하여 해당 값으로 초기화
int_dict = defaultdict(int)
print(int_dict['a']) # 0

max_dict = defaultdict(lambda: sys.maxsize)
print(max_dict['a']) # 9223372036854775808

list_dict = defaultdict(list)
print(list_dict['a']) # []&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비트연산자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://yiyj1030.tistory.com/267&quot;&gt;[ 파이썬 python ] 비트 연산을 위해 알아야 할 것들&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;num = 13
# bin()으로 감싸면 이진수 형태의 문자열이 된다.
print(bin(num)) # 이진수로 출력

# bitcount
print(bin(num).count('1'))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비트 마스킹&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://shoark7.github.io/programming/algorithm/sieve-of-eratosthenes-bitmask&quot;&gt;에라토스테네스의 체 Bitmask로 구현하기&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;반올림&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://trivia-starage.tistory.com/201&quot;&gt;Python의 round는 사사오입? 오사오입?&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hyunie-y.tistory.com/61&quot;&gt;파이썬 round 함수의 오류 (2.5가 2가되는 마법 - 사사오입과 오사오입)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.freeism.co.kr/wp/archives/1792&quot;&gt;은행가의 반올림 (Banker's rounding) - ThinkCUBES&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://brightnightsky77.tistory.com/201&quot;&gt;[Python] round 함수 : 반올림 (3) - 사사오입 반올림을 사용하는 방법&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 일반적으로 쓰는 반올림은 &lt;b&gt;5이상에서 올리고, 5미만에서 버리는&lt;/b&gt; 사사오입이다. 하지만, 파이썬의 &lt;code&gt;round&lt;/code&gt;함수는 &lt;b&gt;5미만에서 버리고, 5초과에서 올리는&lt;/b&gt; 오사오입이다. 5의 경우 &lt;b&gt;앞자리가 홀수인 경우에 올림을 하고 짝수인 경우에 버림&lt;/b&gt;을 하여 짝수로 만들어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오사오입을 왜 써?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말해서 10개의 숫자 중 6개의 숫자를 올리고 4개의 숫자를 버리는 것은 공평하지 않다는 것이다. 숫자가 중요한 금융권, 공학, 자연과학 계열에서 많이 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬에서 오사오입을 사용하려면 사용자 지정 &lt;code&gt;round&lt;/code&gt;함수를 구현해서 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현에는 3가지 방법이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. decimal 모듈 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 주의할 점은 &lt;b&gt;round 함수에 매개변수를 하나&lt;/b&gt;만 넣으면 decimal 모듈을 써서 반올림 모드를 변경해도 &lt;b&gt;사사오입이 적용되지 않는다&lt;/b&gt;는 점이다.&lt;/p&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;import decimal

# 산술 연산을 위한 환경 불러옴
context = decimal.getcontext()
# 반올림 방식을 사사오입으로 변경
# 오사오입은 decimal.ROUND_HALF_EVEN
context.rounding = decimal.ROUND_HALF_UP

# 양의 정수
round(decimal.Decimal('50'), -2)
# 음의 정수
round(decimal.Decimal('-5'), -1)
# 양의 소수
round(decimal.Decimal('0.5'), 0)
# 음의 소수
round(decimal.Decimal('-0.5'), 0)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 미세한 값을 더해주고 round() 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;가장 편해서 자주 사용한다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반올림해야하는 값이 0.xxx5라고 할 때 0.00001과 같이 해당 값에 영향을 줄 수 있는 가장 큰 값을 선택해서 더해주면 되는 것 같다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;a = 0.5
round(a + 0.0000001)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 직접 구현&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;def round_up(num, digits=0):
        return round(val+10**(-len(str(num))-1, digits)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파일 입력받기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dino-rudy.tistory.com/7&quot;&gt;[백준] Python-input.txt로 입력 받기! -공룡 루디&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;f = open(&quot;a.txt&quot;, &quot;rt&quot;) # 텍스트를 읽기

input = f.readline

N, M = map(int, input().rstrip())&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;for-else와 while-else&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wikidocs.net/190098&quot;&gt;2.5 for-else와 while-else&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;hsp&quot;&gt;&lt;code&gt;N = int(input())

for i in range(100):
        if i == N:
                break
else: # 모든 loop 다 돌면 출력
    print(&quot;done&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행시간 측정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bio-info.tistory.com/143&quot;&gt;[Python] Python 코드 실행시간 측정 4가지 방법 (feat. Jupyter Notebook)&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;import time
import datetime # 시간 예쁘게 출력

start = time.time()
do_something()
end = time.time()

# 출력 -&amp;gt; 00:00:0x.xxxx sec
print(f&quot;{str(datetime.timedelta(seconds = end - start)) sec}&quot;) &lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시간 조작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ctkim.tistory.com/entry/%ED%8C%8C%EC%9D%B4%EC%8D%AC-datetime-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EA%B0%80%EC%9E%A5-%EB%A7%8E%EC%9D%B4-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%ED%95%A8%EC%88%98&quot;&gt;https://ctkim.tistory.com/entry/파이썬-datetime-라이브러리-가장-많이-사용하는-함수&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;any(), all() 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://otugi.tistory.com/206&quot;&gt;https://otugi.tistory.com/206&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;temp = [0, 2, 4, 8, 16]

if any(t &amp;gt; 0 for t in temp):
    print(&quot;a&quot;, end=&quot; &quot;)

if all(t % 2 == 0 for t in temp):
    print(&quot;b&quot;)

# 실행 결과 : a b&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리스트(+셋, 딕셔너리) 컴프리헨션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bio-info.tistory.com/28&quot;&gt;https://bio-info.tistory.com/28&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;# 리스트 컴프리헨션
numbers = [i for i in range(1, 10)]
# 실행결과 : [1, 2, 3, 4, 5, 6, 7, 8, 9]

# if 조건문 사용
even_numbers = [i for i in range(1, 10) if i % 2 == 0]
# 실행결과 : [2, 4, 6, 8]

# if - else 조건문 사용
is_odd_number = [True if i % 2 == 1 else False for i in range(10)]
# 실행 결과 : [False, True, False, True, False, True, False, True, False, True]

# 셋 컴프리헨션
odd_numbers = {i for i in range(10) if i % 2 == 1}
# 실행 결과 : {1, 3, 5, 7, 9}

# 딕셔너리 컴프리헨션
chars_dict = {chr(i): i for i in range(65, 91)}
&quot;&quot;&quot;
실행결과 : 
{'A': 65, 'B': 66, 'C': 67, 'D': 68, 'E': 69, 'F': 70, 'G': 71, 'H': 72, 'I': 73, 'J': 74, 'K': 75, 'L': 76, 'M': 77, 'N': 78, 'O': 79, 'P': 80, 'Q': 81, 'R': 82, 'S': 83, 'T': 84, 'U': 85, 'V': 86, 'W': 87, 'X': 88, 'Y': 89, 'Z': 90}
&quot;&quot;&quot;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Python</category>
      <author>greatwhite</author>
      <guid isPermaLink="true">https://greatwhite.tistory.com/33</guid>
      <comments>https://greatwhite.tistory.com/33#entry33comment</comments>
      <pubDate>Fri, 8 Nov 2024 14:33:28 +0900</pubDate>
    </item>
    <item>
      <title>1245 : 농장 관리 Python</title>
      <link>https://greatwhite.tistory.com/40</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;접근&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평범한 BFS 문제라고 생각했는데 조금 다른 부분이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 많이 풀어봤던 땅 문제들과 다르게 단순히 땅의 개수가 아닌 가장 높은 높이를 기준으로 땅을 세는 방식이다. 초반에 &lt;b&gt;산봉우리와 인접한 격자는 모두 산봉우리의 높이보다 작아야 한다&lt;/b&gt; 이 부분을 잘못봐서 조금 헤멨다. 결국 생각해보니 특정 지점이 가장 높다면 산봉우리로 볼 수 있다는 것으로 해석이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러려면 순차 탐색으로는 찾기 어려울 수 있다. 왜? 산봉우리라고 생각했던 지점이 실은 인접한 격자일 수 있기 때문이다. 그렇다면 '차라리 값을 기준으로 최대힙으로 저장해놓고, 이를 꺼내서 사용하는 방법은 어떨까?'하는 생각이 들었고 이 방향으로 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;del&gt;&lt;b&gt;여기서 &quot;인접하다&quot;의 정의는 X좌표 차이와 Y좌표 차이 모두 1 이하일 경우로 정의된다&lt;/b&gt;. 이부분도 잘못 이해했었다. 처음에는 두 좌표의 차이의 합 즉, 상하좌우만 가능하다고 생각했었는데 대각선도 가능이였다.&lt;/del&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;_map = []
for i in range(N):
    row = list(map(int, input().split()))
    for j in range(M):
        if not row[j]:
            continue
        heappush(heapq, (-row[j], i, j))
    _map.append(row)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 이 부분인 것 같다. 처음 값을 받을 때 높이가 0인 경우에는 산 봉우리가 될 수 없으므로 을 제외하고 높이를 최대힙에 저장한다. 이렇게 함으로써 최대힙에서 산봉우리 먼저 뺼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;ans = 0
while heapq:
    val, r, c = heappop(heapq)
    if v[r][c]:
        continue
    BFS(r, c, -val)
    ans += 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 저장한 최대힙에서 산봉우리 후보를 뽑고, 이를 BFS를 돌려 산봉우리와 인접 격자를 모두 방문시킨다. 이후 산봉우리에 포함되지 않은 후보에 대해서만 BFS를 다시 수행한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 코드&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;import sys
from collections import deque
from heapq import heappush, heappop

# print = sys.stdout.write
input = sys.stdin.readline
# 반드시 아래 두 라인 주석 처리 후 제출
f = open(&quot;input.txt&quot;, &quot;rt&quot;)
input = f.readline
# 반드시 위 두 라인 주석 처리 후 제출
DIRECTIONS = ((-1, -1), (-1, 0), (-1, 1), (0, 1), (1, 1), (1, 0), (1, -1), (0, -1))

heapq = []
N, M = map(int, input().split())
_map = []
for i in range(N):
    row = list(map(int, input().split()))
    for j in range(M):
        if not row[j]:
            continue
        heappush(heapq, (-row[j], i, j))
    _map.append(row)

v = [[False] * M for _ in range(N)]

def BFS(r, c, val):
    q = deque([(r, c, val)])
    v[r][c] = True
    while q:
        y, x, value = q.popleft()

        for dy, dx in DIRECTIONS:
            ny, nx = dy + y, dx + x
            if (
                ny &amp;gt;= N
                or nx &amp;gt;= M
                or ny &amp;lt; 0
                or nx &amp;lt; 0
                or v[ny][nx]
                or not _map[ny][nx]
                or _map[ny][nx] &amp;gt; value
            ):
                continue

            q.append((ny, nx, _map[ny][nx]))
            v[ny][nx] = True

def main():
    ans = 0
    while heapq:
        val, r, c = heappop(heapq)
        if v[r][c]:
            continue
        BFS(r, c, -val)
        ans += 1
    print(ans)

if __name__ == &quot;__main__&quot;:
    main()&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Problem Solving/백준</category>
      <author>greatwhite</author>
      <guid isPermaLink="true">https://greatwhite.tistory.com/40</guid>
      <comments>https://greatwhite.tistory.com/40#entry40comment</comments>
      <pubDate>Mon, 26 Aug 2024 11:33:18 +0900</pubDate>
    </item>
    <item>
      <title>CSRF 토큰 확인 불가 문제 해결</title>
      <link>https://greatwhite.tistory.com/39</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;java.lang.NullPointerException: Cannot invoke &quot;org.example.springsecuritystudy.repository.JpaTokenRepository.findTokenByIdentifier(String)&quot; because &quot;this.jpaTokenRepository&quot; is null&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nvyq8/btsIhK1wchm/plzGrrgIcBXEykBdzXfRWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nvyq8/btsIhK1wchm/plzGrrgIcBXEykBdzXfRWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nvyq8/btsIhK1wchm/plzGrrgIcBXEykBdzXfRWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnvyq8%2FbtsIhK1wchm%2FplzGrrgIcBXEykBdzXfRWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;710&quot; height=&quot;137&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;631&quot; data-origin-height=&quot;90&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vA0lG/btsIgnGjxS3/XyoWgbvpdgcwFOUFgUZAkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vA0lG/btsIgnGjxS3/XyoWgbvpdgcwFOUFgUZAkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vA0lG/btsIgnGjxS3/XyoWgbvpdgcwFOUFgUZAkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvA0lG%2FbtsIgnGjxS3%2FXyoWgbvpdgcwFOUFgUZAkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;708&quot; height=&quot;101&quot; data-origin-width=&quot;631&quot; data-origin-height=&quot;90&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;왜인지 모르겠다..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nv2Ov/btsIiSK1dVe/fehHRrMD9SkY0M5aIw605k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nv2Ov/btsIiSK1dVe/fehHRrMD9SkY0M5aIw605k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nv2Ov/btsIiSK1dVe/fehHRrMD9SkY0M5aIw605k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnv2Ov%2FbtsIiSK1dVe%2FfehHRrMD9SkY0M5aIw605k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;706&quot; height=&quot;124&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;112&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;26&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TFJnF/btsIhvjfCho/gM72Hz2MI0Keb3CO77O4fK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TFJnF/btsIhvjfCho/gM72Hz2MI0Keb3CO77O4fK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TFJnF/btsIhvjfCho/gM72Hz2MI0Keb3CO77O4fK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTFJnF%2FbtsIhvjfCho%2FgM72Hz2MI0Keb3CO77O4fK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;830&quot; height=&quot;26&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;26&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성 시 CustomCsrfTokenRepository를 잘 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;921&quot; data-origin-height=&quot;78&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CtvaA/btsIhAq9xyj/kX7wq7FuuYs2n6ElQs7a70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CtvaA/btsIhAq9xyj/kX7wq7FuuYs2n6ElQs7a70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CtvaA/btsIhAq9xyj/kX7wq7FuuYs2n6ElQs7a70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCtvaA%2FbtsIhAq9xyj%2FkX7wq7FuuYs2n6ElQs7a70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;921&quot; height=&quot;78&quot; data-origin-width=&quot;921&quot; data-origin-height=&quot;78&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;@Autowired&lt;/code&gt; 문제인 줄 알았는데 POST로 접속하게 되면 정상적으로 로그가 다 찍힌다. 그래서 뭐가 문제인지 찾아보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://assu10.github.io/dev/2023/12/17/springsecurity-csrf/&quot;&gt;이 글&lt;/a&gt; 마지막에 있는 Spring Security 6 변화점을 통해 힌트를 얻었다. 공식 문서에서 해당 문구를 찾은 결과 아래와 같이 바뀐점을 찾아 볼 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#deferred-csrf-token&quot;&gt;Cross Site Request Forgery (CSRF) :: Spring Security&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티 6부터는 CsrfToken 의 조회가 필요할 때까지 연기된다. 이는 스프링 시큐리티가 기본적으로 HttpSession 에도 CsrfToken을 저장하기 때문이다. 지연된 CSRF 토큰은 매 요청마다 세션에 요청하지 않음으로써 성능을 향상시킬 수 있다.&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;br /&gt;만약 지연된 토큰을 먼저 조회하고 싶고 모든 요청에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;CsrfToken을 로드하려는 경우 아래와 같이 설정할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        XorCsrfTokenRequestAttributeHandler requestAttributeHandler = new XorCsrfTokenRequestAttributeHandler();
        requestAttributeHandler.setCsrfRequestAttributeName(null);
        http
                .csrf(c -&amp;gt; c
                        .csrfTokenRequestHandler(requestAttributeHandler)
                        .csrfTokenRepository(new CustomCsrfTokenRepository(jpaTokenRepository))
                        .ignoringRequestMatchers(&quot;/ciao&quot;)
                )
                .authorizeHttpRequests(
                        request -&amp;gt; request.anyRequest()
                                .permitAll()
                );

        return http.build();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;csrfRequestAttributeName&lt;/code&gt;을 null로 설정하면 &lt;code&gt;CsrfToken&lt;/code&gt;이 어떤 속성 이름을 사용할지 결정하기 위해서 처음에 반드시 로딩되어야 한다. 이는 &lt;code&gt;CsrfToken&lt;/code&gt;이 매 요청마다 로딩되도록 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;340&quot; data-origin-height=&quot;69&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FRcQI/btsIhc5fDqM/1LuT37b3zZ4Q7iOueK0XI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FRcQI/btsIhc5fDqM/1LuT37b3zZ4Q7iOueK0XI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FRcQI/btsIhc5fDqM/1LuT37b3zZ4Q7iOueK0XI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFRcQI%2FbtsIhc5fDqM%2F1LuT37b3zZ4Q7iOueK0XI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;340&quot; height=&quot;69&quot; data-origin-width=&quot;340&quot; data-origin-height=&quot;69&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 정상적으로 생성되는 것은 확인했다. 하지만 여전히 POST 토큰 값을 함께 보냈을 때 403 Forbidden이 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.linkedin.com/pulse/solving-invalid-csrf-token-found-error-spring-security-oyeleye/&quot;&gt;Solving the &quot;Invalid CSRF token found&quot; Error in Spring Security 6.x&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 찾아보니 공식문서에서 사용한 &lt;code&gt;XorCsrfTokenRequestAttributeHandler&lt;/code&gt; 대신 &lt;code&gt;CsrfTokenRequestAttributeHandler&lt;/code&gt; 를 사용하는 예제를 찾을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 &lt;code&gt;CsrfTokenRequestAttributeHandler&lt;/code&gt; 를 사용하니 정상적으로 POST 요청으로 &lt;code&gt;/hello&lt;/code&gt;에 접근할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;845&quot; data-origin-height=&quot;127&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcfvR6/btsIhAdyZUV/1MgCIMTJePYmTttnAkKDx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcfvR6/btsIhAdyZUV/1MgCIMTJePYmTttnAkKDx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcfvR6/btsIhAdyZUV/1MgCIMTJePYmTttnAkKDx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcfvR6%2FbtsIhAdyZUV%2F1MgCIMTJePYmTttnAkKDx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;845&quot; height=&quot;127&quot; data-origin-width=&quot;845&quot; data-origin-height=&quot;127&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;XorCsrfTokenRequestAttributeHandler&lt;/code&gt;는 BREACH 보호를 위해 &lt;code&gt;CsrfTokenRequestAttributeHandler&lt;/code&gt;의 하위 클래스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-token-request-handler-breach&quot;&gt;Cross Site Request Forgery (CSRF) :: Spring Security&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에 따르면 매 요청마다 CSRF 토큰 값에 무작위 인코딩을 추가해 변경된 토큰 값을 반환하여 침해를 방지해주는 클래스였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;209&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGDrHJ/btsIihLhDLR/P3Gl2bNNZQNEelzdKQaJek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGDrHJ/btsIihLhDLR/P3Gl2bNNZQNEelzdKQaJek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGDrHJ/btsIihLhDLR/P3Gl2bNNZQNEelzdKQaJek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGDrHJ%2FbtsIihLhDLR%2FP3Gl2bNNZQNEelzdKQaJek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;559&quot; height=&quot;209&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;209&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공적으로 POST Hello! 메시지를 받았다!&lt;/p&gt;</description>
      <category>Today I Learned</category>
      <author>greatwhite</author>
      <guid isPermaLink="true">https://greatwhite.tistory.com/39</guid>
      <comments>https://greatwhite.tistory.com/39#entry39comment</comments>
      <pubDate>Sat, 29 Jun 2024 14:43:40 +0900</pubDate>
    </item>
    <item>
      <title>필터에 @Component 등록 시 자동 등록</title>
      <link>https://greatwhite.tistory.com/38</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티 인 액션 9장 공부 이후 필터에 &lt;code&gt;@Component&lt;/code&gt;어노테이션을 걸고 Config 클래스에서 기본 인증 필터 위치에 등록했었다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component // 여기가 문제
public class StaticKeyAuthenticationFilter implements Filter {
    private String authorizationKey;

    public StaticKeyAuthenticationFilter(@Value(&quot;${authorization.key}&quot;) String authorizationKey) {
        this.authorizationKey = authorizationKey;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        var httpRequest = (HttpServletRequest) request;
        var httpResponse = (HttpServletResponse) response;

        String authentication = httpRequest.getHeader(&quot;Authorization&quot;);

        if (authentication == null || !authentication.equals(authorizationKey)) {
            httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }
        filterChain.doFilter(request, response);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class ProjectConfig {
    private static final Logger log = LoggerFactory.getLogger(ProjectConfig.class);
    private AuthenticationProviderService authenticationProvider;

    @Autowired
    public ProjectConfig(@Lazy AuthenticationProviderService authenticationProvider,
                                               StaticKeyAuthenticationFilter staticKeyAuthenticationFilter) {
                this.staticKeyAuthenticationFilter = staticKeyAuthenticationFilter;
        this.authenticationProvider = authenticationProvider;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        Map&amp;lt;String, PasswordEncoder&amp;gt; encoderMap = new HashMap&amp;lt;&amp;gt;();
        encoderMap.put(&quot;bcrypt&quot;, new BCryptPasswordEncoder());
        encoderMap.put(&quot;scrypt&quot;, SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
        return new DelegatingPasswordEncoder(&quot;bcrypt&quot;, encoderMap);
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            **.addFilterAt(staticKeyAuthenticationFilter, BasicAuthenticationFilter.class)**
            .authorizeHttpRequests(
                    request -&amp;gt; request.anyRequest()
                            .permitAll()
            )
            .csrf(Customizer.withDefaults());

        return http.build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10장을 공부하면서 다른 필터를 추가하게 되었고 이 과정에서 Config 클래스에서 &lt;code&gt;addFilterAt()&lt;/code&gt; 메서드와 필드에서 StaticKeyAuthenticationFilter를 제거해줬다. 그 뒤 다른 필터를 테스트하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책에서는 멀쩡하게 통신이 되었지만, 내 코드에서 추가한 필터는 동작하지만 HTTP 401 Unauthorized를 반환했다. 어디가 문제인지 하나씩 제거하다보니 &lt;code&gt;@Component&lt;/code&gt;가 문제인 것을 알게되었고 검색해보니 아래와 같은 내용이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://taetaetae.github.io/2020/04/06/spring-boot-filter/&quot;&gt;스프링 부트에 필터를 '조심해서' 사용하는 두 가지 방법&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽고 로깅해봤는데 아마 11단계에서 StaticKeyAuthenticationFilter가 호출되는 과정에서 &lt;code&gt;Authorization&lt;/code&gt;헤더가 없어 문제가 발생한 줄 알았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;71&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1birB/btsIhy0dXwe/QjwYSOyqFgt3kwciOmI6C0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1birB/btsIhy0dXwe/QjwYSOyqFgt3kwciOmI6C0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1birB/btsIhy0dXwe/QjwYSOyqFgt3kwciOmI6C0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1birB%2FbtsIhy0dXwe%2FQjwYSOyqFgt3kwciOmI6C0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1108&quot; height=&quot;71&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;71&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;curl -v -H &quot;Authorzation:auth_123&quot; http://localhost:8080/hello

...
&amp;lt; HTTP/1.1 401
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 &lt;code&gt;Authorization&lt;/code&gt; 헤더와 키값을 넣어줬는데도 401이 발생했다. 혹시나 해서 아래와 같이 이전에 사용했던 &lt;code&gt;Request-Id&lt;/code&gt; 헤더도 추가해줬더니 정상적으로 응답이 온다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;curl -v -H &quot;Authorization:auth_123&quot; -H &quot;Request-Id:12345&quot; http://localhost:8080/hello

...
&amp;lt; HTTP/1.1 200
...
GET Hello!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 작성된 Config 클래스에는 위의 두 필터를 등록하는 코드가 없다. 그럼에도 불구하고 영향을 미치고 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class ProjectConfig {
    private static final Logger log = LoggerFactory.getLogger(ProjectConfig.class);
    private AuthenticationProviderService authenticationProvider;
    @Autowired
    public ProjectConfig(@Lazy AuthenticationProviderService authenticationProvider) {

        this.authenticationProvider = authenticationProvider;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        Map&amp;lt;String, PasswordEncoder&amp;gt; encoderMap = new HashMap&amp;lt;&amp;gt;();
        encoderMap.put(&quot;bcrypt&quot;, new BCryptPasswordEncoder());
        encoderMap.put(&quot;scrypt&quot;, SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
        return new DelegatingPasswordEncoder(&quot;bcrypt&quot;, encoderMap);
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
//                .authenticationProvider(authenticationProvider)
//                .formLogin(
//                        customizer -&amp;gt; customizer
//                                .defaultSuccessUrl(&quot;/hello&quot;, true))
//                .httpBasic(Customizer.withDefaults())
                .addFilterAfter(new CsrfTokenLogger(), CsrfFilter.class)
                .authorizeHttpRequests(
                        request -&amp;gt; request.anyRequest()
                                .permitAll()
                )
                .csrf(Customizer.withDefaults());

        return http.build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히 어떻게 돌아가는지는 잘 모르겠지만 &lt;code&gt;@Component&lt;/code&gt; 어노테이션이 붙은 &lt;code&gt;StaticKeyAuthenticationFilter&lt;/code&gt;가 빈으로 등록되면서 &lt;code&gt;RequestValidationFilter&lt;/code&gt; 역시 영향을 받아 함께 등록된 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Component&lt;/code&gt; 어노테이션을 제거하면 정상적으로 응답에 GET Hello!가 잘 도착한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;curl -v http://localhost:8080/hello

...
&amp;lt; HTTP/1.1 200
...
GET Hello!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터 클래스에 &lt;code&gt;@Component&lt;/code&gt;를 붙이는 것은 주의해야 할 것 같고, 필요없는 필터 클래스 역시 생성하지 않는 것이 좋을 것 같다.&lt;/p&gt;</description>
      <category>Today I Learned</category>
      <author>greatwhite</author>
      <guid isPermaLink="true">https://greatwhite.tistory.com/38</guid>
      <comments>https://greatwhite.tistory.com/38#entry38comment</comments>
      <pubDate>Fri, 28 Jun 2024 15:28:21 +0900</pubDate>
    </item>
    <item>
      <title>스프링 시큐리티 인 액션] 5장_인증</title>
      <link>https://greatwhite.tistory.com/37</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;인증 논리를 담당하는 부분은 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 이다. &lt;code&gt;AuthenticationManager&lt;/code&gt;는 HTTP 요청을 수신하고 &lt;code&gt;AuthenticationProvider&lt;/code&gt;에게 인증 책임을 위임한다. 이 단원에서는 인증 결과가 두 가지인 인증 프로세스를 살펴본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요청하는 엔티티가 인증되지 않는다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 사용자를 인식하지 못해 권한 부여 프로세스에 위임하지 않고 요청을 거부한다. 일반적으로 이 경우 클라이언트에 HTTP 401 Unauthorized 응답이 반환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요청하는 엔티티가 인증된다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청자의 &lt;code&gt;UserDetails&lt;/code&gt; 가 &lt;code&gt;SecurityContext&lt;/code&gt;에 저장되어 애플리케이션이 이를 권한 부여에 이용할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AuthenticationProvider의 이해&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떠한 시나리오가 주어지더라도 구현할 수 있게 해주는 것이 프레임워크의 목적이다. 엔터프라이즈 애플리케이션에서는 사용자의 이름과 암호 기반의 기본 인증 구현이 적합하지 않을 수 있다. 또한 인증과 관련해서 여러 시나리오를 구현해야 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티에서는 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 계약으로 모든 맞춤형 인증 논리를 정의할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인증 프로세스 중 요청 나타내기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AuthenticationProvider&lt;/code&gt;를 구현하기 위해서는 인증 이벤트 자체를 나타내는 방법을 이해해야 한다. &lt;code&gt;Authentication&lt;/code&gt; 은 인증 프로세스의 필수 인터페이스이다. 이 인터페이스는 인증 요청의 이벤트를 나타내며 애플리케이션에 접근을 요청한 엔티티의 세부 정보를 담는다. 인증 요청 이벤트와 관한 정보는 인증 프로세스 도중 그리고 이후에 사용할 수 있다. 애플리케이션에 접근을 요청하는 사용자를 &lt;b&gt;주체(Principal)&lt;/b&gt;이라고 한다. 스프링 시큐리티의 &lt;code&gt;Authentication&lt;/code&gt; 인터페이스는 자바 시큐리티 API의 &lt;code&gt;Principal&lt;/code&gt;인터페이스를 확장한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;986&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C671h/btsIfxu6wBl/5MfKBlvhX48MHKA42OtE5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C671h/btsIfxu6wBl/5MfKBlvhX48MHKA42OtE5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C671h/btsIfxu6wBl/5MfKBlvhX48MHKA42OtE5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC671h%2FbtsIfxu6wBl%2F5MfKBlvhX48MHKA42OtE5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;250&quot; height=&quot;394&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;986&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;스프링 시큐리티의 &lt;code&gt;Authentication&lt;/code&gt; 계약은 주체만 나타내는 것이 아니라 인증 프로세스 완료 여부, 권한의 컬렉션 같은 정보를 추가로 가진다. 이 계약은 자바 시큐리티의 &lt;code&gt;Principal&lt;/code&gt; 계약을 &lt;code&gt;extends&lt;/code&gt; 하여 설계되었다. 따라서, 다른 프레임워크나 애플리케이션 구현에서 호환성이 높다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;291&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cht2Lu/btsIhwOPWwg/dOhkyCVTxoHvRd9vB4IYvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cht2Lu/btsIhwOPWwg/dOhkyCVTxoHvRd9vB4IYvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cht2Lu/btsIhwOPWwg/dOhkyCVTxoHvRd9vB4IYvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcht2Lu%2FbtsIhwOPWwg%2FdOhkyCVTxoHvRd9vB4IYvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;797&quot; height=&quot;271&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;291&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 이 계약에서 알아야 할 메서드는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;isAuthenticated()&lt;/code&gt; - 인증 프로세스가 끝났으면 true를 아직 진행 중이면 false를 반환한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getCredentials()&lt;/code&gt; - 인증 프로세스에 이용된 암호나 비밀번호를 반환한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getAuthorities()&lt;/code&gt; - 인증된 요청에 허가된 권환의 컬렉션을 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Custom AuthenticationProvider 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AuthenticationProvider&lt;/code&gt;의 기본 구현은 &lt;code&gt;UserDetailsService&lt;/code&gt;에서 사용자를 찾고 &lt;code&gt;PasswordEncoder&lt;/code&gt;에서 사용자의 암호를 검증한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;818&quot; data-origin-height=&quot;120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mJK9U/btsIgkIE8WC/VAljdVM3YvbDZzfsKVB7K0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mJK9U/btsIgkIE8WC/VAljdVM3YvbDZzfsKVB7K0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mJK9U/btsIgkIE8WC/VAljdVM3YvbDZzfsKVB7K0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmJK9U%2FbtsIgkIE8WC%2FVAljdVM3YvbDZzfsKVB7K0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;777&quot; height=&quot;114&quot; data-origin-width=&quot;818&quot; data-origin-height=&quot;120&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;code&gt;AuthenticationProvider&lt;/code&gt; 책임은 &lt;code&gt;Authentication&lt;/code&gt;과 강하게 결합되어 있다. 인증 로직을 정의하려면 &lt;code&gt;authenticate&lt;/code&gt; 메서드를 구현해야 하는데 구현 방법을 아래 세 항목으로 간단하게 요약할 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인증에 실패하면 &lt;code&gt;AuthenticationException&lt;/code&gt;을 발생시킨다.&lt;/li&gt;
&lt;li&gt;현재 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 구현에서 지원하지 않는 증명 방식이면 &lt;code&gt;null&lt;/code&gt;을 반환한다. HTTP 필터 수준에서 분리된 여러 &lt;code&gt;Authentication&lt;/code&gt; 형식을 사용할 가능성이 생긴다.&lt;/li&gt;
&lt;li&gt;인증이 완료되었다면 &lt;code&gt;authenticate&lt;/code&gt; 메서드의 결과물로 &lt;code&gt;Authentication&lt;/code&gt; 인스턴스를 반환해야 한다. 이 인스턴스에 대해 &lt;code&gt;isAuthenticated&lt;/code&gt; 메서드는 true를 반환해야 한다. 또한 인증이 완료되었으므로 비밀빈호와 같은 &lt;b&gt;민감 정보는 제거하는 것이 좋다&lt;/b&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AuthenticationProvider&lt;/code&gt;의 두 번째 메서드는 &lt;code&gt;supports(Class&amp;lt;?&amp;gt; authentication)&lt;/code&gt;다. 이 메서드는 현재 요청이 지원하는 인증 형식이면 true를 반환한다. 하지만 이 메서드에서 true를 반환했다고 하더라도 &lt;code&gt;authenticate&lt;/code&gt; 메서드에서 null을 반환할 수 있는데 이는 &lt;b&gt;인증 세부 정보를 기준으로 요청을 거부했기 때문&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AuthenticationManager&lt;/code&gt; 는 사용가능한 인증 공급자 중 하나에 인증을 위임한다. &lt;code&gt;AuthenticationProvider&lt;/code&gt;는 주어진 인증 유형을 지원하지 않거나 객체 유형은 지원하지만 해당 특정 객체를 인증하는 방법을 모를 수 있다. 인증을 평가한 후 요청이 올바른지 판단할 수 있는 &lt;code&gt;AuthenticationProvider&lt;/code&gt;가 &lt;code&gt;AuthenticationManager&lt;/code&gt;에 응답한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

        // 생략된 코드

    @Override
    public boolean supports(Class&amp;lt;?&amp;gt; authenticationType) {
        return authenticationType.equals(UsernamePasswordAuthenticationToken.class);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 종류의 &lt;code&gt;Authentication&lt;/code&gt;을 지원할지 정의해야 한다. 이는 &lt;code&gt;authentication&lt;/code&gt; 메서드의 매개 변수에 어떤 형식이 전달되는지에 따라 달라진다. &lt;code&gt;AuthenticationFilter&lt;/code&gt;에서 별다른 구성을 하지 않았다면 &lt;code&gt;UsernamePasswordAuthenticationToken&lt;/code&gt; 클래스가 형식을 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 형식을 지원할지 결정했으므로 &lt;code&gt;authenticate&lt;/code&gt; 메서드를 구현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        UserDetails u = userDetailsService.loadUserByUsername(username);

        if (passwordEncoder.matches(password, u.getPassword())) {
            return new UsernamePasswordAuthenticationToken(
                    username,
                    password,
                    u.getAuthorities()
            );
        }
        throw new BadCredentialsException(&quot;Something went wrong!&quot;);
    }

    @Override
    public boolean supports(Class&amp;lt;?&amp;gt; authenticationType) {
        return authenticationType.equals(UsernamePasswordAuthenticationToken.class);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;UserDetails&lt;/code&gt;를 가져오기 위해 &lt;code&gt;UserDetailsService&lt;/code&gt; 구현을 사용한다. 이 때 사용자를 찾지 못하거나 &lt;code&gt;PasswordEncoder&lt;/code&gt;로 검증했을 때 비밀번호가 일치하지 않는다면 &lt;code&gt;AuthenticationException&lt;/code&gt;을 발생시킨다. 그러면 인증 프로세스는 중단되고 인증 필터가 응답 상태를 401 Unauthorized를 설정한다. 인증이 성공된 경우 요청의 세부정보를 포함하는 &lt;code&gt;Authentication&lt;/code&gt;을 인증됨으로 표시하고 반환한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2020&quot; data-origin-height=&quot;1458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LN1tG/btsIhhYCNP4/HJ9HSBiNVn8kqtSBJkkBl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LN1tG/btsIhhYCNP4/HJ9HSBiNVn8kqtSBJkkBl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LN1tG/btsIhhYCNP4/HJ9HSBiNVn8kqtSBJkkBl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLN1tG%2FbtsIhhYCNP4%2FHJ9HSBiNVn8kqtSBJkkBl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;609&quot; height=&quot;440&quot; data-origin-width=&quot;2020&quot; data-origin-height=&quot;1458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 그림으로 나타내면 위와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 구현한 &lt;code&gt;AuthenticationProvider&lt;/code&gt;를 연결하려면 프로젝트의 구성 클래스에서 추가해줘야 한다. 책에서는 deprecated된 &lt;code&gt;WebSecurityConfigAdapter&lt;/code&gt;를 사용해 예시를 조금 수정했다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class ProjectConfig{
    @Bean
    public UserDetailsService userDetailsService(DataSource dataSource) {
        String usersByUsernameQuery =
                &quot;select username, password, enabled from users where username = ?&quot;;
        String authByUserQuery =
                &quot;select username, authority from authorities where username = ?&quot;;

        var userDetailsManager = new JdbcUserDetailsManager(dataSource);
        userDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery);
        userDetailsManager.setAuthoritiesByUsernameQuery(authByUserQuery);
        return userDetailsManager;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationProvider authenticationProvider) {
        return new ProviderManager(authenticationProvider);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html#servlet-authentication-providermanager&quot;&gt;&lt;code&gt;ProviderManager&lt;/code&gt;&lt;/a&gt;는 &lt;code&gt;AuthenticationManager&lt;/code&gt;의 대표 구현체이다. &lt;code&gt;AuthenticationProvider&lt;/code&gt; 리스트나 가변 인자를 전달하면 전달된 여러 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 를 가지는 &lt;code&gt;ProviderManager&lt;/code&gt;가 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 &lt;code&gt;@Component&lt;/code&gt;어노테이션을 &lt;code&gt;CustomAuthenticationProvider&lt;/code&gt;에 추가해줬기 때문에 스프링 컨텍스트가 이를 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SecurityContext 이용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 프로세스가 끝난 이후에도 엔티티 세부정보가 필요하다. 사용자가 어떤 권한을 가지는지에 따라 할 수 있는 작업이 다르기 때문이다. 이 때문에 인증이 끝나도 요청이 유지되는 동안 &lt;code&gt;Authentication&lt;/code&gt; 객체를 &lt;code&gt;SecurityContext&lt;/code&gt;에 저장한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;113&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNlK79/btsIhbqEnQS/rWiG9wSylyYc7LXbjIZjbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNlK79/btsIhbqEnQS/rWiG9wSylyYc7LXbjIZjbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNlK79/btsIhbqEnQS/rWiG9wSylyYc7LXbjIZjbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNlK79%2FbtsIhbqEnQS%2FrWiG9wSylyYc7LXbjIZjbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;662&quot; height=&quot;113&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;113&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SecurityContext&lt;/code&gt;계약의 핵심 책임은 &lt;code&gt;Authentication&lt;/code&gt; 을 저장하는 것이다. 그리고 스프링 시큐리티는 &lt;code&gt;SecurityContextHolder&lt;/code&gt; 라는 관리자 역할 객체로 세 가지 전략을 통해 &lt;code&gt;SpringSecurityContext&lt;/code&gt;를 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MODE_THREADLOCAL&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 스레드가 &lt;code&gt;SecurityContext&lt;/code&gt;에 각자의 세부정보를 저장할 수 있게 해준다. 요청 당 스레드 방식의 웹 애플리케이션에서는 각 요청이 개별 스레드를 가지므로 일반적인 접근이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MODE_INHERITABLETHREADLOCAL&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MODE_THREADLOCAL과 비슷하지만 비동기 메서드의 경우 &lt;code&gt;SecurityContext&lt;/code&gt;를 다음 스레드로 복사하도록 스프링 시큐리티에 지시한다. 이 방식으로 &lt;code&gt;@Async&lt;/code&gt; 메서드를 실행하는 새 스레드가 보안 컨텍스트를 상속하게 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MODE_GLOBAL&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션의 모든 스레드가 같은 &lt;code&gt;SecurityContext&lt;/code&gt;인스턴스를 보게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 세 가지 전략 외에 개발자가 스프링에 알려지지 않은 새 스레드를 정의하면 명시적으로 &lt;code&gt;SecurityContext&lt;/code&gt;의 세부 정보를 새 스레드로 복사해야 한다. 스프링 시큐리티는 스프링 컨텍스트에 있지 않은 객체를 자동으로 관리할 수 없지만, 이를 위해 유용한 유틸리티 클래스를 제공한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SecurityContext 기본 전략 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MODE_THREADLOCAL&lt;/code&gt;은 스프링 시큐리티가 &lt;code&gt;SecurityContext&lt;/code&gt;를 관리하는 기본 전략이다. &lt;code&gt;ThreadLocal&lt;/code&gt;은 JDK에 있는 구현이며 이 구현은 각 스레드가 &lt;b&gt;컬렉션에 저장된 데이터만 볼 수 있도록 보장&lt;/b&gt;한다. 각 요청은 자신의 &lt;code&gt;SecurityContext&lt;/code&gt;에 접근하며, 다른 스레드의 &lt;code&gt;ThreadLocal&lt;/code&gt;에 접근할 수 없다. 아래와 같이 T1&amp;rsquo;에 새 스레드가 생길 경우 기존 SecurityContext A의 세부 내용이 복사되지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1396&quot; data-origin-height=&quot;1104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2bfZd/btsIgoKZrTi/17EKoAiBA1Gqpjwf0tC8O1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2bfZd/btsIgoKZrTi/17EKoAiBA1Gqpjwf0tC8O1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2bfZd/btsIgoKZrTi/17EKoAiBA1Gqpjwf0tC8O1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2bfZd%2FbtsIgoKZrTi%2F17EKoAiBA1Gqpjwf0tC8O1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;524&quot; height=&quot;414&quot; data-origin-width=&quot;1396&quot; data-origin-height=&quot;1104&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SecurityContext&lt;/code&gt;를 관리하는 기본 전략이므로 명시적으로 구성할 필요가 없다. 인증 프로세스가 끝난 이후 필요할 때마다 &lt;code&gt;SecurityContextHolder.getContext()&lt;/code&gt; 메서드를 통해 &lt;code&gt;SecurityContext&lt;/code&gt;를 요청만 하면 된다. 이후 &lt;code&gt;SecurityContext&lt;/code&gt;에서 &lt;code&gt;getAuthentication&lt;/code&gt; 메서드로 &lt;code&gt;Authentication&lt;/code&gt; 을 얻올 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;398&quot; data-origin-height=&quot;37&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MxxLQ/btsIf0KmUx0/JsAlqw4MKi3cqVS2YBXML0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MxxLQ/btsIf0KmUx0/JsAlqw4MKi3cqVS2YBXML0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MxxLQ/btsIf0KmUx0/JsAlqw4MKi3cqVS2YBXML0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMxxLQ%2FbtsIf0KmUx0%2FJsAlqw4MKi3cqVS2YBXML0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;398&quot; height=&quot;37&quot; data-origin-width=&quot;398&quot; data-origin-height=&quot;37&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 전략을 고수하는 것이 더 쉬우며 대부분은 이 전략으로 충분하다. &lt;code&gt;MODE_THREADLOCAL&lt;/code&gt;은 각 스레드의 &lt;code&gt;SecurityContext&lt;/code&gt;를 격리할 수 있게 해주고 &lt;code&gt;SecurityContext&lt;/code&gt;를 더 자연스럽고 이해하기 쉽게 만들어준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비동기 호출을 위한 전략 이용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 요청당 여러 스레드가 사용될 때는 상황이 복잡하다. 엔드포인트가 비동기가 되면 요청을 처리하는 스레드와 메서드를 실행하는 스레드가 다른 스레드가 된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@GetMapping(&quot;/bye&quot;)
@Async
public void goodbye() {
    SecurityContext context = SecurityContextHolder.getContext();
    String name = context.getAuthentication().getName();

    // 사용자 이름으로 작업
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Async&lt;/code&gt; 기능을 활성화하기 위해 &lt;code&gt;@EnableAsync&lt;/code&gt; 어노테이션을 &lt;code&gt;Config&lt;/code&gt; 클래스에 추가해줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@EnableAsync
public class ProjectConfig {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;String username = context.getAuthentication.getName();&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 상태로 코드를 실행해보면 인증에서 이름을 얻는 다음 행에서 &lt;code&gt;NullPointerException&lt;/code&gt;이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 &lt;code&gt;SecurityContext&lt;/code&gt;를 상속하지 않는 다른 스레드에서 실행되기 때문이다. 이를 &lt;code&gt;MODE_INHERITABLETHREADLOCAL&lt;/code&gt; 전략으로 해결할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NqHMV/btsIfK8Qbo8/t17iKLCv2n2gpVgJsrtda1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NqHMV/btsIfK8Qbo8/t17iKLCv2n2gpVgJsrtda1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NqHMV/btsIfK8Qbo8/t17iKLCv2n2gpVgJsrtda1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNqHMV%2FbtsIfK8Qbo8%2Ft17iKLCv2n2gpVgJsrtda1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;804&quot; height=&quot;202&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;202&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SecurityContextHolder&lt;/code&gt;클래스를 살펴보면 strategyname 필드가 있다. 이 필드는 &lt;code&gt;spring.security.strategy&lt;/code&gt; 에서 값을 가져오는 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;506&quot; data-origin-height=&quot;102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clVM9n/btsIhA4ILUv/RNIhzsx6X0NLZ6dRrmQOAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clVM9n/btsIhA4ILUv/RNIhzsx6X0NLZ6dRrmQOAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clVM9n/btsIhA4ILUv/RNIhzsx6X0NLZ6dRrmQOAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclVM9n%2FbtsIhA4ILUv%2FRNIhzsx6X0NLZ6dRrmQOAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;506&quot; height=&quot;102&quot; data-origin-width=&quot;506&quot; data-origin-height=&quot;102&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 &lt;code&gt;setStrategyName&lt;/code&gt;메서드로 설정해줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 전략을 사용하면 프레임워크는 요청의 원래 스레드에 있는 세부 정보를 비동기 메서드의 새로 생성된 스레드로 복사한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1396&quot; data-origin-height=&quot;1104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d5shrJ/btsIhaFuqtU/CYzW1VCnV90JRX8TvPuXPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d5shrJ/btsIhaFuqtU/CYzW1VCnV90JRX8TvPuXPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d5shrJ/btsIhaFuqtU/CYzW1VCnV90JRX8TvPuXPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd5shrJ%2FbtsIhaFuqtU%2FCYzW1VCnV90JRX8TvPuXPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;465&quot; height=&quot;368&quot; data-origin-width=&quot;1396&quot; data-origin-height=&quot;1104&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;@Bean
public InitializingBean initializingBean() {
    return () -&amp;gt; SecurityContextHolder.setStrategyName(
            SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ProjectConfig 클래스에 위 메서드를 추가하면 &lt;code&gt;/bye&lt;/code&gt; 엔드포인트에 접근했을 때 &lt;code&gt;Authentication&lt;/code&gt; 이 제대로 복사된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 빈을 등록하지 않으면 아래와 같은 오류가 발생한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;java.lang.NullPointerException: Cannot invoke &quot;org.springframework.security.core.Authentication.getName()&quot; because the return value of &quot;org.springframework.security.core.context.SecurityContext.getAuthentication()&quot; is null&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈을 등록하면 정상 출력된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;25&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dnCBIA/btsIgnrMRlx/aDhWUdU79p9HOWaiK8Qi31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dnCBIA/btsIgnrMRlx/aDhWUdU79p9HOWaiK8Qi31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dnCBIA/btsIgnrMRlx/aDhWUdU79p9HOWaiK8Qi31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdnCBIA%2FbtsIgnrMRlx%2FaDhWUdU79p9HOWaiK8Qi31%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;826&quot; height=&quot;24&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;25&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;839&quot; data-origin-height=&quot;24&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLPOdz/btsIhgMcgkf/uk9JsrqF2bBe3mgrI4CaLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLPOdz/btsIhgMcgkf/uk9JsrqF2bBe3mgrI4CaLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLPOdz/btsIhgMcgkf/uk9JsrqF2bBe3mgrI4CaLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLPOdz%2FbtsIhgMcgkf%2Fuk9JsrqF2bBe3mgrI4CaLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;839&quot; height=&quot;24&quot; data-origin-width=&quot;839&quot; data-origin-height=&quot;24&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위와 같이 시작 시 옵션을 줘서 실행할 수도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;21&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1e309/btsIgEmABNl/JA9TelrJGedna8njZs6eqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1e309/btsIgEmABNl/JA9TelrJGedna8njZs6eqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1e309/btsIgEmABNl/JA9TelrJGedna8njZs6eqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1e309%2FbtsIgEmABNl%2FJA9TelrJGedna8njZs6eqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;950&quot; height=&quot;21&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;21&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;독립형 애플리케이션을 위한 전략 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SecurityContext&lt;/code&gt;가 애플리케이션의 모든 스레드에 공유되는 전략을 원한다면 &lt;code&gt;MODE_GLOBAL&lt;/code&gt;을 이용하면 된다. 이 전략은 일반적인 웹 애플리케이션과 맞지 않기 때문에 웹 서버에서는 사용되지 않는다. 독립형 애플리케이션에는 공유하는 것이 좋은 전략일 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1EWOP/btsIg7aTZlQ/nAY6qsqhrZlGg5QQciORMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1EWOP/btsIg7aTZlQ/nAY6qsqhrZlGg5QQciORMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1EWOP/btsIg7aTZlQ/nAY6qsqhrZlGg5QQciORMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1EWOP%2FbtsIg7aTZlQ%2FnAY6qsqhrZlGg5QQciORMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;451&quot; height=&quot;305&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;@Bean
public InitializingBean initializingBean() {
        return () -&amp;gt; SecurityContextHolder.setStrategyName(
                SecurityContextHolder.MODE_GLOBAL);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 전략에서는 애플리케이션의 모든 스레드가 &lt;code&gt;SecurityContext&lt;/code&gt; 객체에 접근할 수 있으므로 개발자가 &lt;b&gt;동시 접근을 해결&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DelegatingSecurityContextRunnable로 SecurityContext 전달&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 생성된 스레드의 경우 스프링 컨텍스트가 알고 있는 경우에는 &lt;code&gt;MODE_INHERITABLETHREADLOCAL&lt;/code&gt; 전략을 통해 &lt;code&gt;SecurityContext&lt;/code&gt;를 복사할 수 있었다. 그러나 프레임워크가 모르는 방법으로 코드가 새 스레드를 시작하면 여전히 빈틈이 생긴다. 이러한 스레드는 프레임워크가 관리해주지 않아 개발자가 관리해야 하므로 &lt;b&gt;자체 관리&lt;/b&gt; 스레드라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자체 관리 스레드는 개발자가 &lt;code&gt;SecurityContext&lt;/code&gt;를 전파해야 한다. 한 가지 해결책은 별도의 스레드에서 실행하고 싶은 작업을 &lt;code&gt;DelegatingSecurityContextRunnable&lt;/code&gt; 또는 &lt;code&gt;DelegatingSecurityContextCallable&amp;lt;T&amp;gt;&lt;/code&gt;을 사용하는 것이다. 반환 값이 없다면 &lt;code&gt;Runnable&lt;/code&gt;을, 있다면 &lt;code&gt;Callable&amp;lt;T&amp;gt;&lt;/code&gt; 를 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 클래스 모두 다른 &lt;code&gt;Runnable&lt;/code&gt; 또는 &lt;code&gt;Callable&lt;/code&gt;과 마찬가지로 비동기적으로 실행되는 작업을 나타내며, 작업을 실행하는 스레드를 위해 현재 &lt;code&gt;SecurityContext&lt;/code&gt;를 복사시켜 준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWxZSy/btsIfFzJsMY/oyk52q8GKID3O8ntPLtRY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWxZSy/btsIfFzJsMY/oyk52q8GKID3O8ntPLtRY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWxZSy/btsIfFzJsMY/oyk52q8GKID3O8ntPLtRY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWxZSy%2FbtsIfFzJsMY%2Foyk52q8GKID3O8ntPLtRY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;493&quot; height=&quot;302&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@GetMapping(&quot;/ciao&quot;)
public String ciao() throws Exception {
    Callable&amp;lt;String&amp;gt; task = () -&amp;gt; { // Callable 작업 선언
        SecurityContext context = SecurityContextHolder.getContext();
        return context.getAuthentication().getName(); // 현재 Authentication 이름 반환
    };
    ExecutorService e = Executors.newCachedThreadPool();
    try {
        return &quot;Ciao, &quot; + e.submit(task).get() + &quot;!&quot;;
    } finally {
        e.shutdown();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 상태로 실행하면 &lt;code&gt;SecurityContext&lt;/code&gt;를 복사해주지 않았기 때문에 &lt;code&gt;NPE&lt;/code&gt;가 발생한다. 따라서, &lt;code&gt;DelegatingSecurityContextCallable&lt;/code&gt;로 감싸줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;@GetMapping(&quot;/ciao&quot;)
public String ciao() throws Exception {
    Callable&amp;lt;String&amp;gt; task = () -&amp;gt; {
        SecurityContext context = SecurityContextHolder.getContext();
        return context.getAuthentication().getName();
    };
    ExecutorService e = Executors.newCachedThreadPool();
    try {
        var contextTask = new DelegatingSecurityContextCallable&amp;lt;&amp;gt;(task);
        return &quot;Ciao, &quot; + e.submit(contextTask).get() + &quot;!&quot;;
    } finally {
        e.shutdown();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;383&quot; data-origin-height=&quot;33&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/butSph/btsIhvh6u9X/FA0DgZG2w5b2upw8zOMUNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/butSph/btsIhvh6u9X/FA0DgZG2w5b2upw8zOMUNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/butSph/btsIhvh6u9X/FA0DgZG2w5b2upw8zOMUNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbutSph%2FbtsIhvh6u9X%2FFA0DgZG2w5b2upw8zOMUNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;383&quot; height=&quot;33&quot; data-origin-width=&quot;383&quot; data-origin-height=&quot;33&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위와 같이 바꿔주면 제대로 출력된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DelegatingSecurityContextExecutorService로 SecurityContext 전달&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레임워크에 알리지 않고 코드에서 시작한 스레드를 다룰 때는 &lt;code&gt;SecurityContext&lt;/code&gt; 에서 다음 스레드로의 전파를 관리해야 한다. 위에서 살펴보았던 두 클래스는 모두 작업 단위에서 &lt;code&gt;SecurityContext&lt;/code&gt;를 복사했다. 이번에는 스레드 풀에서 전파를 관리하는 법을 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업을 데코레이트하는 대신 특정 유형의 &lt;code&gt;Executor&lt;/code&gt;를 사용할 수 있다. 이번에는 &lt;code&gt;DelegatingSecurityContextExecutorService&lt;/code&gt;가 &lt;code&gt;ExecutorService&lt;/code&gt;를 감싼다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1250&quot; data-origin-height=&quot;664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMEWEF/btsIf2g8Va6/BeSYnQk8tbJtSv0peWN5OK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMEWEF/btsIf2g8Va6/BeSYnQk8tbJtSv0peWN5OK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMEWEF/btsIf2g8Va6/BeSYnQk8tbJtSv0peWN5OK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMEWEF%2FbtsIf2g8Va6%2FBeSYnQk8tbJtSv0peWN5OK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;506&quot; height=&quot;269&quot; data-origin-width=&quot;1250&quot; data-origin-height=&quot;664&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@GetMapping(&quot;/hola&quot;)
public String hola() throws Exception {
    Callable&amp;lt;String&amp;gt; task = () -&amp;gt; {
        SecurityContext context = SecurityContextHolder.getContext();
        return context.getAuthentication().getName();
    };

    ExecutorService e = Executors.newCachedThreadPool();
    e = new DelegatingSecurityContextExecutorService(e);

    try{
        return &quot;Hola, &quot; + e.submit(task).get() + &quot;!&quot;;
    } finally {
        e.shutdown();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&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;DelegatingSecurityContextExecutor&lt;/td&gt;
&lt;td&gt;Executor 인터페이스를 구현하며 Executor 객체를 장식하면 SecurityContext를 해당 풀에 의해 생성된 스레드로 전달하는 기능을 제공한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DelegatingSecurityContextExecutorService&lt;/td&gt;
&lt;td&gt;ExecutorService 인터페이스를 구현하며 ExecutorService 객체를 장식하면 SecurityContext 를 해당 풀에 의해 생성된 스레드로 전달하는 기능을 제공한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DelegatingSecurityContextScheduledExecutorService&lt;/td&gt;
&lt;td&gt;ScheduledExecutorService 인터페이스를 구현하며 ScheduledExecutorService 객체를 장식하면 SecurityContext를 해당 풀에 의해 생성된 스레드로 전달하는 기능을 제공한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DelegatingSecurityContextRunnable&lt;/td&gt;
&lt;td&gt;Runnable 인터페이스를 구현하고 다른 스레드에 의해 실행되며 응답을 반환하지 않는 작업을 나타낸다. Runnable 기능에 더해 새 스레드에서 사용하기 위한 SecurityContext 를 복사한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DelegatingSecurityContextcallable&lt;/td&gt;
&lt;td&gt;Callable인터페이스를 구현하고 다른 스레드에 의해 실행되며 최종적으로 응답을 반환하는 작업을 나타낸다. Callable 기능에 더해 새 스레드에서 사용하기 위한 SecurityContext 를 복사한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTTP Basic 인증과 form 기반 로그인 인증 이해하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 인증 방식으로 &lt;code&gt;HTTP Basic&lt;/code&gt;을 이용했다. 이는 실제 애플리케이션에는 적합하지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HTTP Basic 이용 및 구성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론적인 시나리오에서는 &lt;code&gt;HTTP Basic&lt;/code&gt;으로 충분하지만 더 복잡한 애플리케이션에서는 추가 구성이 필요할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;HTTP Basic&lt;/code&gt;의 명시적인 설정은 아래와 같이 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.httpBasic(Customizer.withDefaults());
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kUs65/btsIgGxZiMJ/1rZfTskydDpQxIkdsAMBK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kUs65/btsIgGxZiMJ/1rZfTskydDpQxIkdsAMBK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kUs65/btsIgGxZiMJ/1rZfTskydDpQxIkdsAMBK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkUs65%2FbtsIgGxZiMJ%2F1rZfTskydDpQxIkdsAMBK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;396&quot; height=&quot;200&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Customizer&amp;lt;T&amp;gt;&lt;/code&gt; 의 함수형 인터페이스를 통해 반환값을 지정하고 &lt;code&gt;HttpSecurity&lt;/code&gt; 의 &lt;code&gt;httpBasic()&lt;/code&gt; 메서드를 호출할 수도 있다. 아래와 같이 영역(realm)을 특정 인증 방식을 이용하는 보호 공간으로 생각할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests((authz) -&amp;gt; authz
                    .anyRequest().authenticated()
            )
            .httpBasic(c -&amp;gt; c.realmName(&quot;OTHER&quot;));
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;92&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IsYAT/btsIgEtj2eX/CfM6vHO0v8lohCbUs8ecI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IsYAT/btsIgEtj2eX/CfM6vHO0v8lohCbUs8ecI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IsYAT/btsIgEtj2eX/CfM6vHO0v8lohCbUs8ecI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIsYAT%2FbtsIgEtj2eX%2FCfM6vHO0v8lohCbUs8ecI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;998&quot; height=&quot;92&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;92&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 이용된 람다식은 &lt;code&gt;Customizer&amp;lt;HttpBasicConfigurer&amp;lt;HttpSecurity&amp;gt;&amp;gt;&lt;/code&gt; 형식의 객체인데, &lt;code&gt;HttpBasicConfigurer&amp;lt;HttpSecurity&amp;gt;&lt;/code&gt; 형식의 매개 변수로 &lt;code&gt;realmName()&lt;/code&gt;을 호출해 영역 이름을 변경할 수 있게 해준다. cURL에 &lt;code&gt;-v&lt;/code&gt; 플래그를 지정하면 반환된 자세한 HTTP 응답을 볼 수 있는데 영역 이름이 OTHER로 변경된 걸 확인할 수 있다. 하지만 WWW-Authenticate 헤더는 응답에서 HTTP 401일 때만 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VDCYf/btsIhiDgn0H/dhVfXJx9swbtI880AkOCYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VDCYf/btsIhiDgn0H/dhVfXJx9swbtI880AkOCYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VDCYf/btsIhiDgn0H/dhVfXJx9swbtI880AkOCYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVDCYf%2FbtsIhiDgn0H%2FdhVfXJx9swbtI880AkOCYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;612&quot; height=&quot;242&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;242&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;code&gt;Customizer&lt;/code&gt;로 인증이 실패했을 때의 응답을 맞춤 구성할 수 있다. 인증이 실패했을 때 클라이언트가 응답에서 특정한 항목을 기대하는 경우 이를 위해 하나 이상의 헤더를 추가하거나 제거해야할 수 있다. 또는 애플리케이션이 민감한 데이터를 클라이언트에 노출하지 않도록 응답 본문을 필터링하는 로직을 작성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증이 실패했을 때 응답을 맞춤 구성하려면 &lt;code&gt;AuthenticationEntryPoint&lt;/code&gt;를 구현하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AuthenticatioEntryPoint&lt;/code&gt;의 &lt;code&gt;commence&lt;/code&gt; 메서드는 &lt;code&gt;HttpServletRequest&lt;/code&gt;, &lt;code&gt;HttpServletResponse&lt;/code&gt;, 그리고 인증 실패를 일으킨 &lt;code&gt;AuthenticationException&lt;/code&gt;을 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AuthenticationEntryPoint&lt;/code&gt; 인터페이스는 스프링 시큐리티 아키텍처에서 &lt;code&gt;ExceptionTranslationManager&lt;/code&gt; 라는 구성 요소에서 직접 사용되며 이 구성 요소는 필터 체인으로 던져진 모든 &lt;code&gt;AccessDeniedException&lt;/code&gt; 및 &lt;code&gt;AuthenticationException&lt;/code&gt; 을 처리한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class CustomEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        response.addHeader(&quot;message&quot;, &quot;something went wrong!&quot;);
        response.sendError(HttpStatus.UNAUTHORIZED.value());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 설정 클래스에서 &lt;code&gt;HTTP Basic&lt;/code&gt; 인증을 위해 직접 만든 CustomEntryPoint를 등록할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests((authz) -&amp;gt; authz
                    .anyRequest().authenticated()
            )
            .httpBasic(c -&amp;gt; {
                c.realmName(&quot;OTHER&quot;);
                **c.authenticationEntryPoint(new CustomEntryPoint());**
            });
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;227&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UYX5z/btsIff9oRXr/EHvU2qk5khCfvY5hFEIrw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UYX5z/btsIff9oRXr/EHvU2qk5khCfvY5hFEIrw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UYX5z/btsIff9oRXr/EHvU2qk5khCfvY5hFEIrw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUYX5z%2FbtsIff9oRXr%2FEHvU2qk5khCfvY5hFEIrw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;612&quot; height=&quot;227&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;227&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;양식 기반 로그인으로 인증 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 웹 기반 애플리케이션에서는 스프링 시큐리티가 제공하는 양식 기반 인증 방식을 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests((authz) -&amp;gt; authz
                    .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해서는 &lt;code&gt;HttpBasic&lt;/code&gt; 대신 &lt;code&gt;formLogin&lt;/code&gt;을 추가하면 된다. 위와 같이 구현하면 아직 다른 정의된 페이지가 없으므로 로그인한 후에는 기본 오류 페이지로 리다이렉션된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식과의 차이는 JSON 형식이 아닌 HTML을 반환하는 엔드포인트를 원한다는 점이다. 이를 위해 &lt;code&gt;resources/static&lt;/code&gt; 경로에 home.html을 생성하고 아래와 같이 Controller를 하나 더 정의해줬다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Controller
Public class HomeController {

        @GetMapping(&quot;/home&quot;)
        public String home() {
                return &quot;home.html&quot;;
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인하지 않고 아무 경로에 접근하려고 하면 로그인 페이지로 리다이렉션되고 이 때 로그인을 하면 원래 가려던 페이지로 리다이렉션된다. &lt;code&gt;formLogin()&lt;/code&gt; 메서드는 &lt;code&gt;FormLoginConfigurer&amp;lt;HttpSecurity&amp;gt;&lt;/code&gt; 형식의 객체를 반환하며 이를 이용해 맞춤 구성을 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
            .formLogin(configurer -&amp;gt; configurer.defaultSuccessUrl(&quot;/home&quot;, true))
            .authorizeHttpRequests((authz) -&amp;gt; authz
                    .anyRequest().authenticated()
            );
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;defaultSuccessUrl()&lt;/code&gt; 메서드로 로그인이 성공하면 보낼 기본 페이지를 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 세부적인 맞춤 구성이 필요하면 &lt;code&gt;AuthenticationSuccessHandler&lt;/code&gt;및 &lt;code&gt;AuthenticationFailureHandler&lt;/code&gt; 객체를 이용할 수 있다. 이러한 인터페이스를 이용하면 인증을 위해 실행되는 논리를 적용할 객체를 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AuthentciationSuccessHandler&lt;/code&gt;의 &lt;code&gt;onAuthenticationSuccess()&lt;/code&gt; 메서드는 매개변수로 서블릿 요청과 응답 그리고 &lt;code&gt;Authentication&lt;/code&gt; 객체를 받는다. 아래와 같이 권한에 따라 다른 리다이렉션을 수행하도록 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        var authorities = authentication.getAuthorities();
        var auth = authorities.stream()
                .filter(a -&amp;gt; a.getAuthority().equals(&quot;read&quot;))
                .findFirst(); // read 권한이 없으면 빈 Optional 객체 반환

        if (auth.isPresent()) {
            response.sendRedirect(&quot;/home&quot;);
            return;
        }
        response.sendRedirect(&quot;/error&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 시나리오에서는 인증에 실패하면 클라이언트에 특정 형식의 응답이 필요한 상황이 있다. 인증에 실패했을 때 실행할 로직을 맞춤 구성하려면 &lt;code&gt;AuthenticationFailureHandler&lt;/code&gt; 를 구현하면 된다. 이 클래스 역시 서블릿 요청과 응답을 갖지만 &lt;code&gt;AuthenticationException&lt;/code&gt;객체를 받는다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        response.setHeader(&quot;failed&quot;, LocalDateTime.now().toString());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 후 두 객체를 사용하려면 &lt;code&gt;successHandler()&lt;/code&gt;와 &lt;code&gt;failureHandler()&lt;/code&gt;로 지정해줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
            .formLogin(configurer -&amp;gt; {
                configurer.defaultSuccessUrl(&quot;/home&quot;, true);
                configurer.successHandler(authenticationSuccessHandler);
                configurer.failureHandler(authenticationFailureHandler);
            })
            .authorizeHttpRequests((authz) -&amp;gt; authz
                    .anyRequest().authenticated()
            );
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 올바른 사용자 이름과 암호를 이용해서 &lt;code&gt;HTTP Basic&lt;/code&gt; 방식으로 &lt;code&gt;/home&lt;/code&gt;에 접근하려고 해도 HTTP 302를 반환한다. 여기에 &lt;code&gt;HTTP Basic&lt;/code&gt;을 추가하면 양식 기반과 HTTP Basic 기반 모두 정상 작동하게 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
            .formLogin(configurer -&amp;gt; {
                configurer.defaultSuccessUrl(&quot;/home&quot;, true);
                configurer.successHandler(authenticationSuccessHandler);
                configurer.failureHandler(authenticationFailureHandler);
            })
            .authorizeHttpRequests((authz) -&amp;gt; authz
                    .anyRequest().authenticated()
            ).httpBasic(Customizer.withDefault());
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Java/Spring</category>
      <category>spring</category>
      <author>greatwhite</author>
      <guid isPermaLink="true">https://greatwhite.tistory.com/37</guid>
      <comments>https://greatwhite.tistory.com/37#entry37comment</comments>
      <pubDate>Fri, 28 Jun 2024 14:51:41 +0900</pubDate>
    </item>
    <item>
      <title>스프링 시큐리티 인 액션] 4장_암호처리</title>
      <link>https://greatwhite.tistory.com/36</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;PasswordEncoder 인터페이스 이해&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 시스템은 암호를 그대로 저장하지 않고 해시를 사용해 저장한다. &lt;code&gt;PasswordEncoder&lt;/code&gt;는 암호를 인코딩하고 인증 프로세스에서 암호가 유효한지 확인한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;203&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k6wGn/btsH7FM3uDK/vMel6e4BfnzuNku3WbIoK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k6wGn/btsH7FM3uDK/vMel6e4BfnzuNku3WbIoK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k6wGn/btsH7FM3uDK/vMel6e4BfnzuNku3WbIoK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk6wGn%2FbtsH7FM3uDK%2FvMel6e4BfnzuNku3WbIoK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;750&quot; height=&quot;203&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;203&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;upgradeEncoding&lt;/code&gt; 메서드는 &lt;code&gt;false&lt;/code&gt;를 반환하도록 기본 구현이 되어 있는데 이를 &lt;code&gt;true&lt;/code&gt; 를 반환하도록 재정의하면 인코딩된 암호를 보안 향상을 위해 다시 인코딩한다. 상황에 따라 재정의해서 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;encode&lt;/code&gt; 와 &lt;code&gt;matches&lt;/code&gt; 메서드는 기능적으로 밀접한 관계가 있어 이 둘을 재정의하려면 기능 면에서 항상 일치해야 한다. &lt;code&gt;PasswordEncoder&lt;/code&gt; 로 인코딩된 암호가 주어진 암호와 같은지 &lt;code&gt;PasswordEncoder&lt;/code&gt;로 확인할 수 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제공된 구현 선택&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티에서 이미 몇 가지 유용한 구현을 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pbkdf2PasswordEncoder - PBKDF2를 사용한다.&lt;/li&gt;
&lt;li&gt;BcryptPasswordEncoder - bcrypt 강력 해싱 함수로 인코딩한다.&lt;/li&gt;
&lt;li&gt;ScryptPasswordEncoder - scrypt 해싱 함수로 인코딩한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;PasswordEncoder p = new Pbkdf2PasswordEncoder();
PasswordEncoder p = new Pbkdf2PasswordEncoder(&quot;secret&quot;);
PasswordEncoder p = new Pbkdf2PasswordEncoder(&quot;secret&quot;, 185000, 256);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;PBKDF2&lt;/code&gt;는 반복 횟수만큼 HMAC을 돌리는 아주 단순하고 느린 해싱 함수이다. 마지막 호출의 세 매개 변수는 각각 인코딩 프로세스에 이용되는 키 값, 암호 인코딩의 반복 횟수, 해시의 크기이다. 해시의 크기가 늘어날 수록 암호가 강력해지지만 리소스 역시 함께 늘어나므로 절충해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 훌륭한 옵션은 &lt;code&gt;bcrypt&lt;/code&gt; 강력 해싱 함수로 암호를 인코딩하는 &lt;code&gt;BcryptPasswordEncoder&lt;/code&gt;가 있다. 매개변수가 없는 생성자로 &lt;code&gt;BcryptPasswordEncoder&lt;/code&gt;를 생성해도 되지만 인코딩 프로세스에 이용되는 로그 라운드를 나타내는 강도 계수를 지정할 수도 있다. 또한, 인코딩에 이용되는 SecureRandom 인스턴스를 변경할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;PasswordEncoder p = new BcryptPasswordEncoder();
PasswordEncoder p = new BcryptPasswordEncoder(4);

SecureRandom s = SecureRandom.getInstanceString();
PasswordEncoder p = new BcryptPasswordEncoder(4, s);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지정하는 로그 라운드 값은 해싱 작업에 이용하는 반복 횟수에 영향을 준다. 반복 횟수는 2로그 라운드로 계산된다. 반복 횟수를 계산하기 위한 로그 라운드 값은 4 ~ 31 사이여야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DelegatingPasswordEncoder를 이용한 여러 인코딩 전략&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 흐름에 암호 일치를 위해 다양한 구현을 적용해야 할 때가 있다. &lt;code&gt;DelegatingPasswordEncoder&lt;/code&gt;는 자체 구현은 없고 &lt;code&gt;PasswordEncoder&lt;/code&gt; 인터페이스를 구현하는 다른 객체에 위임한다. 운영 단계에서 특정 애플리케이션 버전부터 인코딩 알고리즘이 변경된 경우에 &lt;code&gt;DelegatingPasswordEncoder&lt;/code&gt; 객체를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DelegatingPasswordEncoder&lt;/code&gt;를 사용해 해싱하면 해시에 해싱 알고리즘이 &lt;code&gt;{사용된 알고리즘}&lt;/code&gt;형태로 접두사가 추가된다. &lt;code&gt;matches&lt;/code&gt; 메서드를 호출하면 이 접두사를 기준으로 적절한 해싱 알고리즘을 선택해 주어진 비밀번호와 일치하는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 직접 구현하면 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Bean
public PasswordEncoder passwordEncoder() {
        Map&amp;lt;String, PasswordEncoder&amp;gt; encoders = new HashMap&amp;lt;&amp;gt;();

        encoders.put(&quot;noop&quot;, NoOpPasswordEncoder.getInstance());
        encoders.put(&quot;bcrypt&quot;, new BCryptPasswordEncoder());
        encoders.put(&quot;scrypt&quot;, new SCryptPasswordEncoder());

        return new DelegatingPasswordEncoder(&quot;bcrypt&quot;, encoders());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접두사가 없으면 기본 인코더를 이용하고 &lt;code&gt;DelegatingPasswordEncoder&lt;/code&gt;를 생성할 때 첫 번째 매개변수로 지정한다. 위 &lt;code&gt;DelegatingPasswordEncoder&lt;/code&gt;는 기본적으로 &lt;code&gt;BcryptPasswordEncoder&lt;/code&gt;구현에 위임한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;525&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqBFhm/btsH7Z5yuwy/YKdIXImaTZbt2JIW3v7rc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqBFhm/btsH7Z5yuwy/YKdIXImaTZbt2JIW3v7rc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqBFhm/btsH7Z5yuwy/YKdIXImaTZbt2JIW3v7rc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqBFhm%2FbtsH7Z5yuwy%2FYKdIXImaTZbt2JIW3v7rc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;938&quot; height=&quot;525&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;525&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;또는 이미 구현된 &lt;code&gt;PasswordEncoderFactories.creatDelegatingPasswordEncoder&lt;/code&gt;를 호출할 수도 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&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;UserDetails&lt;/td&gt;
&lt;td&gt;스프링 시큐리티가 관리하는 사용자를 나타낸다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GrantedAuthority&lt;/td&gt;
&lt;td&gt;애플리케이션의 목적 내에서 사용자에게 허용되는 작업을 정의한다(예: 읽기, 쓰기, 삭제 등).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UserDetailsService&lt;/td&gt;
&lt;td&gt;사용자의 이름으로 사용자 세부 정보를 검색하는 객체를 나타낸다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UserDetailsManager&lt;/td&gt;
&lt;td&gt;UserDetailsService를 확장한 인터페이스이다. 사용자 컬렉션이나 특정 사용자를 변경할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PasswordEncoder&lt;/td&gt;
&lt;td&gt;암호를 암호화 또는 해시하는 방법과 주어진 인코딩된 문자열을 일반 텍스트 암호와 비교하는 방법을 지정한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스프링 시큐리티 암호화 모듈에 관한 추가 정보&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지 SSCM(Spring Security Crypto Module)을 살펴봤다. 암호화 및 복호화 함수와 키 생성 기능은 자바 언어에서 기본 제공하지 않기 때문에 이러한 기능에 보다 쉽게 접근하기 위한 종속성을 추가할 때 제약이 있다. PasswordEncoder도 SSCM의 일부이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSCM의 두 가지 필수 기능&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;KeyGenerator - 해싱 및 암호화 알고리즘을 위한 키를 생성하는 객체&lt;/li&gt;
&lt;li&gt;Encryptor - 데이터를 암호화 및 복호화하는 객체&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Key Generator 이용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KeyGenerator는 특정한 종류의 키를 생성하는 객체로서 일반적으로 암호화나 해싱 알고리즘에 필요하다. 스프링 시큐리티의 KeyGenerator 구현은 아주 훌륭한 유틸리티 툴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BytesKeyGenerator&lt;/code&gt; 와 &lt;code&gt;StringKeyGenerator&lt;/code&gt;는 &lt;code&gt;KeyGenerator&lt;/code&gt;의 두 가지 주요 유형을 나타내는 인터페이스이며 팩토리 클래스 &lt;code&gt;KeyGenerators&lt;/code&gt;로 직접 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface StringKeyGenerator {
        String generateKey();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;StringKeyGenerator&lt;/code&gt; 를 통해 문자열 키를 얻을 수 있고 이 키는 해싱 또는 암호화 알고리즘의 솔트 값으로 이용된다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;StringKeyGenerator keyGenerator = KeyGenerators.string();
String salt = keyGenerator.generateKey();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1043&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nWQtr/btsH8A5hhZr/HwrmXgkVpjTEDFpugtcvuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nWQtr/btsH8A5hhZr/HwrmXgkVpjTEDFpugtcvuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nWQtr/btsH8A5hhZr/HwrmXgkVpjTEDFpugtcvuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnWQtr%2FbtsH8A5hhZr%2FHwrmXgkVpjTEDFpugtcvuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1043&quot; height=&quot;320&quot; data-origin-width=&quot;1043&quot; data-origin-height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 &lt;code&gt;KeyGenerators.string()&lt;/code&gt; 메서드는 8바이트 키를 생성하고 &lt;code&gt;keyGenerator.generateKey()&lt;/code&gt; 메서드는 생성된 키를 16진수 문자열로 인코딩하여 문자열로 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BytesKeyGenerator&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface BytesKeyGenerator {
        int getKeyLength();
        byte[] generateKey();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 인터페이스에는 키 길이를 바이트 수로 반환하는 &lt;code&gt;getKeyLength()&lt;/code&gt; 메서드가 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom();
byte[] key = keyGenerator.generateKey();
int keyLength = keyGenerator.getKeyLength();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;623&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7WEUW/btsH8CPykzF/AJnnLq5OhvtxIFSSqWXVyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7WEUW/btsH8CPykzF/AJnnLq5OhvtxIFSSqWXVyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7WEUW/btsH8CPykzF/AJnnLq5OhvtxIFSSqWXVyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7WEUW%2FbtsH8CPykzF%2FAJnnLq5OhvtxIFSSqWXVyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;623&quot; height=&quot;486&quot; data-origin-width=&quot;623&quot; data-origin-height=&quot;486&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;KeyGenerators.secureRandom()&lt;/code&gt;은 내부적으로 &lt;code&gt;SecureRandomBytesKeyGenerator&lt;/code&gt; 인스턴스를 생성하는데 8바이트 길이의 키를 생성한다. 여기에 16바이트 키를 생성하고 싶다면 인자로 16을 넣어주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 생성한 &lt;code&gt;BytesKeyGenerator&lt;/code&gt;는 &lt;code&gt;generateKey()&lt;/code&gt; 메서드가 호출될 때마다 고유한 키를 생성한다. 같은 KeyGenerator를 호출했을 때 같은 키값을 반환하는 구현이 필요하다면 &lt;code&gt;KeyGenerators.shared(int length)&lt;/code&gt; 메서드로 &lt;code&gt;BytesKeyGenerator&lt;/code&gt;를 생성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// given
BytesKeyGenerator shared = KeyGenerators.shared(8);

//when
byte[] generatedKey1 = shared.generateKey();
byte[] generatedKey2 = shared.generateKey();

// then
assertEquals(generatedKey1, generatedKey2); // true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Encryptor&lt;/code&gt; 는 암호화 알고리즘을 구현하는 객체다. 암호화와 복호화는 보안을 위한 공통적인 기능이므로 애플리케이션에 이러한 기능이 필요할 가능성이 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템의 구성 요소 간에 데이터를 전송하거나 데이터를 저장할 때 암호화가 필요할 때가 많다. &lt;code&gt;Encryptor&lt;/code&gt;는 암호화와 복호화 작업을 지원하며 SSCM에는 이를 위해 &lt;code&gt;BytesEncryptor&lt;/code&gt;와 &lt;code&gt;TextEncryptor&lt;/code&gt;라는 두 유형의 암호기가 정의돼 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;501&quot; data-origin-height=&quot;113&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cb0RJu/btsH8DOrCZa/8l6LeqE5mp5Z0Zkacc6vr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cb0RJu/btsH8DOrCZa/8l6LeqE5mp5Z0Zkacc6vr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cb0RJu/btsH8DOrCZa/8l6LeqE5mp5Z0Zkacc6vr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcb0RJu%2FbtsH8DOrCZa%2F8l6LeqE5mp5Z0Zkacc6vr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;501&quot; height=&quot;113&quot; data-origin-width=&quot;501&quot; data-origin-height=&quot;113&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;538&quot; data-origin-height=&quot;113&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rFOID/btsH6wDxBIn/cueUUqw1WRYCtDVgAvtoMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rFOID/btsH6wDxBIn/cueUUqw1WRYCtDVgAvtoMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rFOID/btsH6wDxBIn/cueUUqw1WRYCtDVgAvtoMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrFOID%2FbtsH6wDxBIn%2FcueUUqw1WRYCtDVgAvtoMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;538&quot; height=&quot;113&quot; data-origin-width=&quot;538&quot; data-origin-height=&quot;113&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 &lt;code&gt;Encryptor&lt;/code&gt; 는 다른 형식으로 데이터를 처리하며 &lt;code&gt;BytesEncryptor&lt;/code&gt;가 더 범용적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BytesEncryptor&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Encryptor&lt;/code&gt;를 사용하는 옵션에 관해 알아보자. 팩토리 클래스 &lt;code&gt;Encryptors&lt;/code&gt;는 여러 가능성을 제공하며, &lt;code&gt;BytesEncryptor&lt;/code&gt;의 경우 다음과 같이 &lt;code&gt;Encryptors.standard()&lt;/code&gt; 또는 &lt;code&gt;Encryptors.stronger()&lt;/code&gt; 메서드를 이용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;String salt = KeyGenerators.string().generateKey();
String password = &quot;secret&quot;;
String valueToEncrypt = &quot;HELLO&quot;;

BytesEncryptor e = Encryptors.standard(password, salt);
byte[] encrypted = e.encrypt(valueToEncrypt.getBytes());
byte[] decrypted = e.decrypt(encrypted);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnc4AG/btsH8ET8PSq/9mK1mIkl8QnW0qamJHuQ90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnc4AG/btsH8ET8PSq/9mK1mIkl8QnW0qamJHuQ90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnc4AG/btsH8ET8PSq/9mK1mIkl8QnW0qamJHuQ90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcnc4AG%2FbtsH8ET8PSq%2F9mK1mIkl8QnW0qamJHuQ90%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1100&quot; height=&quot;248&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 256바이트 AES암호화를 이용해 입력을 암호화한다. 더 강력한 바이트 암호화 &lt;code&gt;Ecryptor&lt;/code&gt; 인스턴스는 &lt;code&gt;Encryptors.stronger()&lt;/code&gt; 메서드를 호출하면 된다. 차이는 AES 암호화의 작업 모드로 GCM(갈루아/카운터 모드)을 이용한다. 표준 모드는 이보다 약한 방식인 CBC(암호 블록 체인)을 이용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TextEncryptor&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 가지 주요 형식이 있고 &lt;code&gt;Encryptors.text()&lt;/code&gt;, &lt;code&gt;Encryptors.delux()&lt;/code&gt;, &lt;a href=&quot;https://github.com/spring-projects/spring-security/issues/8980&quot;&gt;&lt;code&gt;~~Encryptors.queryableText()~~&lt;/code&gt;&lt;/a&gt; 메서드를 호출해 이러한 형식을 생성할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;449&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2QXtN/btsH7DBFqNJ/dwAuCzWk2vntcbS9CI3hxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2QXtN/btsH7DBFqNJ/dwAuCzWk2vntcbS9CI3hxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2QXtN/btsH7DBFqNJ/dwAuCzWk2vntcbS9CI3hxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2QXtN%2FbtsH7DBFqNJ%2FdwAuCzWk2vntcbS9CI3hxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;784&quot; height=&quot;449&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;449&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값을 암호화하지 않는 더미 &lt;code&gt;TextEncryptor&lt;/code&gt;를 반환하는 메서드도 있다. &lt;code&gt;Encryptors.noOpText()&lt;/code&gt; 메서드를 통해 애플리케이션 성능만을 테스트하거나 데모 예제에서 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;String valueToEncrypt = &quot;HELLO&quot;;
TextEncryptor e = Encryptors.noOpText();
// valueToEncrypt == encrypted
String encrypted = e.encrypt(valueToEncrypt);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 볼 수 있듯이 &lt;code&gt;Encryptors.delux()&lt;/code&gt; 는 &lt;code&gt;Encryptors.stronger()&lt;/code&gt;를, &lt;code&gt;Encryptors.text()&lt;/code&gt;는 &lt;code&gt;Encryptors.standard()&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;delux&lt;/code&gt;와 &lt;code&gt;text&lt;/code&gt; 모두 같은 입력으로 &lt;code&gt;encrypt&lt;/code&gt; 메서드를 반복 호출해도 다른 출력이 반환된다. 암호화 프로세스에 임의의 초기화 벡터가 생성되기 때문이다.&lt;/p&gt;</description>
      <category>Java/Spring</category>
      <category>spring</category>
      <author>greatwhite</author>
      <guid isPermaLink="true">https://greatwhite.tistory.com/36</guid>
      <comments>https://greatwhite.tistory.com/36#entry36comment</comments>
      <pubDate>Fri, 21 Jun 2024 14:43:59 +0900</pubDate>
    </item>
    <item>
      <title>스프링 시큐리티 인 액션] 3장_사용자 관리</title>
      <link>https://greatwhite.tistory.com/35</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;770&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdHFU7/btsH6ynRB3p/J9DGS7q0ZW5l8L4gK4dgt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdHFU7/btsH6ynRB3p/J9DGS7q0ZW5l8L4gK4dgt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdHFU7/btsH6ynRB3p/J9DGS7q0ZW5l8L4gK4dgt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdHFU7%2FbtsH6ynRB3p%2FJ9DGS7q0ZW5l8L4gK4dgt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;559&quot; height=&quot;455&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;770&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 요청이 들어오면 필터가 요청을 가로채고 인증 책임이 &lt;code&gt;AuthenticationManager&lt;/code&gt;로 위임된다. 인증 논리 구현하는 &lt;code&gt;AuthenticationProvider&lt;/code&gt;를 이용하는데 이 때 &lt;code&gt;UserDetailsService&lt;/code&gt;에서 사용자를 찾고 &lt;code&gt;PasswordEncoder&lt;/code&gt; 로 암호를 검증한다. 모든 인증이 끝나면 &lt;code&gt;SecurityContext&lt;/code&gt;에 &lt;code&gt;UserDetails&lt;/code&gt;가 저장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 관리는 &lt;code&gt;UserDetailsManager&lt;/code&gt;와 &lt;code&gt;UserDetailsService&lt;/code&gt;를 사용한다. &lt;code&gt;UserDetailsService&lt;/code&gt; 는 사용자 이름으로 사용자를 찾는 역할만 하고 &lt;code&gt;UserDetailsManager&lt;/code&gt; 는 사용자의 삭제, 추가, 수정 작업을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 애플리케이션 내에서 수행할 수 있는 작업을 나타내는 이용 권리의 집합을 가진다. 이를 &lt;b&gt;권한&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용자 정의하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자를 나타내는 것은 인증 구현의 첫 번째 단계이다. 스프링 시큐리티에서 사용자 정의는 &lt;code&gt;UserDetails&lt;/code&gt; 계약을 준수해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;657&quot; data-origin-height=&quot;333&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcXNlj/btsH61b0dkV/EBLXrk9MbrnGyAdN3XzQUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcXNlj/btsH61b0dkV/EBLXrk9MbrnGyAdN3XzQUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcXNlj/btsH61b0dkV/EBLXrk9MbrnGyAdN3XzQUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcXNlj%2FbtsH61b0dkV%2FEBLXrk9MbrnGyAdN3XzQUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;568&quot; height=&quot;288&quot; data-origin-width=&quot;657&quot; data-origin-height=&quot;333&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;UserDetails&lt;/code&gt;의 메서드는 두 부분으로 나뉜다. 인증과정에서 사용되는 &lt;code&gt;PgtaPssword()&lt;/code&gt;, &lt;code&gt;getUsername()&lt;/code&gt; 과 리소스에 접근할 수 있도록 권한을 부여하는 나머지 다섯 메서드로 나눌 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;권한 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 수행할 수 있는 작업을 &lt;b&gt;권한&lt;/b&gt;이라고 한다. 애플리케이션 마다 사용자가 가진 권한이 다를 수 있다. 스프링 시큐리티에서는 &lt;code&gt;GrantedAuthority&lt;/code&gt; 인터페이스로 권한을 나타낸다. 사용자는 여러 권한을 가질 수 있고 일반적으로 하나 이상의 권한을 가진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;487&quot; data-origin-height=&quot;68&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ztkq6/btsH6RAELuA/AtMU45kcSDLEjwKmoyWZcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ztkq6/btsH6RAELuA/AtMU45kcSDLEjwKmoyWZcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ztkq6/btsH6RAELuA/AtMU45kcSDLEjwKmoyWZcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZtkq6%2FbtsH6RAELuA%2FAtMU45kcSDLEjwKmoyWZcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;487&quot; height=&quot;68&quot; data-origin-width=&quot;487&quot; data-origin-height=&quot;68&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;권한을 구현하기 위해서는 람다식을 사용하거나 &lt;code&gt;SimpleGrantedAuthority&lt;/code&gt; 클래스를 이용해 인스턴스를 만드는 것도 가능하다. 구현 시에는 이름을 반환하게만 구현하면 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용자 책임 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에서 사용자는 여러 책임을 가질 수 있다. 데이터베이스에 저장되는 유저 엔티티 책임과 인증에 사용되는 &lt;code&gt;UserDetails&lt;/code&gt; 책임을 가질 수 있다. 그러나 모든 책임을 한 클래스에 넣는 것은 바람직하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지 책임을 분리해서 User 클래스는 JPA 엔티티 책임만을 가지고 이를 래핑해 인증 관련 책임을 처리하는 클래스를 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
public class User {
        @Id
        private Long id;
        private String username;
        private String password;
        private String authority;

        // getters &amp;amp; setters
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;public class SecurityUser implements UserDetails {
        private final User user;

        public SecurityUser(User user) {
                this.user = user;
        }

        @Override
        public String getUsername() {
                return user.getUsername();
        }

        @Override
        public String getPassword() {
                return user.getPassword();
        }

        @Override
        public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
                 return List.of(() -&amp;gt; user.getAuthority());
        }

        ....
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스프링 시큐리티 사용자 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 앞서 만든 사용자를 &lt;code&gt;UserDetailsService&lt;/code&gt;를 구현해 관리하는 방법을 지정해줘야 한다. 여기에 더 많은 기능을 사용하려면 &lt;code&gt;UserDetailsManager&lt;/code&gt;를 구현하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;UserDetailsService&lt;/code&gt;는 &lt;code&gt;loadUserByUsername()&lt;/code&gt; 한 개의 메서드만을 가진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2264&quot; data-origin-height=&quot;924&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d43OP4/btsH8VA6WVh/pFAJnax3eY5NJ2ka0wyDB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d43OP4/btsH8VA6WVh/pFAJnax3eY5NJ2ka0wyDB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d43OP4/btsH8VA6WVh/pFAJnax3eY5NJ2ka0wyDB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd43OP4%2FbtsH8VA6WVh%2FpFAJnax3eY5NJ2ka0wyDB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2264&quot; height=&quot;924&quot; data-origin-width=&quot;2264&quot; data-origin-height=&quot;924&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 구현은 &lt;code&gt;loadUserByUsername()&lt;/code&gt; 메서드를 호출해 주어진 이름과 일치하는 &lt;code&gt;UserDetails&lt;/code&gt;를 얻는다. 이 때 주어진 이름이 없으면 &lt;code&gt;UsernameNotFoundException&lt;/code&gt;이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UserDetailsService 구현&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티에서 필요한 것은 어떤 방식으로든 &lt;b&gt;사용자 이름으로 사용자를 조회하는 기능&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class InMemoryUserDetailsService implements UserDetailsService {
    private final List&amp;lt;UserDetails&amp;gt; users;

    public InMemoryUserDetailsService(List&amp;lt;UserDetails&amp;gt; users) {
        this.users = users;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return users.stream()
                .filter(
                        u -&amp;gt; u.getUsername().equals(username)
                ).findFirst()
                .orElseThrow(() -&amp;gt; new UsernameNotFoundException(&quot;User not found&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 상으로 찾을 때는 위와 같이 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UserDetailsManager&lt;/b&gt; &lt;b&gt;구현&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티에서 제공하는 &lt;code&gt;serDetailsManager&lt;/code&gt; 인터페이스는 &lt;code&gt;UserDetailsService&lt;/code&gt; 인터페이스를 extends한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNhZIi/btsH8K0SgOZ/6fX5bSWK6UkAOAJCVxjBCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNhZIi/btsH8K0SgOZ/6fX5bSWK6UkAOAJCVxjBCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNhZIi/btsH8K0SgOZ/6fX5bSWK6UkAOAJCVxjBCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNhZIi%2FbtsH8K0SgOZ%2F6fX5bSWK6UkAOAJCVxjBCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;558&quot; height=&quot;245&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lfRws/btsH65Mc4Ny/ELjDAiuluYYrLFp1iErLsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lfRws/btsH65Mc4Ny/ELjDAiuluYYrLFp1iErLsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lfRws/btsH65Mc4Ny/ELjDAiuluYYrLFp1iErLsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlfRws%2FbtsH65Mc4Ny%2FELjDAiuluYYrLFp1iErLsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;664&quot; height=&quot;106&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현체로 &lt;code&gt;InMemoryUserDetailsManager&lt;/code&gt;와 &lt;code&gt;JdbcUserDetailsManager&lt;/code&gt; 를 제공한다. &lt;code&gt;JdbcUserDetailsManager&lt;/code&gt;는 SQL 데이터베이스에 저장된 사용자를 관리하며 JDBC를 통해 데이터베이스에 직접 연결한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;854&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPMeWf/btsH8jpbBnJ/9MofrrEp18Dpgo5UyCUWX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPMeWf/btsH8jpbBnJ/9MofrrEp18Dpgo5UyCUWX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPMeWf/btsH8jpbBnJ/9MofrrEp18Dpgo5UyCUWX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPMeWf%2FbtsH8jpbBnJ%2F9MofrrEp18Dpgo5UyCUWX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;599&quot; height=&quot;370&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;854&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 살펴봤던 그림과 조금 달라졌다. &lt;code&gt;UserDetails&lt;/code&gt; 얻는 과정에서 &lt;code&gt;JdbcUserDetailsManager&lt;/code&gt; 가 데이터베이스에서 사용자 이름으로 조회하는 부분이 추가되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 Jdbc를 설정해줘야 하므로 의존성을 추가해줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;dependencies {
    runtimeOnly 'com.h2database:h2'
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용할 데이터베이스에 따라 다른 드라이버를 설치해주면 되는데 h2가 더 간편하기 때문에 h2Driver를 추가해줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이제는 application.yml에 dataSource를 설정해줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spring:
    datasource:
        driver-class-name: org.h2.Driver
        url: '&amp;lt;사용할 url&amp;gt;'
        username: 'sa'
        password: 
    sql:
        init:
            mode: always&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;spring.datasource.initialization-mode&lt;/code&gt; 를 &lt;code&gt;always&lt;/code&gt;로 두려고 했는데 deprecated되었다고 한다. 대신 &lt;code&gt;spring.sql.init.mode&lt;/code&gt; 로 바뀌었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 프로젝트에 사용될 &lt;code&gt;UserDetailsService&lt;/code&gt;를 빈으로 등록해줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class ProjectConfig {
        @Bean
        public UserDetailsService userDetailsService(DataSource dataSource) {
                return new JdbcUserDetailsManager(dataSource);
        }

        @Bean
        public PasswordEncoder passwordEncoder() { 
                return NoOpsPasswordEncoder.getInstance();
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 &lt;code&gt;JdbcUserDetailsService&lt;/code&gt;에 이용되는 쿼리도 구성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Bean
public UserDetailsService userDetailsService() {
        String usersByUsernameQuery =
            &quot;select username, password, enabled from users where username = ?&quot;;
    String authByUserQuery =
            &quot;select username, authority from authority where username = ?&quot;;

    var userDetailsManager = new JdbcUserDetailsManager(dataSource);
    userDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery);
    userDetailsManager.setAuthoritiesByUsernameQuery(authByUserQuery);
    return userDetailsManager;
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Java/Spring</category>
      <category>spring</category>
      <author>greatwhite</author>
      <guid isPermaLink="true">https://greatwhite.tistory.com/35</guid>
      <comments>https://greatwhite.tistory.com/35#entry35comment</comments>
      <pubDate>Wed, 19 Jun 2024 16:39:49 +0900</pubDate>
    </item>
  </channel>
</rss>