<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>be_better</title>
    <link>https://jnjeaaaat.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Tue, 7 Jul 2026 23:30:54 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>NiceTry</managingEditor>
    <image>
      <title>be_better</title>
      <url>https://tistory1.daumcdn.net/tistory/6392987/attach/ec6ed784b16247d186b41ad271b743f1</url>
      <link>https://jnjeaaaat.tistory.com</link>
    </image>
    <item>
      <title>[Springboot] Test code &amp;quot;csrf&amp;quot; 403 Forbidden</title>
      <link>https://jnjeaaaat.tistory.com/82</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;에러내용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Springboot 2.x버전으로 프로젝트 진행 중 3.x버전으로 변경하게 되었고 SpringSecurity도 6.x 버전&lt;/b&gt;으로 Upgrade를 해야하는 상황이었다.&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;Springboot, Security 둘다 버전업을 하고 Postman에서 정상적으로 동작하는것까지 확인했는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘되던 &lt;b&gt;Test Code Controller test에서 말썽&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;Controller Test중 Body에는 아무값도 들어있지 않고 &lt;b&gt;403 Forbidden이 발생&lt;/b&gt;한 상황&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1349&quot; data-origin-height=&quot;256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J9gUB/btsDfpg1sTP/puCDc0QAVnCwIKmYK5ddcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J9gUB/btsDfpg1sTP/puCDc0QAVnCwIKmYK5ddcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J9gUB/btsDfpg1sTP/puCDc0QAVnCwIKmYK5ddcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ9gUB%2FbtsDfpg1sTP%2FpuCDc0QAVnCwIKmYK5ddcK%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;1349&quot; height=&quot;256&quot; data-origin-width=&quot;1349&quot; data-origin-height=&quot;256&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Security&lt;/b&gt;의 어노테이션인 &lt;b&gt;EnableWebSecurity에는 기본적으로 CSRF 공격을 방지하는 기능&lt;/b&gt;을 갖고있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;CSRF&lt;/b&gt;(&lt;b&gt;C&lt;/b&gt;ross-&lt;b&gt;S&lt;/b&gt;ite&amp;nbsp;&lt;b&gt;R&lt;/b&gt;equest&amp;nbsp;&lt;b&gt;F&lt;/b&gt;orgery) 공격이란&lt;br /&gt;&lt;br /&gt;&lt;b&gt;사이트 간 요청 위조의 줄임말로&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;웹 애플리케이션 취약점 중 하나로 사용자가 자신의 의지와 무관하게 공격자가 의도한 행동을 해서&amp;nbsp;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;특정 웹페이지를 보안에 취약하게 한다거나 수정, 삭제 등의 작업을 하게 만드는 공격 방법&lt;/b&gt;&lt;/span&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;이것을 방지하기위해 대표적으로 CSRF Token 방식이 활용되는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리의 &lt;b&gt;Test code에는 CSRF Token이 존재하지 않아서 접근제한&lt;/b&gt;을 둔것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 간단하게 mockMvc.perform uri 요청에 .with(csrf()) 코드를 추가하면 해결할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;mockMvc.perform(post(&quot;/api/v1/sign-in&quot;)
        .contentType(MediaType.APPLICATION_JSON)
        .content(objectMapper.writeValueAsString(
            new SignInRequest(&quot;testUid&quot;, &quot;password&quot;)
        ))
        .with(csrf()))  // 추가한 부분
    .andExpect(jsonPath(&quot;$.result.uid&quot;).value(&quot;testUid&quot;))
    .andExpect(jsonPath(&quot;$.result.roles[0]&quot;).value(&quot;ROLE_VIEWER&quot;))
    .andExpect(jsonPath(&quot;$.result.accessToken&quot;).value(&quot;testAccessToken&quot;))
    .andExpect(jsonPath(&quot;$.result.refreshToken&quot;).value(&quot;testRefreshToken&quot;))
    .andExpect(status().isOk())
    .andDo(print());&lt;/code&gt;&lt;/pre&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;1363&quot; data-origin-height=&quot;207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kDBE6/btsDhU8pWWI/wlk3jfEPNZLCSPzA1W7ssk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kDBE6/btsDhU8pWWI/wlk3jfEPNZLCSPzA1W7ssk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kDBE6/btsDhU8pWWI/wlk3jfEPNZLCSPzA1W7ssk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkDBE6%2FbtsDhU8pWWI%2Fwlk3jfEPNZLCSPzA1W7ssk%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;1363&quot; height=&quot;207&quot; data-origin-width=&quot;1363&quot; data-origin-height=&quot;207&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;1313&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6yiSO/btsDfP05Mys/RPusIDjvxIuaHDqtKH3RMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6yiSO/btsDfP05Mys/RPusIDjvxIuaHDqtKH3RMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6yiSO/btsDfP05Mys/RPusIDjvxIuaHDqtKH3RMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6yiSO%2FbtsDfP05Mys%2FRPusIDjvxIuaHDqtKH3RMK%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;1313&quot; height=&quot;266&quot; data-origin-width=&quot;1313&quot; data-origin-height=&quot;266&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 비어있던 Parameters에 &lt;b&gt;{_csrf=[]}&lt;/b&gt; 값이 생긴것을 확인할 수 있다.&lt;/p&gt;</description>
      <category>Springboot/Error 해결</category>
      <category>csrf</category>
      <category>Java</category>
      <category>mock</category>
      <category>spring</category>
      <category>SpringBoot</category>
      <category>SpringSecurity</category>
      <category>TestCode</category>
      <category>개발자</category>
      <category>백엔드</category>
      <category>서버</category>
      <author>NiceTry</author>
      <guid isPermaLink="true">https://jnjeaaaat.tistory.com/82</guid>
      <comments>https://jnjeaaaat.tistory.com/82#entry82comment</comments>
      <pubDate>Tue, 9 Jan 2024 22:22:53 +0900</pubDate>
    </item>
    <item>
      <title>[Springboot] ImmutableCollections 에러 해결</title>
      <link>https://jnjeaaaat.tistory.com/81</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;에러 내용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User 권한 추가 테스트 코드 작성 중 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;ImmutableCollections 에러가 발생&lt;/b&gt;&lt;/span&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;나의 경우 User 권한 roles를 Set 타입으로 지정하고 관리하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mock으로 가상의 유저를 만들어주고 권한도 지정해준다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Set&amp;lt;String&amp;gt; roleSet = Set.of(&quot;ROLE_VIEWER&quot;);
given(userRepository.findByUidAndDeletedAtNull(anyString()))
    .willReturn(Optional.of(User.builder()
            .uid(&quot;testId&quot;)
            .roles(roleSet)
            .build()
        )
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 roleSet을 선언해줄때 new HashSet&amp;lt;&amp;gt;() 이 아닌 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Set.of() 로 지정해줬기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Set.of()를 사용하면 불변 컬렉션으로 만들라는 의미인데, 말그대로 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;불변 컬렉션은 삽입, 삭제, 수정 을 할 수 없게된다.&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ImmutableCollections의 내부를 살펴보면&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;static UnsupportedOperationException uoe() { return new UnsupportedOperationException(); }

static abstract class AbstractImmutableCollection&amp;lt;E&amp;gt; extends AbstractCollection&amp;lt;E&amp;gt; {
    // all mutating methods throw UnsupportedOperationException
    @Override public boolean add(E e) { throw uoe(); }
    @Override public boolean addAll(Collection&amp;lt;? extends E&amp;gt; c) { throw uoe(); }
    @Override public void    clear() { throw uoe(); }
    @Override public boolean remove(Object o) { throw uoe(); }
    @Override public boolean removeAll(Collection&amp;lt;?&amp;gt; c) { throw uoe(); }
    @Override public boolean removeIf(Predicate&amp;lt;? super E&amp;gt; filter) { throw uoe(); }
    @Override public boolean retainAll(Collection&amp;lt;?&amp;gt; c) { throw uoe(); }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;add(), clear(), remove() 등 호출하면 UnsupportedOperationException() 예외를 내보내는것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방법은 간단하게 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;Set.of() 를 new HashSet&amp;lt;&amp;gt;() 으로 코드를 수정&lt;/span&gt;&lt;/b&gt;해주면 된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Set&amp;lt;String&amp;gt; roleSet = new HashSet&amp;lt;&amp;gt;(List.of(&quot;ROLE_VIEWER&quot;));
given(userRepository.findByUidAndDeletedAtNull(anyString()))
    .willReturn(Optional.of(User.builder()
            .uid(&quot;testId&quot;)
            .roles(roleSet)
            .build()
        )
    );&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Springboot/Error 해결</category>
      <category>error</category>
      <category>Java</category>
      <category>spring</category>
      <category>SpringBoot</category>
      <category>개발자</category>
      <category>백엔드</category>
      <category>불변컬렉션</category>
      <author>NiceTry</author>
      <guid isPermaLink="true">https://jnjeaaaat.tistory.com/81</guid>
      <comments>https://jnjeaaaat.tistory.com/81#entry81comment</comments>
      <pubDate>Wed, 3 Jan 2024 17:36:32 +0900</pubDate>
    </item>
    <item>
      <title>[Springboot] test code mockmvc perform header에 jwt 권한 인증처리</title>
      <link>https://jnjeaaaat.tistory.com/80</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 진행중에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저 정보를 update 하는 api를 unit 테스트 하려는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;header 값으로 넣어줄 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;jwt accessToken에 대한 유저권한처리&lt;/span&gt;&lt;/b&gt;가 안돼서 발목을 잡혔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SecurityConfig에서 SecurityContext에 유저 권한을 넣어주고 Filter에서 처리하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test code를 실행할때도 main module의 SecurityContext를 참조하기 때문에 유저 역할(권한) 처리가 안되는 것이라고 이해했다.&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;따라서 test module에서 &lt;b&gt;따로 SecurityContext에 필요한 권한을 넣어줘야 한다.&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;나같은 경우에는 유저 정보 update api는 &lt;b&gt;&quot;USER_VIEWER&quot;&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;매번 권한을 설정하는것보다 어노테이션을 만드는게 나아서 customAnnotation을 만들어 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;어노테이션 정의&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;/**
 * token 권한이 필요한 test method 에 적용하는 Annotation
 */
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithCustomMockUserSecurityContextFactory.class)
public @interface WithCustomMockUser {
  String uid() default &quot;testId&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;어노테이션 구현&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;/**
 * security config 와의 의존성을 깨고 test 할때만 사용하게되는 SecurityContext 재설정 class
 */
public class WithCustomMockUserSecurityContextFactory implements
    WithSecurityContextFactory&amp;lt;WithCustomMockUser&amp;gt; {

  @Override
  public SecurityContext createSecurityContext(WithCustomMockUser annotation) {
    String uid = annotation.uid();

    Authentication auth = new UsernamePasswordAuthenticationToken(uid, &quot;&quot;,
        List.of(new SimpleGrantedAuthority(&quot;ROLE_VIEWER&quot;)));

    SecurityContext context = SecurityContextHolder.getContext();
    context.setAuthentication(auth);

    return context;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;test코드에 적용&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
@WithCustomMockUser
@DisplayName(&quot;유저 정보 변경 성공&quot;)
void success_modify_user() throws Exception {

  ...(생략)
  
  mockMvc.perform(multipart(HttpMethod.PUT, &quot;/api/v1/user/1&quot;)
          .file(image)
          .file(new MockMultipartFile(&quot;request&quot;, &quot;&quot;, &quot;application/json&quot;,
              valueAsString.getBytes(StandardCharsets.UTF_8)))
          .contentType(MediaType.MULTIPART_FORM_DATA)
          .accept(MediaType.APPLICATION_JSON)
          .characterEncoding(&quot;UTF-8&quot;)
          .with(csrf())
          .header(&quot;X-AUTH-TOKEN&quot;, &quot;testAccessToken&quot;))

      .andExpect(status().isOk())
      .andDo(print());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;70&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xqo3h/btsCMxmTF5h/GXtCIUNNNEsdNjL4golmtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xqo3h/btsCMxmTF5h/GXtCIUNNNEsdNjL4golmtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xqo3h/btsCMxmTF5h/GXtCIUNNNEsdNjL4golmtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxqo3h%2FbtsCMxmTF5h%2FGXtCIUNNNEsdNjL4golmtk%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;414&quot; height=&quot;70&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;70&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MultipartFile 인터페이스로 image를 받아야 하기 때문에 multipart(&quot;uri&quot;) 메소드 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;HttpMethod.PUT 설정&lt;/span&gt;&lt;/b&gt;을 해주지 않으면 자동으로 POST Method가 적용된다.&lt;/li&gt;
&lt;li&gt;필요한 header의 키값은 &quot;X-AUTH-TOKEN&quot;이고 value값으로 아무 토큰값을 입력해준다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;@WithCustomMockUser&lt;/span&gt;&lt;/b&gt; 어노테이션에서 SecurityContext auth에 USER_VIEWER 권한을 넣어줬기 때문에 이상없이 실행 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;참조링크&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@da_na/Spring-Security-JWT-Spring-Security-Controller-Unit-Test%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;테스트코드 Header jwt 권한 설정&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1703840171829&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring Security + JWT] Spring Security Controller Unit Test하기&quot; data-og-description=&quot;Unit Test로 더 작은 단위로 쪼개기 위해서 Controller, Service, Repository 계층별로 테스트를 진행해야 했습니다.하지만, Controller는 Spring Security와 JWT와 깊은 의존성을 가지고 있어 별도의 처리를 해야했&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@da_na/Spring-Security-JWT-Spring-Security-Controller-Unit-Test%ED%95%98%EA%B8%B0&quot; data-og-url=&quot;https://velog.io/@da_na/Spring-Security-JWT-Spring-Security-Controller-Unit-Test하기&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cp7EKq/hyUXKT6vb8/vcmi3LS8PVBXWD13HKj5g1/img.png?width=564&amp;amp;height=553&amp;amp;face=0_0_564_553,https://scrap.kakaocdn.net/dn/DVwlp/hyUTAyPXVd/tHh6b78BKX5kaKsMoMwnqK/img.png?width=564&amp;amp;height=553&amp;amp;face=0_0_564_553,https://scrap.kakaocdn.net/dn/bQh0TY/hyUXSR8NH2/eJuS0OMCKMNScMEvJlMaq1/img.jpg?width=564&amp;amp;height=564&amp;amp;face=0_0_564_564&quot;&gt;&lt;a href=&quot;https://velog.io/@da_na/Spring-Security-JWT-Spring-Security-Controller-Unit-Test%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@da_na/Spring-Security-JWT-Spring-Security-Controller-Unit-Test%ED%95%98%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cp7EKq/hyUXKT6vb8/vcmi3LS8PVBXWD13HKj5g1/img.png?width=564&amp;amp;height=553&amp;amp;face=0_0_564_553,https://scrap.kakaocdn.net/dn/DVwlp/hyUTAyPXVd/tHh6b78BKX5kaKsMoMwnqK/img.png?width=564&amp;amp;height=553&amp;amp;face=0_0_564_553,https://scrap.kakaocdn.net/dn/bQh0TY/hyUXSR8NH2/eJuS0OMCKMNScMEvJlMaq1/img.jpg?width=564&amp;amp;height=564&amp;amp;face=0_0_564_564');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring Security + JWT] Spring Security Controller Unit Test하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Unit Test로 더 작은 단위로 쪼개기 위해서 Controller, Service, Repository 계층별로 테스트를 진행해야 했습니다.하지만, Controller는 Spring Security와 JWT와 깊은 의존성을 가지고 있어 별도의 처리를 해야했&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Springboot/코딩</category>
      <category>Java</category>
      <category>JUnit</category>
      <category>MockMvc</category>
      <category>spring</category>
      <category>SpringBoot</category>
      <category>TestCode</category>
      <category>백엔드</category>
      <author>NiceTry</author>
      <guid isPermaLink="true">https://jnjeaaaat.tistory.com/80</guid>
      <comments>https://jnjeaaaat.tistory.com/80#entry80comment</comments>
      <pubDate>Fri, 29 Dec 2023 17:58:04 +0900</pubDate>
    </item>
    <item>
      <title>[Springboot] Type definition error: [simple type, class java.time.LocalDateTime]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module &amp;quot;co..</title>
      <link>https://jnjeaaaat.tistory.com/79</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;에러 내용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 진행 중&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Response Dto에 createdAt, updatedAt 을 리턴하는 코드를 구현했는데 결과값을 확인하는 과정에서 이와같은 에러가 발생했다.&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;그전까진 잘 작동하다가 ObjectMapper를 Bean으로 등록하고나서 생긴 오류이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;87&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oNfMH/btsCDhkjZyT/KuauRrJRyMApiJR1TrQYgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oNfMH/btsCDhkjZyT/KuauRrJRyMApiJR1TrQYgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oNfMH/btsCDhkjZyT/KuauRrJRyMApiJR1TrQYgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoNfMH%2FbtsCDhkjZyT%2FKuauRrJRyMApiJR1TrQYgK%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;1782&quot; height=&quot;87&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;87&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LocalDateTime type 직렬화, 역직렬화에 문제가 있어서 생기는 에러이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 주고받으면서 내부의 MessageConverter의 ObjectMapper가 데이터 형식을 바꾸는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ObjectMapper를 빈으로 등록해 사용하게되니 적용이 잘 안된게 원인이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;첫번째 해결책&lt;/b&gt;&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;span style=&quot;color: #ee2323;&quot;&gt;add&amp;nbsp;Module&amp;nbsp;&quot;com.fasterxml.jackson.datatype:jackson-datatype-jsr310&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;라는 해결책이 나와있는데 이미 자체적으로 적용되어있는 모듈이다. 따라서 gradle에 따로 build할 필요는 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;두번째 해결책&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;LocalDateTime type을 쓰는 Entity column에 어노테이션 추가&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

  // 생성일자
  @CreatedDate
  @JsonSerialize(using = LocalDateTimeSerializer.class)
  @JsonDeserialize(using = LocalDateTimeDeserializer.class)
  private LocalDateTime createdAt;

  // 정보 변경 일자
  @LastModifiedDate
  @JsonSerialize(using = LocalDateTimeSerializer.class)
  @JsonDeserialize(using = LocalDateTimeDeserializer.class)
  private LocalDateTime updatedAt;

}&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;세번째 해결책&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ObjectMapper Bean 코드 수정&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class ObjectMapperConfig {

  @Bean
  public ObjectMapper objectMapper() {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(new JavaTimeModule());
    return objectMapper;
  }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 수정하고 테스트를 진행해보면 Date Format이 엉망으로 되어있는 것을 볼수있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;326&quot; data-origin-height=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkE2UE/btsCEJ8T81e/mAXk8bjiws03WOrxbI5Tf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkE2UE/btsCEJ8T81e/mAXk8bjiws03WOrxbI5Tf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkE2UE/btsCEJ8T81e/mAXk8bjiws03WOrxbI5Tf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkE2UE%2FbtsCEJ8T81e%2FmAXk8bjiws03WOrxbI5Tf1%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;506&quot; data-origin-width=&quot;326&quot; data-origin-height=&quot;660&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 Year, Month, Day, ... 항목들을 디테일하게 나타낸 것인데 간략하게 보이게 하기 위해서 다음 코드를 삽입한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class ObjectMapperConfig {

  @Bean
  public ObjectMapper objectMapper() {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(new JavaTimeModule());
    // 추가된 코드
    objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    return objectMapper;
  }

}&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;78&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A7jOD/btsCLpuNPN7/GDqjJQbUmt0DJV7uivNhGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A7jOD/btsCLpuNPN7/GDqjJQbUmt0DJV7uivNhGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A7jOD/btsCLpuNPN7/GDqjJQbUmt0DJV7uivNhGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA7jOD%2FbtsCLpuNPN7%2FGDqjJQbUmt0DJV7uivNhGk%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;350&quot; height=&quot;44&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;78&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Springboot/Error 해결</category>
      <category>error</category>
      <category>Java</category>
      <category>ObjectMapper</category>
      <category>spring</category>
      <category>SpringBoot</category>
      <category>백엔드</category>
      <category>백엔드개발자</category>
      <category>역직렬화</category>
      <category>직렬화</category>
      <author>NiceTry</author>
      <guid isPermaLink="true">https://jnjeaaaat.tistory.com/79</guid>
      <comments>https://jnjeaaaat.tistory.com/79#entry79comment</comments>
      <pubDate>Wed, 27 Dec 2023 23:21:27 +0900</pubDate>
    </item>
    <item>
      <title>[Springboot] MultipartFile 로 이미지 업로드</title>
      <link>https://jnjeaaaat.tistory.com/77</link>
      <description>&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;유저의 프로필 사진을 받아 AWS s3에 저장하는 기능을 구현하기 전에&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;업로드 테스트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;편의를 위해 Controller 만 구현하였다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;TestController&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@RestController
@RequestMapping(&quot;/test&quot;)
public class TestController {

  @GetMapping(&quot;&quot;)
  public String uploadImgFile(@RequestParam(value = &quot;image&quot;) MultipartFile image) {

    log.info(&quot;[uploadImgFileTest] file name : {}&quot;, image.getOriginalFilename());

    try {
      image.transferTo(
          new File(&quot;/Users/parktj/Documents/IntelliJ/personal/Onbition/doc/img/&quot;
              + image.getOriginalFilename())
      );
      
    } catch (IOException e) {
      e.printStackTrace();
    }

    return image.getOriginalFilename();
  }
  
}&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Postman test&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;postman test&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1007&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vck5T/btsBY3ZVaQD/hTGfOBWozLzNPtJBaq9kl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vck5T/btsBY3ZVaQD/hTGfOBWozLzNPtJBaq9kl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vck5T/btsBY3ZVaQD/hTGfOBWozLzNPtJBaq9kl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvck5T%2FbtsBY3ZVaQD%2FhTGfOBWozLzNPtJBaq9kl1%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;1007&quot; height=&quot;416&quot; data-origin-width=&quot;1007&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Postman 에서 테스트를 하려면 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Body에 form-data&lt;/span&gt;&lt;/b&gt; 에서 &lt;b&gt;Key값을 file 형식으로 지정&lt;/b&gt;하고 &lt;b&gt;반드시 key name을 써줘야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;postman 에러&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;file working directory 에러&lt;/b&gt;&lt;/span&gt;가 뜰 경우에는 다음과 같이 해결한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;173&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmZhuY/btsBYsMqAnc/dTcnn3373ZEd8palawKhdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmZhuY/btsBYsMqAnc/dTcnn3373ZEd8palawKhdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmZhuY/btsBYsMqAnc/dTcnn3373ZEd8palawKhdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmZhuY%2FbtsBYsMqAnc%2FdTcnn3373ZEd8palawKhdk%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;404&quot; height=&quot;173&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;173&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;1552&quot; data-origin-height=&quot;987&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/538O5/btsBQpw5TVl/N7PodfbkKQsMD5BkIQjljk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/538O5/btsBQpw5TVl/N7PodfbkKQsMD5BkIQjljk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/538O5/btsBQpw5TVl/N7PodfbkKQsMD5BkIQjljk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F538O5%2FbtsBQpw5TVl%2FN7PodfbkKQsMD5BkIQjljk%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;1552&quot; height=&quot;987&quot; data-origin-width=&quot;1552&quot; data-origin-height=&quot;987&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Setting - General - Working directory에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일을 찾을 directory를 선택하고 Read files outside working directory를 체크한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;정상적으로 동작&lt;/b&gt;&lt;/h3&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;380&quot; data-origin-height=&quot;183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9Cddw/btsBXPBasCS/O6qp0mIQORsjDSGaADLOGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9Cddw/btsBXPBasCS/O6qp0mIQORsjDSGaADLOGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9Cddw/btsBXPBasCS/O6qp0mIQORsjDSGaADLOGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9Cddw%2FbtsBXPBasCS%2FO6qp0mIQORsjDSGaADLOGK%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;380&quot; height=&quot;183&quot; data-origin-width=&quot;380&quot; data-origin-height=&quot;183&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;시행착오&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;file pathname&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;157&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEDfbb/btsBQjDF9ti/NcbZOmlkHbbjdhgcfPYH90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEDfbb/btsBQjDF9ti/NcbZOmlkHbbjdhgcfPYH90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEDfbb/btsBQjDF9ti/NcbZOmlkHbbjdhgcfPYH90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEDfbb%2FbtsBQjDF9ti%2FNcbZOmlkHbbjdhgcfPYH90%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;1212&quot; height=&quot;157&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;157&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;file pathname을 주의하자!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;file pathname을 작업중인 디렉토리가 아닌 ~(루트) 디렉토리부터 써줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;RequestParam vs RequestPart&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@GetMapping(&quot;&quot;)
public String uploadImgFile(@RequestPart MultipartFile image) {

  log.info(&quot;[uploadImgFileTest] file name : {}&quot;, image.getOriginalFilename());

  try {
    image.transferTo(
        new File(&quot;/Users/parktj/Documents/IntelliJ/personal/Onbition/doc/img/&quot;
            + image.getOriginalFilename())
    );

  } catch (IOException e) {
    e.printStackTrace();
  }

  return image.getOriginalFilename();
}&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;RequestParam과 RequestPart는 혼용해서 많이 사용한다는데,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;RequestParam&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;RequestParam&lt;/b&gt;&lt;/span&gt;은 &lt;b&gt;다른 RequestParam들과 같이 사용할 때 사용&lt;/b&gt;하고&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;example&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@GetMapping(&quot;&quot;)
public String uploadImgFile(
    @RequestParam(&quot;image&quot;) MultipartFile image,
    @RequestParam(&quot;name&quot;) String name) {
    
    ... (생략)
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;RequestPart&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;RequestPart&lt;/b&gt;&lt;/span&gt;는 &lt;b&gt;RequestBody와 같이 JSON 형식으로 값을 받는것과 같이 사용할 때 사용&lt;/b&gt;하면 된다고 이해한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;example&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@GetMapping(&quot;&quot;)
public String uploadImgFile(
    @RequestPart(&quot;image&quot;) MultipartFile image,
    @RequestPart RequestDto request) {
    
    ... (생략)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 RequestPart에 &lt;b&gt;MultipartFile 데이터가 포함되어있지 않다면 &lt;span style=&quot;color: #006dd7;&quot;&gt;RequestBody와 동일한 동작&lt;/span&gt;&lt;/b&gt;을 한다!&lt;/p&gt;</description>
      <category>Springboot/코딩</category>
      <category>IMAGE</category>
      <category>requestBody</category>
      <category>requestParam</category>
      <category>RequestPart</category>
      <category>spring</category>
      <category>SpringBoot</category>
      <category>업로드</category>
      <author>NiceTry</author>
      <guid isPermaLink="true">https://jnjeaaaat.tistory.com/77</guid>
      <comments>https://jnjeaaaat.tistory.com/77#entry77comment</comments>
      <pubDate>Thu, 14 Dec 2023 12:10:55 +0900</pubDate>
    </item>
    <item>
      <title>[Springboot] 서비스와 인증과 권한 부여 - 스프링 시큐리티와 JWT 적용</title>
      <link>https://jnjeaaaat.tistory.com/76</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;스프링 시큐리티와 JWT 적용&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SpringSecurity 의존성&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;프로젝트 생성후 의존성을 추가&lt;/blockquote&gt;
&lt;pre id=&quot;code_1701936729634&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// gradle
dependencies {
  // spring security
  implementation 'org.springframework.boot:spring-boot-starter-security'
  // jwt 토큰
  implementation 'io.jsonwebtoken:jjwt:0.9.1'
}

// maven
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;spring-boot-starter-security&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;

&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;io.jsonwebtoken&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;jjwt&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;0.9.1&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;UserDetails와 UserDetailsService 구현&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;data/entity/User.java&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(nullable = false, unique = true)
    private String uid;

    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String name;

    @ElementCollection(fetch = FetchType.EAGER)
    @Builder.Default // 필드값이 초기값을 갖고 있을 때 사용
    private List&amp;lt;String&amp;gt; roles = new ArrayList&amp;lt;&amp;gt;();

    /*
     계정이 가지고 있는 권한 목록 리턴
     */
    @Override
    public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
        return roles
                .stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

    /*
    계정의 이름을 리턴
    일반적으로 아이디(이메일)를 리턴
     */
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Override
    public String getUsername() {
        return this.uid;
    }

    /*
    계정이 만료되었는지를 리턴
    true는 만료되지 않았음을 의미
     */
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /*
    계정이 잠겨있는지 리턴
    true는 잠겨있지 않음을 의미
     */
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /*
    비밀번호가 만료되었는지 리턴
     */
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /*
    계정이 활성화되어 있는지 리턴
     */
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Override
    public boolean isEnabled() {
        return true;
    }
}&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;UserDetails는&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserDetailsService 를 통해 입력된 로그인 정보를 가지고 데이터베이스에서 사용자 정보를 가져오는 역할&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getAuthorities(): 계정이 가지고 있는 권한 목록 리턴&lt;/li&gt;
&lt;li&gt;getPassword(): 계정의 비밀번호 리턴&lt;/li&gt;
&lt;li&gt;getUsername(): 계정의 이름 리턴, 일반적으로 아이디 리턴&lt;/li&gt;
&lt;li&gt;isAccountNonExpired(): 계정이 만료되었는지 리턴. true -&amp;gt; 만료 x&lt;/li&gt;
&lt;li&gt;isAccountNonLocked(): 계정이 잠겨있는지 리턴. true -&amp;gt; unLock&lt;/li&gt;
&lt;li&gt;isCredentialNonExpired(): 비밀번호가 만료되었는지 리턴. true -&amp;gt; 만료 x&lt;/li&gt;
&lt;li&gt;isEnabled(): 계정이 활성화 되어 있는지 리턴. true -&amp;gt; 활성화 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;UserRepository 구현 (data/repository/UserRepository.java)&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Repository
public interface UserRepository extends JpaRepository&amp;lt;User, Long&amp;gt; {
    
    User getByUid(String uid);
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;UserDetailsServiceImpl 구현 (service/impl/UserDetailsService.java)&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    /*
    Username(uid) 로 User 조회
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info(&quot;[loadUserByUsername] loadUserByUsername 수행, username : {}&quot;, username);
        return userRepository.getByUid(username);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;UserDetailsService&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface UserDetailsService {

   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

}&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;UserDetails &lt;/b&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;/li&gt;
&lt;li&gt;UserDetails의 &lt;b&gt;username은 각 사용자를 구분하는 ID&lt;/b&gt;(email, id, 등..)&lt;/li&gt;
&lt;li&gt;UserDetails의 구현체로 User 엔티티를 생성했기 때문에&lt;br /&gt;UserDetailsServiceImpl에서는User 객체를 리턴하게끔 구현한 것&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JwtTokenProvider 구현&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 UserDetails에서 JWT 토큰 생성에 필요한 정보를 얻을 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;JWT 토큰을 생성하는 JwtTokenProvider 생성 (config/security/JwtTokenProvider.java)&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

    private final UserDetailsService userDetailsService;

    @Value(&quot;${springboot.jwt.secret}&quot;)
    private String secretKey = &quot;secretKey&quot;;
    private final long tokenValidMillisecond = 1000L * 60 * 60;

    @PostConstruct
    protected void init() {
        log.info(&quot;[init] JwtTokenProvider 내 secretKey 초기화 시작&quot;);
        secretKey = Base64
                .getEncoder()
                .encodeToString(secretKey.getBytes(StandardCharsets.UTF_8));

        log.info(&quot;[init] JwtTokenProvider 내 secretKey 초기화 완료&quot;);
    }

    public String createToken(String userUid, List&amp;lt;String&amp;gt; roles) {
        log.info(&quot;[createToken] 토큰 생성 시작&quot;);
        Claims claims = Jwts.claims()
                .setSubject(userUid);
        claims.put(&quot;roles&quot;, roles);  // 일종의 map

        Date now = new Date(System.currentTimeMillis());

        String token = Jwts.builder()       // 토큰 생성
                .setClaims(claims)
                //  시작 시간 : 현재 시간기준으로 만들어짐
                .setIssuedAt(now)
                // 끝나는 시간 : 지금 시간 + 유지할 시간(입력받아옴)
                .setExpiration(new Date(now.getTime() + tokenValidMillisecond))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();

        log.info(&quot;[createToken] 토큰 생성 완료&quot;);
        return token;
    }

    /*
    토큰 인증 정보 조회
     */
    public Authentication getAuthentication(String token) {
        log.info(&quot;[getAuthentication] 토큰 인증 정보 조회 시작&quot;);
        UserDetails userDetails =
                userDetailsService.loadUserByUsername(this.getUsername(token));
        log.info(&quot;[getAuthentication] 토큰 인증 정보 조회 완료, UserDetails Username : {}&quot;, userDetails.getUsername());
        return new UsernamePasswordAuthenticationToken(
                userDetails, &quot;&quot;, userDetails.getAuthorities()
        );
    }

    /*
    token 으로 uid 추출
     */
    private String getUsername(String token) {
        log.info(&quot;[getUsername] 토큰 기반 회원 email 추출&quot;);
        String info = Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(token) // Jws&amp;lt;Claims&amp;gt;
                .getBody() // Claims
                .getSubject(); // uid 값
        log.info(&quot;[getUsername] 토큰 기반 회원 구별 정보 추출 완료, info : {}&quot;, info);
        return info;
    }

    /*
    헤더로부터 token 추출
     */
    public String resolveToken(HttpServletRequest request) {
        log.info(&quot;[resolveToken] HTTP 헤더에서 Token 값 추출&quot;);
        return request.getHeader(&quot;X-AUTH-TOKEN&quot;);
    }

    /*
    토큰 유효성 체크
     */
    public boolean validateToken(String token) {
        log.info(&quot;[validateToken] 토큰 유효 체크 시작&quot;);
        try {
            Jws&amp;lt;Claims&amp;gt; claims =
                    Jwts.parser()
                            .setSigningKey(secretKey)
                            .parseClaimsJws(token);

            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            log.info(&quot;[validateToken] 토큰 유효 체크 예외 발생&quot;);
            return false;
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;application.yml 파일에 @Value 값 정의&lt;/blockquote&gt;
&lt;pre class=&quot;dts&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;&lt;code&gt;springboot:
  jwt:
    secret: &quot;asdfasdfqwer&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;yml 파일에서 값을 가져올 수 없다면&lt;/span&gt; 직접 지정해준 'secretKey' 값을 사용&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;init()&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@PostConstruct - 해당 객체가 빈 객체로 주입된 이후 수행되는 메서드를 지칭&lt;/li&gt;
&lt;li&gt;secretKey를 Base64 형식으로 인코딩&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;createToken()&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JWT 토큰의 &lt;b&gt;payload 에 값을 넣기 위해 Claims 사용&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;User의 uid 값을 &lt;b&gt;setSubject() 를 통해 sub 속성&lt;/b&gt;에 추가.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;claims.put(&quot;key&quot;, &quot;value&quot;) 로 role 값을 별개로 추가&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;Jwts.builder()를 사용해 토큰 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;getAuthentication()&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필터에서 인증 성공 -&amp;gt; SecurityContextHolder에 저장할 Authentication 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UsernamePasswordAuthenticationToken 사용해 Authentication 구현&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;UsernamePasswordAuthenticationToken 구조&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzkBCi/btsByRmb5oC/CvdH6khjkoHkadlMmKzwr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzkBCi/btsByRmb5oC/CvdH6khjkoHkadlMmKzwr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzkBCi/btsByRmb5oC/CvdH6khjkoHkadlMmKzwr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzkBCi%2FbtsByRmb5oC%2FCvdH6khjkoHkadlMmKzwr0%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;661&quot; height=&quot;360&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authentication, CredentialContainer 인터페이스를 구현하는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AbstractAuthenticationToken를 상속하는 구조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;getUsername()&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jwts.parser()를 통해 secretKey 값을 확인 후 sub 값 추출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;resolveToken()&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;헤더 값으로 전달된 'X-AUTH-TOKEN' 값을 가져와 리턴&lt;/li&gt;
&lt;li&gt;클라이언트가 헤더를 통해 JWT 토큰 값을 전달해야 추출 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;헤더의 이름은 임의로 변경 가능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;validateToken()&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클레임의 유효기간 체크 후 boolean 타입의 값 리턴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JwtAuthenticationFilter 구현&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;JwtAuthenticationFilter&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- JWT 토큰을 인증하고 SecurityContextHolder에 추가하는 필터 설정 클래스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;JwtAuthenticationFilter (config/security/JwtAuthenticationFilter.java)&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;

    public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    protected void doFilterInternal(
            HttpServletRequest request, HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        
        String token = jwtTokenProvider.resolveToken(request);
        log.info(&quot;[doFilterInternal] token 값 추출 완료. token : {}&quot;, token);

        log.info(&quot;[doFilterInternal] token 값 유효성 체크 시작&quot;);
        if (token != null &amp;amp;&amp;amp; jwtTokenProvider.validateToken(token)) {
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            log.info(&quot;[doFilterInternal] token 값 유효성 체크 완료&quot;);
        }

        filterChain.doFilter(request, response);
        
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;doFilterInternal()&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마지막 줄 doFilter()
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;서블릿 실행 메서드.&lt;/li&gt;
&lt;li&gt;doFilter() 앞에 작성한 코드는 서블릿이 실행되기 전에 실행&lt;/li&gt;
&lt;li&gt;뒤에 작성된 코드는 서블릿이 실행된 후 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;토큰이 유효하다면 Authentication 객체를 생성해 SecurityContextHolder에 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SeuciryConfiguration 구현&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티 관련 설정.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSecurityConfigureAdapter를 상속받는 Configuration 클래스 구현 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;(대표적인 방법)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;SecurityConfiguration 클래스 (config/security/SecurityConfiguration.java)&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final JwtTokenProvider jwtTokenProvider;

    @Autowired
    public SecurityConfiguration(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        log.info(&quot;[configure] Security configure start&quot;);
        httpSecurity.httpBasic().disable()

                .csrf().disable()

                .sessionManagement()
                .sessionCreationPolicy(
                        SessionCreationPolicy.STATELESS
                )

                .and()
                .authorizeRequests()
                .antMatchers(&quot;/sign-api/sign-up&quot;, &quot;/sign-api/sign-in&quot;,
                        &quot;/sign-api/exception&quot;).permitAll()
                .antMatchers(HttpMethod.GET, &quot;/product/**&quot;).permitAll()

                // 그 외
                .antMatchers(&quot;**exception**&quot;).permitAll()

                .anyRequest().hasRole(&quot;ADMIN&quot;)

                .and()
                .exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler())
                .and()
                .exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())

                .and()
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
                        UsernamePasswordAuthenticationFilter.class)
        ;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .antMatchers(
                        &quot;/v2/api-docs&quot;,
                        &quot;/swagger-resources/**&quot;,
                        &quot;/swagger-ui.html&quot;,
                        &quot;/webjars/**&quot;,
                        &quot;/swagger/**&quot;,
                        &quot;/sign-api/exception&quot;
                );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;HttpSecurity configure()&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대표적인 기능&lt;/b&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;/li&gt;
&lt;li&gt;인증 실패 시 발생하는 예외 처리&lt;/li&gt;
&lt;li&gt;인증 로직 커스터마이징&lt;/li&gt;
&lt;li&gt;csrf, cors 등의 스프링 시큐리티 설정&lt;/li&gt;
&lt;/ul&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;httpBasic().disable()
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;기본값인 UI 사용 설정 비활성화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;csrf().disable()
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;CSRF는 Cross-Site Request Forgery의 줄임말 - '사이트 간 요청 위조'를 의미&lt;/li&gt;
&lt;li&gt;REST API에서는 CSRF 보안이 필요 없기 때문에 비활성화&lt;/li&gt;
&lt;li&gt;csrf() 메서드는 CSRF 토큰을 발급해서 클라이언트로부터 요청을 받을 때마다 토큰 검증&lt;/li&gt;
&lt;li&gt;브라우저 사용 환경이 아니라면 비활성화해도 크게 문제가 되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1701942817177&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;사이트 간 요청 위조

웹 애플리케이션의 취약점 중 하나로서
사용자가 자신의 의지와 무관하게 웹 애플리케이션을 대상으로 공격자가 의도한 행동을 함으로써
특정 페이지의 보안을 취약하게 한다거나 수정, 삭제 등의 작업을 하는 공격 방법&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;REST API 기반 애플리케이션의 동작 방식 설정&lt;/li&gt;
&lt;li&gt;Session 대신 JWT 토큰으로 인증 처리 하기 때문에 STATELESS 로 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;authorizeRequest()
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;애플리케이션에 들어오는 요청에 대한 사용 권한 체크&lt;/li&gt;
&lt;li&gt;antMatchers() 메서드는 antPattern을 통해 권한 설정&lt;/li&gt;
&lt;li&gt;permitAll(): 모두에게 허용&lt;/li&gt;
&lt;li&gt;'/product'로 시작하는 GET 요청 모두 허용&lt;/li&gt;
&lt;li&gt;'exception' 단어가 들어간 경로는 모두 허용&lt;/li&gt;
&lt;li&gt;기타 요청은 인증된 권한을 가진 사용자에게 허용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;exceptionHandling().accessDeniedHandler()
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;권한 확인 과정에서 예외 발생할 경우 예외 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;exceptionHandling().authenticationEntryPoint()
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;인증 과정에서 예외 발생할 경우 예외 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;addFilterBefore()
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;커스텀 필터를 어느 필터 앞에 추가할 것인지 설정할 것인지 결정&lt;/li&gt;
&lt;li&gt;UsernamePasswordAuthenticationFilter 앞에 JwtAuthenticationFilter를 추가한다는 의미&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;WebSecurity configure()&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSecurity는 HttpSecurity 앞단에 적용됨.&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;커스텀 AccessDeniedHandler, AuthenticationEntryPoint 구현&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;CustomAccessDeniedHandler 클래스 (config/secutiry/CustomAccessDeniedHandler.java)&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException)
            throws IOException, ServletException {

        log.info(&quot;[handle] 접근이 막혔을 경우 경로 리다이렉트&quot;);
        response.sendRedirect(&quot;/sign-api/exception&quot;);

    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AccessDeniedException은 &lt;b&gt;액세스 권한이 없는 리소스에 접근할 경우 발생하는 예외.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;이 예외를 처리하기 위해 AccessDeniedHandler 인터페이스가 사용됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;CustomAuthenticationEntryPoint 클래스 (config/secutiry/CustomAuthenticationEntryPoint.java)&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ObjectMapper objectMapper = new ObjectMapper();
        log.info(&quot;[commence] 인증 실패로 response.sendError 발생&quot;);

        EntryPointErrorResponse entryPointErrorResponse = new EntryPointErrorResponse();
        entryPointErrorResponse.setMsg(&quot;인증이 실패하였습니다.&quot;);

        response.setStatus(401);
        response.setContentType(&quot;application/json&quot;);
        response.setCharacterEncoding(&quot;utf-8&quot;);
        response.getWriter().write(objectMapper.writeValueAsString(entryPointErrorResponse));
    }
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;EntryPointErrorResponse 클래스 (data/dto/EntryPointErrorResponse.java)&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class EntryPointErrorResponse {

    private String msg;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;회원가입과 로그인 구현&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;SignUpResultDto 클래스 (data/dto/SignUpResultDto.java)&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class SignUpResultDto {
    
    private boolean success;
    
    private int code;
    
    private String ms;
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;SignInResultDto 클래스 (data/dto/SignInResultDto.java)&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class SignInResultDto extends SignUpResultDto{

    private String token;

    @Builder
    public SignInResultDto(boolean success, int code, String msg, String token) {
        super(success, code, msg);
        this.token = token;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;SignService 인터페이스 (service/SignService.java)&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface SignService {

    SignUpResultDto signUp(String id, String password, String name, String role);

    SignInResultDto signIn(String id, String password) throws RuntimeException;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;SignServiceImpl 클래스 (service/impl/SignServiceImpl.java)&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Service
@RequiredArgsConstructor
public class SignServiceImpl implements SignService {

    public UserRepository userRepository;
    public JwtTokenProvider jwtTokenProvider;
    public PasswordEncoder passwordEncoder;

    @Autowired
    public SignServiceImpl(UserRepository userRepository,
                           JwtTokenProvider jwtTokenProvider,
                           PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.jwtTokenProvider = jwtTokenProvider;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public SignUpResultDto signUp(String id, String password, String name, String role) {
        log.info(&quot;[getSignUpResult] 회원 가입 정보 전달&quot;);
        User user;
        if (role.equalsIgnoreCase(&quot;admin&quot;)) {
            user = User.builder()
                    .uid(id)
                    .name(name)
                    .password(passwordEncoder.encode(password))
                    .roles(Collections.singletonList(&quot;ROLE_ADMIN&quot;))
                    .build();
        } else {
            user = User.builder()
                    .uid(id)
                    .name(name)
                    .password(passwordEncoder.encode(password))
                    .roles(Collections.singletonList(&quot;ROLE_USER&quot;))
                    .build();
        }

        User savedUser = userRepository.save(user);
        SignUpResultDto signUpResultDto = new SignUpResultDto();
        log.info(&quot;[getSignUpResult] userEntity 값이 들어왔는지 확인 후 결과값 주입&quot;);

        if (!savedUser.getName().isEmpty()) {
            log.info(&quot;[getSignUpResult] 정상 처리 완료&quot;);
            setSuccessResult(signUpResultDto);
        } else {
            log.info(&quot;[getSignUpResult] 실패 처리 완료&quot;);
            setFailResult(signUpResultDto);
        }

        return signUpResultDto;
    }

    @Override
    public SignInResultDto signIn(String id, String password) {
        log.info(&quot;[getSignInResult] signDataHandler 로 회원 정보 요청&quot;);
        User user = userRepository.getByUid(id);
        log.info(&quot;[getSignInResult] id : {}&quot;, id);

        log.info(&quot;[getSignInResult] 패스워드 비교&quot;);
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new RuntimeException();
        }
        log.info(&quot;[getSignInResult] 패스워드 일치&quot;);

        log.info(&quot;[getSignInResult] SignInResultDto 객체 생성&quot;);
        SignInResultDto signInResultDto =
                SignInResultDto.builder()
                        .token(jwtTokenProvider.createToken(
                                        String.valueOf(user.getUid()),
                                        user.getRoles()
                                )
                        )
                        .build();
        
        log.info(&quot;[getSignInResult] SignInResultDto 객체에 값 주입&quot;);
        setSuccessResult(signInResultDto);
        
        return signInResultDto;
    }
    
    private void setSuccessResult(SignUpResultDto result) {
        result.setSuccess(true);
        result.setCode(CommonResponse.SUCCESS.getCode());
        result.setMsg(CommonResponse.SUCCESS.getMsg());
    }
    
    private void setFailResult(SignUpResultDto result) {
        result.setSuccess(false);
        result.setCode(CommonResponse.FAIL.getCode());
        result.setMsg(CommonResponse.FAIL.getMsg());
    }
}&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;PasswordEncoder 활용을 위해 Bean으로 등록&lt;/b&gt;해야 함.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;PasswordEncoderConfiguration 클래스 (config/PasswordEncoderConfiguration.java)&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class PasswordEncoderConfiguration {
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
    
}&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;SignServiceImpl 내부 로직 동작 과정&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;id를 기반으로 UserRepository에서 User 엔티티를 가져옴.&lt;/li&gt;
&lt;li&gt;PasswordEncoder를 사용해 db에 저장되어있는 password 와 입력받은 password 일치 여부 확인&lt;/li&gt;
&lt;li&gt;password가 일치해서 인증 통과 -&amp;gt; JwtTokenProvider를 통해 id와 role 값으로 토큰 생성 후 전달&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;CommonResponse 클래스 (common/CommonResponse.java)&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public enum CommonResponse {
    
    SUCCESS(0, &quot;Success&quot;), FAIL(-1, &quot;Fail&quot;);
    
    int code;
    String msg;
    
    CommonResponse(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    
    public int getCode() {
        return code;
    }
    
    public String getMsg() {
        return msg;
    }
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;SignController 전체 코드 (controller/SignController.java)&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@RestController
@RequestMapping(&quot;/sign-api&quot;)
public class SignController {

    private final SignService signService;

    @Autowired
    public SignController(SignService signService) {
        this.signService = signService;
    }

    @PostMapping(value = &quot;/sign-in&quot;)
    public SignInResultDto signIn(
            @ApiParam(value = &quot;ID&quot;, required = true) @RequestParam String id,
            @ApiParam(value = &quot;Password&quot;, required = true) @RequestParam String password)
            throws RuntimeException {
        
        log.info(&quot;[signIn] 로그인 시도. id : {}, pw : ****&quot;, id);
        SignInResultDto signInResultDto = signService.signIn(id, password);
        
        if (signInResultDto.getCode() == 0) {
            log.info(&quot;[signIn] 정상적으로 로그인되었습니다. id : {}, token : {}&quot;, id, signInResultDto.getToken());
        }
        
        return signInResultDto;
    }
    
    @PostMapping(value = &quot;/sign-up&quot;)
    public SignUpResultDto signUp(
            @ApiParam(value = &quot;ID&quot;, required = true) @RequestParam String id,
            @ApiParam(value = &quot;비밀번호&quot;, required = true) @RequestParam String password,
            @ApiParam(value = &quot;이름&quot;, required = true) @RequestParam String name,
            @ApiParam(value = &quot;권한&quot;, required = true) @RequestParam String role) {
        log.info(&quot;[signUp] 회원가입을 수행합니다. id : {}, password: ****, name : {}, role : {}&quot;, id, name, role);
        SignUpResultDto signUpResultDto = signService.signUp(id, password, name, role);
        
        log.info(&quot;[signUp] 회원가입을 완료했습니다. id : {]&quot;, id);
        return signUpResultDto;
    }
    
    @PostMapping(value = &quot;exception&quot;)
    public void exceptionTest() throws RuntimeException {
        throw new RuntimeException(&quot;접근이 금지되었습니다.&quot;);
    }
    
    @ExceptionHandler(value = RuntimeException.class)
    public ResponseEntity&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; ExceptionHandler(RuntimeException e) {
        HttpHeaders responseHeaders = new HttpHeaders();
        // responseHeaders.add(HttpHeaders.CONTENT_TYPE, &quot;application/json&quot;);
        HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
        
        log.error(&quot;ExceptionHandler 호출, {}, {}&quot;, e.getCause(), e.getMessage());
        
        Map&amp;lt;String, String&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
        map.put(&quot;error type&quot;, httpStatus.getReasonPhrase());
        map.put(&quot;code&quot;, &quot;400&quot;);
        map.put(&quot;message&quot;, &quot;에러 발생&quot;);
        
        return new ResponseEntity&amp;lt;&amp;gt;(map, responseHeaders, httpStatus);
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Springboot/이론</category>
      <category>JWT</category>
      <category>spring</category>
      <category>SpringBoot</category>
      <category>SpringSecurity</category>
      <category>인가</category>
      <category>인증</category>
      <author>NiceTry</author>
      <guid isPermaLink="true">https://jnjeaaaat.tistory.com/76</guid>
      <comments>https://jnjeaaaat.tistory.com/76#entry76comment</comments>
      <pubDate>Thu, 7 Dec 2023 16:52:58 +0900</pubDate>
    </item>
    <item>
      <title>[Springboot] 서비스의 인증과 권한 부여 - JWT</title>
      <link>https://jnjeaaaat.tistory.com/75</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;JWT(JSON Web Token)&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JWT 특징&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;당사자 간의 정보를 JSON 형태&lt;/b&gt;로 &lt;b&gt;안전하게 전송하기 위한 토큰&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;URL로 이용할 수 있는 문자열로만 구성되어 있으며, &lt;b&gt;디지털 서명이 적용되어 있어 신뢰 가능&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주로 서버와&lt;/b&gt;의 통신에서 &lt;b&gt;권한 인가를 위해 사용&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTP 구성요소 어디든 위치&lt;/b&gt;할 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JWT 구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;헤더(Header)&lt;/span&gt;&lt;/b&gt;, &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;내용(Payload)&lt;/b&gt;&lt;/span&gt;, &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;서명(Signature)&lt;/span&gt;&lt;/b&gt; 을 &quot;.&quot;으로 구분하여 구성된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;419&quot; data-origin-height=&quot;79&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4hW5c/btsBxdcjmVc/C09QsXODcLS2bPsYxusT4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4hW5c/btsBxdcjmVc/C09QsXODcLS2bPsYxusT4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4hW5c/btsBxdcjmVc/C09QsXODcLS2bPsYxusT4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4hW5c%2FbtsBxdcjmVc%2FC09QsXODcLS2bPsYxusT4K%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;419&quot; height=&quot;79&quot; data-origin-width=&quot;419&quot; data-origin-height=&quot;79&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;헤더&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;검증과 관련&lt;/b&gt;된 내용&lt;/p&gt;
&lt;pre id=&quot;code_1701934591078&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  /*
  해싱 알고리즘 지정,
  SHA256 또는 RSA 사용
  서명 부분에서 사용됨
  &quot;HS256&quot; 은 &quot;HMAC SHA256&quot; 알고리즘 사용을 의미
  */
  &quot;alg&quot;: &quot;HS256&quot;,
  /*
  토큰의 타입 지정
  */
  &quot;typ&quot;: &quot;JWT&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 완성된 헤더는 Base64Url 형식으로 인코딩되어 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;내용&lt;/b&gt;&lt;/h3&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;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;클레임(Claim)&lt;/b&gt;&lt;/span&gt; 이라고 하며&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;등록된 클레임(Registered Claims)&lt;/li&gt;
&lt;li&gt;공개 클레임(Public Claims)&lt;/li&gt;
&lt;li&gt;비공개 클레임(Private Claims)&lt;/li&gt;
&lt;/ul&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;등록된 클레임(Registered Claims)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필수는 아니지만 이미 이름이 정해져 있는 클레임을 뜻함&lt;/li&gt;
&lt;li&gt;iss: JWT의 발급자(Issuer) 주체를 나타냄. iss의 값은 문자열이나 URI를 포함하는 대소문자를 구분하는 문자열&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #5733b1;&quot;&gt;sub&lt;/span&gt;&lt;/b&gt;: JWT의 제목(Subject)&lt;/li&gt;
&lt;li&gt;aud: JWT의 수신인(Audience). JWT를 처리하려는 각 주체는 해당 값으로 자신을 식별해야함. 요청을 처리하는 주체가 'aud' 값으로 자신을 식별하지 않으면 JWT는 거부됨.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #5733b1;&quot;&gt;exp&lt;/span&gt;&lt;/b&gt;: JWT의 만료기간(Expriation). NumericDate 형식으로 지정&lt;/li&gt;
&lt;li&gt;nbf: 'Not Before'를 의미&lt;/li&gt;
&lt;li&gt;iat: JWT가 발급된 시간(issued at)&lt;/li&gt;
&lt;li&gt;jti: JWT의 식별자(JWT ID). 주로 중복 처리를 방지하기 위해 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;공개 클레임(Public Claims)&lt;/b&gt;&lt;/h4&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;비공개 클레임(Private Claims)&lt;/b&gt;&lt;/h4&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;blockquote data-ke-style=&quot;style2&quot;&gt;JWT 내용 예시&lt;/blockquote&gt;
&lt;pre id=&quot;code_1701935087820&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;sub&quot;: &quot;wikibooks payload&quot;,
  &quot;exp&quot;: &quot;1602076408&quot;,
  &quot;userId&quot;: &quot;wikibooks&quot;,
  &quot;username&quot;: &quot;jnjeaaaat&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 완성된 내용은 Base64Url 형식으로 인코딩되어 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;서명&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;인코딩된 헤더, 인코딩된 내용, 비밀키, 헤더의 알고리즘 속성값&amp;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;HMAC SHA256 알고리즘을 사용해서 서명을 생성한다면,&lt;/p&gt;
&lt;pre id=&quot;code_1701935227993&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HMACSHA256(
  base64UrlEncode(header) + &quot;.&quot; +
  base64UrlEncode(payload),
  secret
  )&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JWT 디버거 사용하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jwt.io/#debugger-io&quot;&gt;https://jwt.io/#debugger-io&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1701935316303&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;JWT.IO&quot; data-og-description=&quot;JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.&quot; data-og-host=&quot;jwt.io&quot; data-og-source-url=&quot;https://jwt.io/#debugger-io&quot; data-og-url=&quot;http://jwt.io/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Spnyi/hyUIz0xU78/PLB75q4kpPfWufK667CTa1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/7jX9k/hyUIr9jP28/PLqyBPosgEE9e9xtd2RKS1/img.png?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot;&gt;&lt;a href=&quot;https://jwt.io/#debugger-io&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jwt.io/#debugger-io&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Spnyi/hyUIz0xU78/PLB75q4kpPfWufK667CTa1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/7jX9k/hyUIr9jP28/PLqyBPosgEE9e9xtd2RKS1/img.png?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;JWT.IO&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jwt.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이트에서 Header, Payload, Signature 내용 확인 가능&lt;/p&gt;</description>
      <category>Springboot/이론</category>
      <category>JWT</category>
      <category>spring</category>
      <category>SpringBoot</category>
      <category>SpringSecurity</category>
      <author>NiceTry</author>
      <guid isPermaLink="true">https://jnjeaaaat.tistory.com/75</guid>
      <comments>https://jnjeaaaat.tistory.com/75#entry75comment</comments>
      <pubDate>Wed, 6 Dec 2023 23:44:59 +0900</pubDate>
    </item>
    <item>
      <title>[Springboot] Spring Security 필터 동작</title>
      <link>https://jnjeaaaat.tistory.com/74</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;보안 필터체인 종류 및 순서&lt;/b&gt;&lt;/h2&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;b&gt;공식 문서에서 소개하는 필터의 실행 순서&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ChannelProcessingFilter&lt;/li&gt;
&lt;li&gt;WebAsyncManagerIntergrationFilter&lt;/li&gt;
&lt;li&gt;SecurityContextPersistenceFilter&lt;/li&gt;
&lt;li&gt;HeaderWriterFilter&lt;/li&gt;
&lt;li&gt;CorsFilter&lt;/li&gt;
&lt;li&gt;CsrfFilter&lt;/li&gt;
&lt;li&gt;LogoutFilter&lt;/li&gt;
&lt;li&gt;OAuth2AuthorizationRequestRedirectFilter&lt;/li&gt;
&lt;li&gt;Saml2WebSsoAuthenticationRequestFilter&lt;/li&gt;
&lt;li&gt;X509AuthenticationFilter&lt;/li&gt;
&lt;li&gt;AbstractPreAuthenticationedProcessingFilter&lt;/li&gt;
&lt;li&gt;CasAuthenticationFilter&lt;/li&gt;
&lt;li&gt;OAuth2LoginAuthenticationFilter&lt;/li&gt;
&lt;li&gt;Saml2WebSsoAuthenticationFilter&lt;/li&gt;
&lt;li&gt;Saml2WebSsoAuthenticationFilter&lt;/li&gt;
&lt;li&gt;UsernamePasswordAuthenticationFilter&lt;/li&gt;
&lt;li&gt;OpenIDAuthenticationFilter&lt;/li&gt;
&lt;li&gt;DefaultLoginPageGeneratingFilter&lt;/li&gt;
&lt;li&gt;DefaultLogoutPageGeneratingFilter&lt;/li&gt;
&lt;li&gt;ConcurrentSessionFilter&lt;/li&gt;
&lt;li&gt;DigestAuthenticationFilter&lt;/li&gt;
&lt;li&gt;BearerTokenAuthenticationFilter&lt;/li&gt;
&lt;li&gt;BasicAuthenticationFilter&lt;/li&gt;
&lt;li&gt;RequestCacheAwareFilter&lt;/li&gt;
&lt;li&gt;SecurityContextHolderAwareRequestFilter&lt;/li&gt;
&lt;li&gt;JaasApiIntegrationFilter&lt;/li&gt;
&lt;li&gt;RememberMeAuthenticationFilter&lt;/li&gt;
&lt;li&gt;AnonymousAuthenticationFilter&lt;/li&gt;
&lt;li&gt;SessionManagementFilter&lt;/li&gt;
&lt;li&gt;ExceptionTraslationFilter&lt;/li&gt;
&lt;li&gt;FilterSecurityInterceptor&lt;/li&gt;
&lt;li&gt;SwitchUserFilter&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;보안 필터체인 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;보안 필터체인은 WebSecurityConfigurerAdapter 클래스를 상속받아 설정&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;필터체인 프록시는 WebSecurityConfigurerAdapter 클래스를 &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;b&gt;WebSecurityConfigurerAdapter의 @Order 어노테이션으로 필터 순서를 정의&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;별다른 설정이 없다면 UsernamePasswordAuthenticationFilter를 통해 인증 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;UsernamePasswordAuthenticationFilter를 통한 인증 과정&lt;/blockquote&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;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AbCOE/btsBuati37c/ioREEh3a0yrxng403ACSkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AbCOE/btsBuati37c/ioREEh3a0yrxng403ACSkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AbCOE/btsBuati37c/ioREEh3a0yrxng403ACSkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAbCOE%2FbtsBuati37c%2FioREEh3a0yrxng403ACSkK%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;540&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;540&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트로부터 요청 -&amp;gt; 서블릿 필터에서 SecurityFilterChain 으로 작업 위임, 그중 UsernamePasswordAuthenticationFilter (위 그림에서 AuthenticationFilter에 해당) 에서 인증 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AuthenticationFilter는&lt;/b&gt; 요청 객체 (HttpServletRequest) 에서 username 과 password 를 추출해서 토큰 생성 후 AuthenticationManager에게 토큰 전달.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AuthenticationManager는&lt;/b&gt; 인터페이스, 일반적인 구현체는 ProviderManager&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ProviderManager는&lt;/b&gt; 인증을 위해 AuthenticationProvider로 토큰 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AuthenticationProvider는&lt;/b&gt; 토큰의 정보를 UserDetailsService에 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UserDetailsService는&lt;/b&gt; 전달받은 정보를 통해 데이터베이스에서 일치하는 사용자를 찾아 UserDetails 객체 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;생성된 UserDetails 객체는&lt;/b&gt; AuthenticationProvider로 전달, 해당 Provider에서 인증 수행 -&amp;gt; 성공하면 ProviderManager로 권한을 담은 토큰 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ProviderManager는&lt;/b&gt; 검증된 토큰을 AuthenticationFilter로 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AuthenticationFilter는&lt;/b&gt; 검증된 토큰을 SecurityContextHolder에 있는 &lt;b&gt;SecurityContext에 저장&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UsernamePasswordAuthenticationFilter는 접근 권한을 확인.&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;또는 JWT 토큰을 사용해 인증을 수행할 경우 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JWT 관련 필터 생성후&lt;/b&gt; UsernamePasswordAuthenticationFilter 앞에 배치해 &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;a href=&quot;https://jnjeaaaat.tistory.com/75&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JWT 생성, 설정&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1701873959203&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Springboot] 서비스의 인증과 권한 부여 - JWT&quot; data-og-description=&quot;JWT(JSON Web Token) JWT 특징 당사자 간의 정보를 JSON 형태로 안전하게 전송하기 위한 토큰. URL로 이용할 수 있는 문자열로만 구성되어 있으며, 디지털 서명이 적용되어 있어 신뢰 가능. 주로 서버와의&quot; data-og-host=&quot;jnjeaaaat.tistory.com&quot; data-og-source-url=&quot;https://jnjeaaaat.tistory.com/75&quot; data-og-url=&quot;https://jnjeaaaat.tistory.com/75&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/l6ncg/hyUIwbubiC/X3BqcXjOw9QQZwFNoBgbO1/img.jpg?width=716&amp;amp;height=205&amp;amp;face=26_13_173_174,https://scrap.kakaocdn.net/dn/PJU96/hyUIy8cbac/vY6worhVIDs9k9VP4KVOaK/img.jpg?width=716&amp;amp;height=205&amp;amp;face=26_13_173_174,https://scrap.kakaocdn.net/dn/bwsrPF/hyUIwifnmr/kIdAWcxDt8mhMG4SfwnXfK/img.jpg?width=1080&amp;amp;height=1481&amp;amp;face=0_0_1080_1481&quot;&gt;&lt;a href=&quot;https://jnjeaaaat.tistory.com/75&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jnjeaaaat.tistory.com/75&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/l6ncg/hyUIwbubiC/X3BqcXjOw9QQZwFNoBgbO1/img.jpg?width=716&amp;amp;height=205&amp;amp;face=26_13_173_174,https://scrap.kakaocdn.net/dn/PJU96/hyUIy8cbac/vY6worhVIDs9k9VP4KVOaK/img.jpg?width=716&amp;amp;height=205&amp;amp;face=26_13_173_174,https://scrap.kakaocdn.net/dn/bwsrPF/hyUIwifnmr/kIdAWcxDt8mhMG4SfwnXfK/img.jpg?width=1080&amp;amp;height=1481&amp;amp;face=0_0_1080_1481');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Springboot] 서비스의 인증과 권한 부여 - JWT&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;JWT(JSON Web Token) JWT 특징 당사자 간의 정보를 JSON 형태로 안전하게 전송하기 위한 토큰. URL로 이용할 수 있는 문자열로만 구성되어 있으며, 디지털 서명이 적용되어 있어 신뢰 가능. 주로 서버와의&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jnjeaaaat.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Springboot/이론</category>
      <category>Filter</category>
      <category>Java</category>
      <category>spring</category>
      <category>SpringBoot</category>
      <category>SpringSecurity</category>
      <category>백엔드</category>
      <category>보안</category>
      <author>NiceTry</author>
      <guid isPermaLink="true">https://jnjeaaaat.tistory.com/74</guid>
      <comments>https://jnjeaaaat.tistory.com/74#entry74comment</comments>
      <pubDate>Wed, 6 Dec 2023 23:25:37 +0900</pubDate>
    </item>
    <item>
      <title>[Springboot] 서비스의 인증과 권한 부여 - SpringSecurity 소개</title>
      <link>https://jnjeaaaat.tistory.com/73</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;보안 용어&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인증 - authentication&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;사용자가 누구인지 확인&lt;/b&gt;&lt;/span&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;로그인 성공 -&amp;gt; 사용자에게 토큰(token) 전달&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰이 없는 사용자는 리소스에 접근 x&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인가 - authorization&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;토큰을 가진 인증된 사용자&lt;/b&gt;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;접근 주체 - principal&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션의 기능을 사용하는 주체 -&amp;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;b&gt;인증 과정&lt;/b&gt;(신뢰하는 접근 주체인지 확인) -&amp;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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;스프링 시큐리티&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;스프링 시큐리티 동작 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서블릿 필터(Servlet Filter)를 기반으로 동작, DispatcherServlet 앞에 필터가 배치&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;827&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bp6eWe/btsBzMcOyZi/z5UudkSfyWmabuu872sc5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bp6eWe/btsBzMcOyZi/z5UudkSfyWmabuu872sc5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bp6eWe/btsBzMcOyZi/z5UudkSfyWmabuu872sc5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbp6eWe%2FbtsBzMcOyZi%2Fz5UudkSfyWmabuu872sc5k%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;800&quot; height=&quot;380&quot; data-origin-width=&quot;827&quot; data-origin-height=&quot;393&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;필터체인(FilterChain)&lt;/b&gt;은 서블릿 컨테이너에서 관리하는 ApplicationFilterChain을 의미&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;span style=&quot;color: #006dd7;&quot;&gt;클라이언트 요청&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 서블릿 컨테이너에서 URI 확인, 필터와 서블릿 매핑&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 스프링 시큐리티는 사용하려는 필터체인을 &lt;b&gt;서블릿 컨테이너의 필터 사이에서 동작&lt;/b&gt; - &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;DelegantingFilterProxy 사용&lt;/b&gt;&lt;/span&gt;&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;572&quot; data-origin-height=&quot;547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgkHGa/btsBvf1JOrw/bcpVXibC4T8Zzjra45WZo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgkHGa/btsBvf1JOrw/bcpVXibC4T8Zzjra45WZo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgkHGa/btsBvf1JOrw/bcpVXibC4T8Zzjra45WZo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgkHGa%2FbtsBvf1JOrw%2FbcpVXibC4T8Zzjra45WZo1%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;550&quot; height=&quot;526&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;547&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;DelegatingFilterProxy&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 서블릿 컨테이너의 생명주기와 스프링 애플리케이션 컨텍스트(Application Context) &lt;b&gt;다리 역할&lt;/b&gt;하는 필터 구현체&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 표준 서블릿 필터 구현, 역할 위임할 &lt;b&gt;필터체인 프록시(FilterChainProxy)를 내부에 갖고 있음&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;b&gt;필터체인 프록시&lt;/b&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;- 보안 필터체인(SecurityFilterChain)을 통해 많은 보안 필터(Security Filter) 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 사용 가능한 보안 필터 체인은 List 형식으로 담을 수 있게 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- URI 패턴에 따라 특정 보안필터 체인 선택해서 사용&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://jnjeaaaat.tistory.com/74&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;보안 필터체인 종류 및 동작순서&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1701873600591&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Springboot] Spring Security 필터 동작&quot; data-og-description=&quot;보안 필터체인 종류 및 순서 보안필터 체인에서 사용하는 필터에는 여러 종류가 있다. 공식 문서에서 소개하는 필터의 실행 순서 ChannelProcessingFilter WebAsyncManagerIntergrationFilter SecurityContextPersistence&quot; data-og-host=&quot;jnjeaaaat.tistory.com&quot; data-og-source-url=&quot;https://jnjeaaaat.tistory.com/74&quot; data-og-url=&quot;https://jnjeaaaat.tistory.com/74&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bjLK94/hyUIuEJsuY/wAmOW0cnjThP7EaLDNZ9E1/img.jpg?width=716&amp;amp;height=205&amp;amp;face=26_13_173_174,https://scrap.kakaocdn.net/dn/2U3zR/hyUItlvyyV/2inDKrXBAGLXVC6oiA6cK1/img.jpg?width=716&amp;amp;height=205&amp;amp;face=26_13_173_174,https://scrap.kakaocdn.net/dn/uKoai/hyUIrutURr/nWE5VkjvT7LGwIYmyuHPaK/img.jpg?width=1080&amp;amp;height=1481&amp;amp;face=0_0_1080_1481&quot;&gt;&lt;a href=&quot;https://jnjeaaaat.tistory.com/74&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jnjeaaaat.tistory.com/74&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bjLK94/hyUIuEJsuY/wAmOW0cnjThP7EaLDNZ9E1/img.jpg?width=716&amp;amp;height=205&amp;amp;face=26_13_173_174,https://scrap.kakaocdn.net/dn/2U3zR/hyUItlvyyV/2inDKrXBAGLXVC6oiA6cK1/img.jpg?width=716&amp;amp;height=205&amp;amp;face=26_13_173_174,https://scrap.kakaocdn.net/dn/uKoai/hyUIrutURr/nWE5VkjvT7LGwIYmyuHPaK/img.jpg?width=1080&amp;amp;height=1481&amp;amp;face=0_0_1080_1481');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Springboot] Spring Security 필터 동작&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;보안 필터체인 종류 및 순서 보안필터 체인에서 사용하는 필터에는 여러 종류가 있다. 공식 문서에서 소개하는 필터의 실행 순서 ChannelProcessingFilter WebAsyncManagerIntergrationFilter SecurityContextPersistence&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jnjeaaaat.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Springboot/이론</category>
      <category>Java</category>
      <category>security</category>
      <category>spring</category>
      <category>SpringBoot</category>
      <category>SpringSecurity</category>
      <category>백엔드</category>
      <author>NiceTry</author>
      <guid isPermaLink="true">https://jnjeaaaat.tistory.com/73</guid>
      <comments>https://jnjeaaaat.tistory.com/73#entry73comment</comments>
      <pubDate>Wed, 6 Dec 2023 17:13:16 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] @ManyToOne 연관관계에 따른 데이터베이스 오류</title>
      <link>https://jnjeaaaat.tistory.com/72</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;에러 내용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 진행 중에 &lt;b&gt;하나의 User(유저)&lt;/b&gt;에 &lt;b&gt;여러 개의 Shop(상점)을 등록&lt;/b&gt;해두기 위해 Shop Entity에 &lt;b&gt;@ManyToOne 단방향 연관관계를 설정&lt;/b&gt;해두었다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
public class Shop extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 상점을 등록한 유저 id
    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = &quot;owner_id&quot;)
    @ToString.Exclude
    private User owner;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ShopServiceImpl에 deleteShop() 구현&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Override
@Transactional
public ShopDeleteResponse deleteShop(Long shopId, ShopDeleteRequest request) {
    log.info(&quot;[deleteShop] 상점 삭제 - 상점 id : {}&quot;, shopId);

    // 상점 삭제
    shopRepository.deleteById(shopId);
    log.info(&quot;[deleteShop] 삭제 완료&quot;);

    return ShopDeleteResponse.from(
            ShopDto.builder()
                    .id(shopId)
                    .build()
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 코드를 짜고 postman으로 테스트를 했는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Shop Table에 foreign key로 저장되어있던 User 정보까지 같이 삭제되는 현상이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/h2&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;Shop Entity에서 &lt;b&gt;Owner(User) 정보를 null 로 set&lt;/b&gt; 한 다음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Shop을 삭제해주면 해결!&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Override
@Transactional
public ShopDeleteResponse deleteShop(Long shopId, ShopDeleteRequest request) {
    log.info(&quot;[deleteShop] 상점 삭제 - 상점 id : {}&quot;, shopId);

    Shop shop = shopRepository.findById(shopId)
                .orElseThrow(() -&amp;gt; new BaseException(SHOP_NOT_FOUND));
                
    // 상점 삭제
    shop.setOwner(null); // 추가한 내용
    shopRepository.deleteById(shopId);
    log.info(&quot;[deleteShop] 삭제 완료&quot;);

    return ShopDeleteResponse.from(
            ShopDto.builder()
                    .id(shopId)
                    .build()
    );
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>JPA</category>
      <category>error</category>
      <category>JPA</category>
      <category>ManyToOne</category>
      <category>spring</category>
      <category>SpringBoot</category>
      <author>NiceTry</author>
      <guid isPermaLink="true">https://jnjeaaaat.tistory.com/72</guid>
      <comments>https://jnjeaaaat.tistory.com/72#entry72comment</comments>
      <pubDate>Sat, 2 Dec 2023 12:48:14 +0900</pubDate>
    </item>
  </channel>
</rss>