Jekyll2023-06-12T07:47:59+00:00https://smjeon.dev/feed.xmlSMJ BlogarchivingMartinoeeen3@gmail.comGithub CLI 사용기2020-09-19T09:00:59+00:002020-09-19T09:00:59+00:00https://smjeon.dev/etc/github-cli<p>9월 17일 날짜로 Github CLI 1.0 버전이 릴리즈 되었습니다. Github에 들어가보니 옆에 뭔가 거슬리는 것이 있길래 확인해보고 사용해본 후기를 정리하여 포스팅합니다.</p>
<h2 id="무엇을-할-수-있나">무엇을 할 수 있나</h2>
<p>릴리즈 페이지에 가보면, Github CLI 1.0 으로 다음과 같은 것들을 할 수 있다고 나와있다.</p>
<ol>
<li>전체 GitHub 워크플로우를 터미널에서 할 수 있다.</li>
<li>GitHub API를 호출해서 거의 모든 작업을 스크립트화 할 수 있고, 어떤 커맨드에도 커스텀 alias를 설정할 수 있다.</li>
<li>Github enterprise도 연결할 수 있다.</li>
</ol>
<h2 id="실제-사용법">실제 사용법</h2>
<p>소개 페이지에 나와있는 예시를 기반으로 실제로 사용해보자. 다음과 같은 순서로 해볼 생각이다.</p>
<ol>
<li>Github에 public repository 생성</li>
<li>master 브랜치에 커밋 푸시</li>
<li>issue 생성, label: enhancement</li>
<li>develop 브랜치에 커밋 푸시</li>
<li>develop -> master로 머지하는 Pull Request 생성</li>
<li>PR 내용 확인 후 머지</li>
</ol>
<p>MacOS 기준으로 설치하려면 <code class="language-plaintext highlighter-rouge">brew install gh</code> 명령을 통해 간단하게 설치할 수 있다. GitHub CLI를 설치 후 Github 계정에 인증을 받는 과정을 기본으로 거쳐야 다음 작업을 할 수 있다.</p>
<h3 id="repository-생성">Repository 생성</h3>
<p>Github CLI로 repository를 생성해본다. 명령어를 어떻게 쳐야하는지 모르기 때문에 <code class="language-plaintext highlighter-rouge">gh repo --help</code>명령으로 가능한 조합을 확인해봤다.</p>
<p><img src="/assets/img/gh_cli/gh-repo-help.png" alt="gh repo help" /></p>
<p><code class="language-plaintext highlighter-rouge">gh repo create [repo명]</code> 커맨드로 레포를 만들 수 있는 것을 알 수 있다.</p>
<p><img src="/assets/img/gh_cli/gh-repo-create.png" alt="gh repo create" /></p>
<p>이후에 master 브랜치에 커밋을 푸시하기 위해 README.md 파일을 생성하여 마스터 브랜치에 커밋을 하나 만들고 원격 레포에 푸시를 했다.</p>
<p><img src="/assets/img/gh_cli/github-initial-commit.png" alt="gh initial commit" /></p>
<h3 id="issue-생성">Issue 생성</h3>
<p>이제 이슈를 생성해보자. <code class="language-plaintext highlighter-rouge">gh issue create --label [원하는라벨]</code> 명령으로 생성한다. 명령어를 치면 대화형으로 선택지가 나와서 간단하게 이슈를 생성할 수 있다.</p>
<p><img src="/assets/img/gh_cli/gh-issue-create.png" alt="gh issue create" /></p>
<p><img src="/assets/img/gh_cli/gh-issue-create-web.png" alt="gh issue create web" /></p>
<p>다음으로 PR을 생성하기 전에 develop branch에 새로운 커밋을 쌓는다. 아래 사진과 같은 커밋로그를 만들었다.</p>
<p><img src="/assets/img/gh_cli/commit-log.png" alt="commit log" /></p>
<h3 id="pr-생성">PR 생성</h3>
<p>이제 develop branch에서 master branch로 merge 요청하는 PR을 생성한다. PR 내용에 issue close하는 커맨드까지 작성하여 PR merge시 위에서 생성한 Issue를 close 하도록 작성해보자</p>
<p>터미널에서 develop branch에 checkout 한 상태로 <code class="language-plaintext highlighter-rouge">gh pr create</code> 명령으로 develop에서 master로 PR을 생성한다. PR의 Body는 resolve: #1으로 작성했다. Reviewer, Assignee, Metadata들을 설정할 수도 있다.</p>
<p>Github CLI로 커맨드로 PR 관련하여 file diff(<code class="language-plaintext highlighter-rouge">gh pr diff [pr#]</code>)를 확인한다거나 pr 코드로 checkout(<code class="language-plaintext highlighter-rouge">gh pr checkout [pr#]</code>) 하여 코드를 확인한다거나 할 수 있다. 그러나 터미널로 파일 변경사항 비교하는 것은 개인적으로 어려워서 이 방법으로는 못할 것 같다. 그 대신 IntelliJ등을 사용하면서 PR로 checkout 후 추가된 테스트코드를 확인한다거나, 직접 코드를 보면서 리뷰를 할 수 있을 것 같다.</p>
<h3 id="pr-확인-후-merge">PR 확인 후 Merge</h3>
<p><code class="language-plaintext highlighter-rouge">gh pr diff 2</code> 명령으로 파일 변경사항을 확인 할 수 있다.(아래 사진처럼 나와서 확인하기 어렵다)</p>
<p><img src="/assets/img/gh_cli/gh-pr-diff.png" alt="pr diff" /></p>
<p>변경사항을 모두 확인했고, PR을 머지해도 될 상태라면 <code class="language-plaintext highlighter-rouge">gh pr merge [pr#]</code> 명령을 통해 merge를 한다. Github에서 PR Merge 했을 때의 옵션들이 나온다.(Merge commit 생성, Rebase merge, Squash merge)</p>
<p><img src="/assets/img/gh_cli/gh-pr-merge.png" alt="pr merge" /></p>
<p>여기서 squash merge를 선택하여 merge 한 결과는 아래와 같다.</p>
<p><img src="/assets/img/gh_cli/gh-pr-merge-web.png" alt="pr merge web" /></p>
<p>PR도 머지가 됬고 해당 PR이 머지 될 때 자동으로 연결된 Issue가 close 되도록 작성해두었기 때문에 자동으로 Issue까지 Close 되었다.(PR 머지 후 branch 삭제까지도 선택할 수 있다.)</p>
<h2 id="사용후기">사용후기</h2>
<p>Github CLI가 정식으로 릴리즈 되었다길래 일단 한번 사용해봤다. 익숙해지면 Web에서 굳이 확인하지 않고도 Github의 모든 기능을 사용할 수 있게 되었기 때문에 사용해봐도 괜찮을 것 같다. 이 포스팅에서 사용한 것들 외에도 굉장히 많은 기능들이 있지만, 내가 실제로도 Github에서 사용하는 기능들이 위에 나와있는 내용 정도라서 이정도면 충분한 것 같다.</p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://github.blog/2020-09-17-github-cli-1-0-is-now-available/">GitHub CLI 1.0 is now available</a></li>
</ul>Martinoeeen3@gmail.com9월 17일 날짜로 Github CLI 1.0 버전이 릴리즈 되었습니다. Github에 들어가보니 옆에 뭔가 거슬리는 것이 있길래 확인해보고 사용해본 후기를 정리하여 포스팅합니다.TCP 상태(CLOSE_WAIT, TIME_WAIT)2020-08-09T09:05:59+00:002020-08-09T09:05:59+00:00https://smjeon.dev/etc/tcp-state<p>트래픽을 만들어내는 어떤 툴을 사용하다가 CLOSE_WAIT 상태로 계속 유지되는 버그를 마주쳤다. 그런 의미에서 TCP 상태에 대해서 공부하고 정리한다.</p>
<h2 id="tcp-state">TCP State</h2>
<p>먼저 CLOSE_WAIT, TIME_WAIT가 어디서 나오는 용어인지 알아보자.</p>
<table>
<thead>
<tr>
<th>상태</th>
<th>설명</th>
</tr>
</thead>
<tbody>
<tr>
<td>CLOSE</td>
<td>커넥션 없음</td>
</tr>
<tr>
<td>LISTEN</td>
<td>Passive open, SYN을 기다리는 상태</td>
</tr>
<tr>
<td>SYN-SENT</td>
<td>SYN을 보내고 ACK를 기다리는 상태</td>
</tr>
<tr>
<td>SYN-RCVD</td>
<td>SYN+ACK을 보내고 ACK를 기다리는 상태</td>
</tr>
<tr>
<td>ESTABLISHED</td>
<td>커넥션이 생성된 상태, 데이터를 전송할 수 있다.</td>
</tr>
<tr>
<td>FIN-WAIT-1</td>
<td>첫 FIN이 보내진 상태, ACK를 기다리고 있다.</td>
</tr>
<tr>
<td>FIN-WAIT-2</td>
<td>첫 FIN에 대한 ACK를 받은 상태, 2번째 FIN을 기다리고 있다.</td>
</tr>
<tr>
<td>CLOSE-WAIT</td>
<td>첫 FIN을 받고 ACK를 보낸 상태, 어플리케이션의 종료를 기다리고 있다.</td>
</tr>
<tr>
<td>TIME-WAIT</td>
<td>2번째 FIN을 받고 ACK를 보낸 상태, 동일 포트와 주소에 커넥션이 생성되지 않도록 하는 시간(2MSL time out)을 기다리는 상태</td>
</tr>
<tr>
<td>LAST-ACK</td>
<td>2번쨰 FIN을 보내고 ACK를 기다리는 상태</td>
</tr>
<tr>
<td>CLOSING</td>
<td>양쪽이 동시에 닫기로 한 상태</td>
</tr>
</tbody>
</table>
<h3 id="tcp-connection-open">TCP Connection open</h3>
<p>TCP에서 connection established 까지 가는 과정을 그림으로 그렸다. Iperf라는 툴을 사용해서 Client로부터 Server로 100M의 트래픽을 전송하는 것을 대상으로 한다.</p>
<p><img src="/assets/img/tcp/tcp_open.gif" alt="tcp open" /></p>
<p>각 TCP state가 established 되면 클라이언트에서는 데이터를 전송한다.(100M)</p>
<h3 id="tcp-connection-close">TCP connection close</h3>
<p>TCP 연결 해제 과정에서의 TCP 상태에 대해 알아보자</p>
<p>TCP 커넥션 종료는 클라이언트와 서버 어느 쪽에서도 할 수 있기 때문에, 클라이언트와 서버로 나누지 않고 active close와 passive close로 나눈다. 여기서 말하는 Active close와 Passive close는 다음과 같다.</p>
<ul>
<li>Active close: TCP 연결 해제 요청한 쪽, 그러니까 트래픽을 전송(request)하고 정상적으로 전송 되었으면 연결을 끊는 쪽</li>
<li>Passive close: TCP 연결 해제 요청을 받은 쪽, 트래픽을 받는 쪽(api 서버라고 치면 클라이언트에게 응답을 주는 쪽)</li>
</ul>
<p><img src="/assets/img/tcp/tcp_close.gif" alt="tcp close" /></p>
<p>이 포스팅에서는 Client가 Active Close, Server가 Passive Close라고 생각한다.</p>
<h3 id="active-close">Active close</h3>
<ol>
<li>클라이언트는 FIN을 전송하고 FIN-WAIT-1 상태로 전환</li>
<li>클라이언트는 FIN에 대한 ACK를 수신하고 FIN-WAIT-2 상태로 전환</li>
<li>클라이언트는 FIN을 수신하면 ACK를 전송하고 TIME-WAIT 상태로 전환</li>
<li>TIME-WAIT 상태로 2MSL 동안 남아있는다.</li>
<li>타이머가 만료되면 클라이언트는 CLOSED 상태가 된다.</li>
</ol>
<p>FIN-WAIT-2 상태에서는 일정시간이 지나면 TIME-WAIT로 전환된다.</p>
<h3 id="passive-close">Passive close</h3>
<ol>
<li>서버는 FIN을 수신하고 ACK를 보낸다. CLOSE-WAIT 상태로 전환</li>
<li>프로세스로부터 passive close 명령을 받으면 서버도 FIN을 전송한다. LAST-ACK 상태로 전환</li>
<li>LAST-ACK 상태로 있다가 클라이언트로부터 온 마지막 ACK를 수신하면 CLOSED 상태가 된다.</li>
</ol>
<p>아래 그림을 보면 connection open 부터 close 까지의 TCP 상태에 대해 알 수 있다.</p>
<p><img src="/assets/img/tcp/tcp.png" alt="tcp state" /></p>
<p>위 그림에서 server 쪽에서 passive close를 받고(또는 받지 못하거나) fin을 정상적으로 보내지 못하면 Server쪽은 영원히 CLOSE_WAIT 상태로 기다리게 된다.(Client의 FIN-WAIT-2는 일정 시간이 지나면 TIME WAIT로 바뀐다)</p>
<h2 id="close-wait">CLOSE WAIT</h2>
<p>FIN-WAIT는 일정 시간이 지나면 TIME-WAIT 상태로 전환 되고, TIME-WAIT는 재 사용이 가능한 상태지만 CLOSE WAIT는 포트를 잡고 있는 프로세스의 종료, 네트워크 재시작 외에는 제거할 방법이 없다.(그래서 어플리케이션의 <strong>정상 종료</strong>가 필요하다.)</p>
<p>나도 어떤 툴(iperf2 server를 daemon으로 실행)을 사용했을 때 트래픽을 받는 쪽에서 CLOSE_WAIT 상태로 계속해서 포트가 잡혀있는 상황이 발생했고, 이 CLOSE_WAIT 상태가 계속 쌓인다면 특정 개수 이상으로 쌓이면 서버가 정상적으로 동작하지 못할 것으로 보인다. 서버쪽에서 FIN을 수신하고 ACK를 보냈지만, 서버 프로세스로부터 passive close 명령을 받지 못해서 FIN을 전송하지 못하고 CLOSE_WAIT 상태로 유지되었다. CLOSE_WAIT 상태는 위에서 말했듯 프로세스에서 정상 종료가 되지 않으면, timeout도 없어서 다른 상태로 변경할 수도 없는 상태이기 때문에 이 프로세스의 종료 또는 네트워크를 재시작하는 것 아니면 제거가 불가능하다. 나는 iperf의 버전을 3.~ 으로 올려서 해결했다.</p>
<h2 id="time-wait">TIME WAIT</h2>
<p>FIN WAIT 상태는 일정 시간이 지나면 TIME WAIT로 바뀌고, TIME_WAIT 상태는 2MSL동안 유지되고 2MSL 이 지나면 CLOSED 상태로 되고 커넥션이 닫힌다.</p>
<p>Active Close 쪽의 TIME WAIT는 2MSL(2분 정도)의 시간이 지나면 CLOSED 되기 때문에, 영원히 port를 잡고있는 사태는 없을 것이다.</p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&mallGb=KOR&barcode=9788960552890&orderClick=LEa&Kc=">데이터 통신과 네트워크</a> - Chapter 24. 전송층 프로토콜</li>
<li><a href="https://tech.kakao.com/2016/04/21/closewait-timewait/">카카오 기술 블로그</a></li>
</ul>Martinoeeen3@gmail.com트래픽을 만들어내는 어떤 툴을 사용하다가 CLOSE_WAIT 상태로 계속 유지되는 버그를 마주쳤다. 그런 의미에서 TCP 상태에 대해서 공부하고 정리한다.31주차 회고2020-08-02T13:00:59+00:002020-08-02T13:00:59+00:00https://smjeon.dev/retrospective/weekly-WW31<p>31주차 회고</p>
<h2 id="이번-주-ww31">이번 주 (WW31)</h2>
<p>최근들어 회고를 거의 못했다. 매주 썼을 때, 거의 반성문만 작성하는 것 같아서 그만 쓰게 된 것 같다. 거의 두 달만에 회고를 작성해본다. 아무래도 항상 부족한 점만 생각이 나니 아쉬운 점부터 시작한다.</p>
<h2 id="아쉬운-점">아쉬운 점</h2>
<ol>
<li>블로그 포스팅을 못했다.</li>
<li>TIL 정리를 못했다.</li>
<li>개인 공부를 못했다.</li>
<li>1일 1커밋 루틴이 깨졌다.</li>
<li>코드만 짜는 코드몽키가 되어가는 것 같다.</li>
</ol>
<h3 id="블로그-포스팅">블로그 포스팅</h3>
<p>블로그 포스팅 해야겠다고 생각한 주제들은 많은데, 실제로 포스팅을 못했다. 포스팅은 좀 더 신중하고, 정확한 정보를 전달해야한다는 생각과 시간이 부족하다는 핑계가 합쳐져서 최근들어 거의 포스팅을 못한 것 같다. 여전히 정확한 정보를 전달해야 한다는 생각은 변함 없지만 시간이 부족하다는 핑계를 줄이고 주말을 이용해 포스팅할 수 있도록 해봐야겠다.</p>
<h3 id="til">TIL</h3>
<p>TIL은 실제로 하루동안 배우고 느낀점들은 많았지만, 실제로 Github에 공유하고 싶지만 공유할 수 없는 내용들이 많았다. 거의 경험을 기반으로 배운 내용들인데, 그 경험을 그대로 녹여내기에는 보안상 불가능하다고 생각했다. 그러나 이것도 약간 비틀어서 공유할 수 있는 내용으로 바꾸어서 경험을 다시 녹여낼 수 있지 않을까 생각을 했다. 또한 기술적인 내용을 간단하게 소개하는 형식도 괜찮을 것 같다. 출, 퇴근 시간에 정보를 모아서 TIL에 정리하는 방식도 괜찮은 것 같다.</p>
<h3 id="개인-공부">개인 공부</h3>
<p>개인 공부에 관한 것은 하고 싶은 공부는 많은데 그 쪽에 쏟을 시간이 부족했다. DDD, Kotlin, JPA 등 공부하고 싶은 내용은 많이 있다. 이 부분은 확실하게 딱 시간을 정해놓고(예를 들어 토요일 2시부터 5시까지 라던가) 하지 않으면 시간을 할애하기 어려울 것 같다. 이 부분은 좀 더 생각해보고 할당해야 할 것 같다.</p>
<h3 id="1일-1커밋">1일 1커밋</h3>
<p>7월 말쯤 까지는 1일 1커밋을 유지했었다. 그러나 매일 내 개인 github id로 커밋한 내용이 없었고, 억지로 하루에 남은 2~3시간 정도로 커밋을 짜내는 게 오히려 손해라고 생각하게 되었다. 1일 1커밋을 이루기 위해서 커밋을 억지로(?) 짜내게 된다는 생각이 들었고 주객전도가 된 느낌이었다. 조금 더 깊은 생각을 하고 코드를 짜야하는데, 커밋을 만들어내기 위해서 생각이 좀 더 얕아지고 더 좋은 설계를 생각해낼 시간을 짧게 만드는 것 같아서 포기하게 되었다. 1일 1커밋이 끊긴 이유는 TIL 작성에 실패한 것도 이유인데, TIL을 매일 작성했더라면 이것이 1일 1커밋의 재료가 되었을 것인데, 위에서 말했듯 TIL을 매일 작성할 수 없다고 생각했었다.</p>
<h3 id="생각-없는-코딩">생각 없는 코딩</h3>
<p>요즘은 이렇게 코드 짜면 안되는데.. 하면서 일단 돌아가는 코드를 짜고 있는 것 같다. 이렇게 짜면 내가 한 3일 있다가 읽어도 이해하는데 시간이 걸릴 것 같은데, 다른 사람이 읽으면 얼마나 난해할까 하는 생각이 든다. 그래서 자꾸 시간이 날때마다(시간을 내서) 최대한 가독성 높고 이해할 수 있도록 리팩토링을 하고 있다. 또한 테스트코드도 부족해서, 내가 만들어낸 코드가 테스트 코드에서 확인할 수 없는 코드가 많다. 전체적인 구조에서 여러 개의 클래스들이 의존성이 엮여있어 단일 클래스의 로직만을 테스트하기 쉽지 않고, Dto들의 크기가 엄청나게 커져서 이를 mocking하는 것도 힘들어진 경우가 많은 것 같다. 포비에게 배우면서 나는 테스트 코드 작성 열심히하고, 클린코드를 짜내겠다 라고 다짐했으나 실제로 요즘에는 이루고 있지 못한 것 같다. 시간이 날 때마다 리팩토링과 더 가독성이 있는 코드를 만들어내기 위한 생각을 해야겠다는 해결법 밖에는 없는 것 같다. 머리 속에서 지워지면 다시 생각하는 것이 쉽지는 않을 것 같다.</p>
<h2 id="좋았던-점">좋았던 점</h2>
<p>새로운 것을 하면서 재미가 있었다. 거의 모든 것이 새로운 내용이고 배워가고 있기 때문에 재미있었다. 모르는 내용도 고민해보고, 어떤 방식으로 풀어나갈까를 생각해보는 것도 오랜만에 겪은 것 같아 기분이 좋았다. 전문적으로 무엇인가를 하고 있지는 않지만 좀 더 깊이있게 이해를 하고 싶은 욕심도 생긴다. 최근들어 한번도 사용해보지 않은 기술을 많이 사용해봤다. 근데 사용만 해봤지, 실제로 어떻게 돌아가는지? 내부에는 어떤 로직들이 있는지? 전체적인 흐름이 어떻게 되는지는 아직 이해하지 못했다. 똑같이 다시 해봐라 하면 지금 했던 내용을 복붙하면서 해결하겠지만, 약간만 응용해서 바꿔서 구현해보라고 하면 못할 것 같다. 최근 배웠던 기술에 대해 좀 더 깊은 공부를 하고 싶다라는 계기를 얻었다.</p>
<p>또 빠르게 해결할 수 있을 것이라고 생각했으나 의외의 장벽에 부딪혀 꽤나 시간을 오래 잡아먹은 일들도 많이 있다. 이런 일들을 겪으면서 시간 분배를 내 생각대로, 너무 긍정적으로만 하면 안된다는 것도 배우게 되었다. 협업을 하면서 본인의 의견을 확실하게 표현하고 현재 방식에서 변화를 주기 위해서는 현재 방식에는 어떤 문제가 있고, 이를 해결하기 위한 새로운 방법으로는 어떤 것이 있고 어떤 장점이 있는 지 이야기 해야 좋다라는 것도 느꼈다. 또한 협업하면서 업무 분배를 하더라도 다른 팀원들의 업무에 대해서도 충분히 파악하고 어떤 로직, 어떤 생각으로 구현하게 되었는지를 이해하는 것도 팀원들과 소통하는데 큰 도움이 된다는 것도 느꼈다.</p>
<h2 id="다음주-목표-ww32">다음주 목표 (WW32)</h2>
<p>일단 목표는 이룰 수 있을 정도로 세워야하기 때문에 작은 목표를 세운다.</p>
<ol>
<li>TIL 3일 이상 작성</li>
<li>주말을 이용해 블로그 포스팅 (평일에 자료를 모으고, 토요일 1시부터 작성)</li>
<li>코드 리뷰 들어온 것 다시 해주기</li>
<li>회고 템플릿을 다시 생각해보고 변경할 것</li>
<li>일요일에는 DDD Start 읽기</li>
<li>이번 주에 아쉬웠던 점들을 얼마나 개선했는지 살펴보자</li>
</ol>Martinoeeen3@gmail.com31주차 회고ProGit 정리 - 데이터 복구2020-07-14T13:00:00+00:002020-07-14T13:00:00+00:00https://smjeon.dev/git/git-recovery<p>바쁘신 분은 맨 밑의 요약 해둔 과정만 읽고 따라하시면 됩니다!</p>
<p>실제로 Git을 사용하다보면 커밋을 잃어버리는 일이 종종 발생한다. 작업 중이던 브랜치를 잠깐 develop branch에 갔다가 지워버린다거나, Hard Reset을 해버렸다거나 하면 종종 발생한다. 이럴 경우 어떻게 커밋을 다시 찾을 수 있을까?</p>
<h2 id="잃어버린-커밋-복구하기reflog">잃어버린 커밋 복구하기(reflog)</h2>
<p>master 브랜치에서 hard reset 한 경우 잃어버린 커밋을 복구해보자.</p>
<p><img src="/assets/img/git_recovery/git-log-before-reset.png" alt="git log before reset" /></p>
<p>일단 위와 같은 커밋로그를 만들어두었다. 이제 여기서 세번째 커밋으로 hard reset 해보자.(<code class="language-plaintext highlighter-rouge">git reset --hard fa0e799ce14bfb4d3719af3749fd665057cc35eb</code>)</p>
<p><img src="/assets/img/git_recovery/git-log-after-reset.png" alt="git log after reset" /></p>
<p>이제 위에 두 개의 커밋은 해시값을 기억하고 있지 않는 이상 잃어버렸다고 할 수 있다. 보통은 <code class="language-plaintext highlighter-rouge">git reflog</code> 명령을 사용하는게 좋다. HEAD가 가리키는 커밋이 바뀔 때마다 Git은 그 커밋이 무엇인지 기록한다. 일단 그럼 <code class="language-plaintext highlighter-rouge">git reflog</code>를 실행해보자.</p>
<p><img src="/assets/img/git_recovery/git-reflog.png" alt="git reflog" /></p>
<p>이걸 좀 더 자세히 보려면 <code class="language-plaintext highlighter-rouge">git log -g</code> 명령을 사용하면 된다.</p>
<p><img src="/assets/img/git_recovery/git-log-g.png" alt="git log -g" /></p>
<p>여기서 보면 두 번째 커밋으로 복구했으면 좋겠다는 것을 알 수 있으므로, 그 커밋을 가리키는 브랜치를 만들어서 복구하면 된다.(<code class="language-plaintext highlighter-rouge">git branch recover f1bb5bee31cfba88638358572ce24acb2ad2cd9d</code>)</p>
<h2 id="좀-더-심각한-상황">좀 더 심각한 상황</h2>
<p>이제 recover 브랜치에 가보면 총 5개의 커밋을 모두 확인할 수 있게 되었다. 하지만 잃어버린 커밋을 reflog에서도 찾을 수 없는 경우가 있을 수 있다. 이 상황도 재연해보자.</p>
<p>일단 recover 브랜치를 지우고, reflog에서 지우기 위해서 .git/logs/ 디렉토리를 지운다.(reflog는 .git/logs/ 에 존재한다.)</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git branch <span class="nt">-D</span> recover <span class="c"># master 브랜치나, 다른 브랜치에서</span>
<span class="nb">rm</span> <span class="nt">-rf</span> .git/logs/
</code></pre></div></div>
<p>이 명령 이후 <code class="language-plaintext highlighter-rouge">git reflog</code> 명령을 수행해보면 reflog가 텅 비어있음을 볼 수 있다.</p>
<p>이제 어떻게 복구할 수 있을까? <code class="language-plaintext highlighter-rouge">git fsck</code> 명령으로 데이터베이스의 Integrity를 검사할 수 있다. 일단 해보자(<code class="language-plaintext highlighter-rouge">git fsck --full</code>)</p>
<p><img src="/assets/img/git_recovery/git-fsck.png" alt="git fsck" /></p>
<p>여기서 보이는 dangling commit이 바로 잃어버린 커밋이므로 아까와 동일하게 이 커밋을 가리키는 브랜치를 만들어서 복구하면 된다.</p>
<h2 id="네-줄-요약">네 줄 요약</h2>
<ol>
<li>일단 <code class="language-plaintext highlighter-rouge">git reflog</code>를 확인하고 여기서 내가 복구하고 싶은 커밋을 찾아본다.</li>
<li>reflog에 없을 경우, <code class="language-plaintext highlighter-rouge">git fsck --full</code>을 통해 dangling commit 을 찾아본다.</li>
<li>찾아낸 commit의 hash값을 이용하여 <code class="language-plaintext highlighter-rouge">git branch 브랜치명 해시값</code> 명령을 통해 잃어버린 커밋으로 복구한다.</li>
<li><code class="language-plaintext highlighter-rouge">git checkout 브랜치명</code>으로 해당 브랜치로 작업을 계속한다.</li>
</ol>
<h2 id="참고자료">참고자료</h2>
<ul>
<li>ProGit - 10. Git 개체</li>
</ul>Martinoeeen3@gmail.com바쁘신 분은 맨 밑의 요약 해둔 과정만 읽고 따라하시면 됩니다!ProGit 정리 - Git의 내부2020-07-13T13:00:00+00:002020-07-13T13:00:00+00:00https://smjeon.dev/git/git-object<p>Git은 Content-addressable 파일 시스템이다. Git의 핵심은 Key-Value 데이터 저장소라는 것이다.</p>
<p>일단 <code class="language-plaintext highlighter-rouge">git init</code> 명령을 해보자. 디렉토리에 .git 디렉토리가 생긴다. 그 내부는 다음과 같다.</p>
<p><img src="/assets/img/git_object/git-init.png" alt="git init" /></p>
<h2 id="blob-개체">Blob 개체</h2>
<p>다음 명령으로 Git에 텍스트 파일을 저장해본다. <code class="language-plaintext highlighter-rouge">echo 'test' | git hash-object -w --stdin</code></p>
<p><img src="/assets/img/git_object/hash-object.png" alt="hash object" /></p>
<p>이제 <code class="language-plaintext highlighter-rouge">.git/objects</code> 디렉토리 아래에 어떤 파일이 생겼는지 살펴보면 위에서 나왔던 체크섬의 값을 가진 파일이 생긴 것을 볼 수 있다.</p>
<p><img src="/assets/img/git_object/find.png" alt="find" /></p>
<p><code class="language-plaintext highlighter-rouge">git cat-file</code> 명령으로 위에서 나온 해시값으로 파일의 내용을 읽을 수 있다. 위에서 나온 값으로 예시를 들면 <code class="language-plaintext highlighter-rouge">git cat-file -p 9daeafb9864cf43055ae93beb0afd6c7d144bfa4</code> 라고 명령어를 치면 test 라는 내용이 나올 것이다.(hash값을 몇 자만 쳐도 된다.)</p>
<p>Git이 파일 버전을 관리하는 방식을 이해하기 위해 다음과 같은 상황을 만든다.</p>
<ol>
<li>새로운 파일(test.txt)을 하나 만든다.</li>
<li>Git repository에 저장</li>
<li>해당 파일을 수정 후 git repository에 저장</li>
<li>파일 내용을 첫 번째 버전으로 되돌린다.</li>
<li>두 번째 버전을 다시 적용한다.</li>
</ol>
<p>위와 같은 과정을 순서대로 진행해보면 아래와 같다.</p>
<p><img src="/assets/img/git_object/cat-file.png" alt="cat file" /></p>
<p>이런 종류의 개체를 Blob 개체라고 부른다. <code class="language-plaintext highlighter-rouge">cat-file -t</code> 명령으로 무슨 객체인지 확인할 수 있다.</p>
<h2 id="tree-개체">Tree 개체</h2>
<p>Tree 개체에는 파일 이름을 저장한다. 파일 여러 개를 한꺼번에 저장할 수도 있다. 다른 프로젝트에서 <code class="language-plaintext highlighter-rouge">git cat-file -p master^{tree}</code> 명령을 쳐보자.</p>
<p><img src="/assets/img/git_object/cat-file-tree.png" alt="cat file" /></p>
<p>commerce project에서 해봤다. 여기서 gradle, src는 blob이 아니고 또 다른 tree 개체이다.</p>
<p>그러면 이제 직접 Tree 개체를 만들어보자. Git은 Staging Area의 상태대로 Tree 개체를 만들고 기록하기 때문에, Tree 개체를 만들기 위해서는 먼저 Staging Area에 파일을 추가하여 Index를 만들어야 한다.</p>
<p><code class="language-plaintext highlighter-rouge">git update-index --add --cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 test.txt</code>의 명령을 쳐본다. 여기서 100644는 보통의 파일을 나타낸다.(실행파일은 100755, 심볼릭 링크는 120000이다.)</p>
<p>Staging Area를 Tree 개체로 저장할 때는 write-tree 명령을 사용한다.</p>
<p><img src="/assets/img/git_object/write-tree.png" alt="write tree" /></p>
<p>여기서 새로운 파일도 추가하고 새 버전의 test.txt 파일도 Staging Area에 추가하고 새로운 Tree 개체를 만든다.</p>
<p><img src="/assets/img/git_object/new-tree.png" alt="new tree" /></p>
<p>이걸 보면 test.txt는 2번째 파일의 해시값인 1f7a7a1인 것을 알 수 있다. 그럼 이제 처음에 만든 Tree 개체를 하위 디렉토리로 만들어보자.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git read-tree <span class="nt">--prefix</span><span class="o">=</span>bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
git write-tree
<span class="c"># hash 값이 나옴</span>
git cat-file <span class="nt">-p</span> 해시값
</code></pre></div></div>
<p><img src="/assets/img/git_object/read-tree.png" alt="read tree" /></p>
<p>–prefix 옵션을 주면 Tree 개체를 하위 디렉토리로 추가할 수 있다. 이 구조를 보면 다음과 같은 구조로 되어 있을 것이다.</p>
<p><img src="/assets/img/git_object/tree-structure.png" alt="tree structure" /></p>
<h2 id="commit-개체">Commit 개체</h2>
<p>위에서 Tree 개체와 Blob 개체들을 만들어봤다. 근데 해시값으로 불러와야하고, 누가 언제 이 스냅샷을 만들었는지에 대한 정보가 전혀 없다. 이런 정보는 커밋 개체에 기록된다. 이런 커밋 개체는 commit-tree 로 만들 수 있다. 우리가 만들었던 첫 번째 Tree개체로 커밋 개체를 만들어보자.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s1">'first commit'</span> | git commit-tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
</code></pre></div></div>
<p><img src="/assets/img/git_object/commit-tree.png" alt="commit tree" /></p>
<p>커밋 개체는 단순하다. 해당 스냅샷에서 최상단 Tree를 하나 가리킨다. 그리고 user.name 과 user.email 설정에서 가져온 Author/Committer 정보, 시간, 그리고 커밋 메시지가 들어간다. 이제 커밋 개체를 두개만 더 만들어보자. 각 커밋 개체는 이전 개체를 가리키도록 한다.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s1">'second commit'</span> | git commit-tree 4d74ff21dadab0bc77516b42884fd9fdfd25a2cc <span class="nt">-p</span> b70785c0a00e9b70d102f787cb2ab57fbb8182de
<span class="nb">echo</span> <span class="s1">'third commit'</span> | git commit-tree 8357c9d0649c103bc8490e183fa37055127cf867 <span class="nt">-p</span> 01f6cfeaf512131559d2b59d4fa40d17d1964a82
</code></pre></div></div>
<p><img src="/assets/img/git_object/commit-tree-2.png" alt="commit tree 2" /></p>
<p>이렇게 만들고 나면 이제 Git history를 만든 것이다. 마지막 커밋 개체의 해시값으로 git log를 실행하면 다음처럼 출력된다.</p>
<p><img src="/assets/img/git_object/git-log.png" alt="git log" /></p>
<p>우리가 단순히 지금까지 git add, git commit으로 해왔던 내용들을 저수준 명령어로 해낸 것이다. Git 개체를 조금 만들어봤으니 이제 .git/objects 디렉토리를 살펴보면 다음처럼 되어있다.</p>
<p><img src="/assets/img/git_object/git-object.png" alt="git object" /></p>
<h2 id="git-refs">Git Refs</h2>
<p>위에서처럼 <code class="language-plaintext highlighter-rouge">git log --stat fa0e799ce14bfb4d3719af3749fd665057cc35eb</code> 라고 실행하면 전체 히스토리를 볼 수 있지만, 결국에는 해시값을 계속 기억하고 있어야한다는 단점이 있다. 그래서 우리는 보통 HEAD라거나 master라거나 하는 외우기 쉬운 이름으로 된 포인터를 사용한다.</p>
<p>Git에서는 이런 것을 Refs라고 부른다. 이는 .git/refs 디렉토리에 있다.</p>
<p>지금은 refs에 아무것도 없고 기본 디렉토리만 있을 것이다. 이것을 이용해서 이제 쉬운 이름으로 커밋을 조회 해보자.</p>
<p>아래 사진처럼 refs를 생성하고 이를 이용해보자.</p>
<p><img src="/assets/img/git_object/update-refs-master.png" alt="update refs" /></p>
<p><img src="/assets/img/git_object/git-log-master.png" alt="git log" /></p>
<p>이렇게 직접 refs 파일을 고칠 수도 있고, update-ref 명령을 이용할수도 있다. (<code class="language-plaintext highlighter-rouge">git update-ref refs/heads/master fa0e799ce14bfb4d3719af3749fd665057cc35eb</code>)</p>
<p>Git 브랜치의 역할이 바로 이것이다. 이를 이용해서 두 번째 커밋을 가리키는 브랜치를 만들어보면 다음처럼 할 수 있다.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git update-ref refs/heads/test 01f6cfeaf512131559d2b59d4fa40d17d1964a82
git log <span class="nt">--pretty</span><span class="o">=</span>oneline <span class="nb">test</span>
</code></pre></div></div>
<p><img src="/assets/img/git_object/git-log-test.png" alt="git log test" /></p>
<p><code class="language-plaintext highlighter-rouge">git branch 브랜치이름</code> 명령을 실행하면 내부적으로 Git은 update-ref 명령을 실행한다. 입력받은 브랜치 이름과 현 브랜치의 마지막 커밋의 해시값을 이용해 update-ref 명령을 실행한다.</p>
<h3 id="head">HEAD</h3>
<p><code class="language-plaintext highlighter-rouge">git branch 브랜치이름</code> 명령에서 Git은 어떻게 현 브랜치의 마지막 커밋의 해시값을 알아낼까? HEAD 파일은 현 브랜치를 가리키는 Symbolic Refs다. 실제로 <code class="language-plaintext highlighter-rouge">cat .git/HEAD</code>를 실행해보면 어떤 ref를 가리키고 있는지 알 수 있다.</p>
<p><code class="language-plaintext highlighter-rouge">git checkout test</code>명령으로 현 브랜치를 바꾸고 다시 실행해보면 HEAD가 바뀌어있는 것을 확인할 수 있다.</p>
<p><img src="/assets/img/git_object/after-checkout.png" alt="head" /></p>
<p>그리고 git commit을 실행하면 커밋 개체가 만들어지고, 현재 HEAD가 가리키는 커밋의 해시값이 새로운 커밋 개체의 부모로 사용된다.</p>
<p>이 .git/HEAD 파일도 직접 수정할 수 있지만, symbolic-ref 명령으로 더 안전하게 사용할 수 있다.</p>
<p><code class="language-plaintext highlighter-rouge">git symbolic-ref HEAD refs/heads/test</code> 명령으로 위에서 checkout 한 것처럼 할 수 있다.</p>
<p><img src="/assets/img/git_object/symbolic-ref.png" alt="symbolic ref" /></p>
<h3 id="리모트">리모트</h3>
<p>리모트 Refs도 있다. 리모트를 추가하고 push 하면 각 브랜치마다 push 한 마지막 커밋이 무엇인지 refs/remotes 디렉토리에 저장한다. 예를 들어 origin 리모트를 추가하고 master 브랜치를 push 해보자.</p>
<p><img src="/assets/img/git_object/remote-ref.png" alt="symbolic ref" /></p>
<p>리모트 refs는 checkout 할 수 없고, 읽기 용도로만 쓸 수 있는 브랜치다. 리모트 refs는 서버의 브랜치가 가리키는 커밋이 무엇인지 적어둔 북마크라고 보면 된다.</p>
<h2 id="태그">태그</h2>
<p>태그 개체는 커밋 개체와 비슷하다. 커밋 개체처럼 누가, 언제 태그를 달았고 태그 메세지는 무엇이고 어떤 커밋을 가리키는지에 대한 정보가 들어있다. 태그 개체는 Tree개체가 아니라 Commit 개체를 가리키는 것이 차이점이다.</p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li>ProGit - 10. Git 개체</li>
</ul>Martinoeeen3@gmail.comGit은 Content-addressable 파일 시스템이다. Git의 핵심은 Key-Value 데이터 저장소라는 것이다.Spring Security Test2020-07-03T13:30:59+00:002020-07-03T13:30:59+00:00https://smjeon.dev/etc/with-mock-user<p>Spring Security를 사용하면 권한을 이용한 테스트를 할 때가 있다. @WithMockUser 어노테이션을 사용하고 싶은데 커스텀 Authentication 객체를 사용할 때, 이를 그대로 사용할 수는 없다. 이걸 어떻게 쓰려고 했는지 경험을 기록하려고 한다.</p>
<h2 id="spring-security-docs">Spring Security Docs</h2>
<p>일단 스프링 시큐리티 공식 문서에서 19. Testing 부분에서 내가 참고했던 부분을 번역해본다.</p>
<h3 id="withmockuser">@WithMockUser</h3>
<blockquote>
<p>문제는 “특정 사용자로서 테스트를 어떻게 할까?” 이다. 답은 <code class="language-plaintext highlighter-rouge">@WithMockUser</code>를 사용하는 것이다. 아래 테스트는 username “user”와 password “password”인 “ROLE_USER” 권한으로 테스트를 돌린다.</p>
</blockquote>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="nd">@WithMockUser</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">getMessageWithMockUser</span><span class="o">()</span> <span class="o">{</span>
<span class="nc">String</span> <span class="n">message</span> <span class="o">=</span> <span class="n">messageService</span><span class="o">.</span><span class="na">getMessage</span><span class="o">();</span>
<span class="o">...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>다음과 같은 내용이 뒷받침된다.</p>
<ol>
<li>“user”이름을 가진 유저는 없어도 된다.(mocking하기 때문에)</li>
<li>UsernamePasswordAuthenticationToken 타입의 Authentication 객체가 SecurityContext 내에 생긴다.</li>
<li>Authentication 객체 내의 principal은 Spring Security의 User 객체이다.</li>
<li>User는 username은 “user”, password는 “password”, 권한은 “ROLE_USER”이다.</li>
</ol>
<p>이 문서를 읽어보면 @WithMockUser는 Spring Security의 User 객체를 사용하고, 기본 Authentication 객체를 이 User 객체로 채운다. 그리고 SecurityContext내에 Authentication Setting한다.</p>
<h3 id="withanonymoususer">@WithAnonymousUser</h3>
<blockquote>
<p>@WithAnonymousUser를 사용하는 것은 anonymous 사용자로 테스트를 돌리는 것이다. 이 방식은 대부분은 특정 유저로 테스트하고, 일부분의 테스트만 anonymous로 하려고 할 때 특히 유용하다. 예를 들어 아래 나오는 내용은 withMockUser1과 withMockUser2 메서드는 @WithMockUser로 돌리고, anonymous 메서드는 anonymous 사용자로 테스트를 돌린다.</p>
</blockquote>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@RunWith</span><span class="o">(</span><span class="nc">SpringJUnit4ClassRunner</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="nd">@WithMockUser</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">WithUserClassLevelAuthenticationTests</span> <span class="o">{</span>
<span class="nd">@Test</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">withMockUser1</span><span class="o">()</span> <span class="o">{</span>
<span class="o">}</span>
<span class="nd">@Test</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">withMockUser2</span><span class="o">()</span> <span class="o">{</span>
<span class="o">}</span>
<span class="nd">@Test</span>
<span class="nd">@WithAnonymousUser</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">anonymous</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
<span class="c1">// override default to run as anonymous user</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>SecurityContext에 anonymous로 테스트하기 위해서는 @WithAnonymousUser를 사용하면 된다.</p>
<h3 id="withsecuritycontext">@WithSecurityContext</h3>
<p>우리는 @WithSecurityContext를 이용해서 우리가 원하는 SecurityContext를 만들 수 있는 커스텀 어노테이션을 만들 수 있다. 예를 들어 우리는 @WithMockCustomUser라는 어노테이션을 아래 나온 것처럼 만들 수 있다.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Retention</span><span class="o">(</span><span class="nc">RetentionPolicy</span><span class="o">.</span><span class="na">RUNTIME</span><span class="o">)</span>
<span class="nd">@WithSecurityContext</span><span class="o">(</span><span class="n">factory</span> <span class="o">=</span> <span class="nc">WithMockCustomUserSecurityContextFactory</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">public</span> <span class="nd">@interface</span> <span class="nc">WithMockCustomUser</span> <span class="o">{</span>
<span class="nc">String</span> <span class="nf">username</span><span class="o">()</span> <span class="k">default</span> <span class="s">"rob"</span><span class="o">;</span>
<span class="nc">String</span> <span class="nf">name</span><span class="o">()</span> <span class="k">default</span> <span class="s">"Rob Winch"</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>@WithMockCustomUser는 @WithSecurityContext 어노테이션과 함께 사용되었음을 볼 수 있다. 이는 Spring Security Test support에게 우리가 테스트에서 SecurityContext를 만들기 위해 신호를 주는 것이다. @WithSecurityContext 어노테이션은 @WithMockCustomUser에게 새로운 SecurityContext를 제공하기 위한 SecurityContextFactory를 특정하는 것이 필요하다. SecurityContextFactory를 아래처럼 구현하는 것을 볼 수 있다.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">WithMockCustomUserSecurityContextFactory</span>
<span class="kd">implements</span> <span class="nc">WithSecurityContextFactory</span><span class="o"><</span><span class="nc">WithMockCustomUser</span><span class="o">></span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">SecurityContext</span> <span class="nf">createSecurityContext</span><span class="o">(</span><span class="nc">WithMockCustomUser</span> <span class="n">customUser</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">SecurityContext</span> <span class="n">context</span> <span class="o">=</span> <span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">createEmptyContext</span><span class="o">();</span>
<span class="nc">CustomUserDetails</span> <span class="n">principal</span> <span class="o">=</span>
<span class="k">new</span> <span class="nf">CustomUserDetails</span><span class="o">(</span><span class="n">customUser</span><span class="o">.</span><span class="na">name</span><span class="o">(),</span> <span class="n">customUser</span><span class="o">.</span><span class="na">username</span><span class="o">());</span>
<span class="nc">Authentication</span> <span class="n">auth</span> <span class="o">=</span>
<span class="k">new</span> <span class="nf">UsernamePasswordAuthenticationToken</span><span class="o">(</span><span class="n">principal</span><span class="o">,</span> <span class="s">"password"</span><span class="o">,</span> <span class="n">principal</span><span class="o">.</span><span class="na">getAuthorities</span><span class="o">());</span>
<span class="n">context</span><span class="o">.</span><span class="na">setAuthentication</span><span class="o">(</span><span class="n">auth</span><span class="o">);</span>
<span class="k">return</span> <span class="n">context</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>이제 이 내용을 참고해서 내 @WithMockCustomUser 를 만들어보자.</p>
<h2 id="커스텀-withmockuser">커스텀 @WithMockUser</h2>
<p>일단 @WithMockUser 가 사용하는 WithMockUserSecurityContextFactory를 살펴보자.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="nc">WithMockUserSecurityContextFactory</span> <span class="kd">implements</span>
<span class="nc">WithSecurityContextFactory</span><span class="o"><</span><span class="nc">WithMockUser</span><span class="o">></span> <span class="o">{</span>
<span class="kd">public</span> <span class="nc">SecurityContext</span> <span class="nf">createSecurityContext</span><span class="o">(</span><span class="nc">WithMockUser</span> <span class="n">withUser</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">String</span> <span class="n">username</span> <span class="o">=</span> <span class="nc">StringUtils</span><span class="o">.</span><span class="na">hasLength</span><span class="o">(</span><span class="n">withUser</span><span class="o">.</span><span class="na">username</span><span class="o">())</span> <span class="o">?</span> <span class="n">withUser</span>
<span class="o">.</span><span class="na">username</span><span class="o">()</span> <span class="o">:</span> <span class="n">withUser</span><span class="o">.</span><span class="na">value</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">username</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="n">withUser</span>
<span class="o">+</span> <span class="s">" cannot have null username on both username and value properites"</span><span class="o">);</span>
<span class="o">}</span>
<span class="nc">List</span><span class="o"><</span><span class="nc">GrantedAuthority</span><span class="o">></span> <span class="n">grantedAuthorities</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o"><>();</span>
<span class="k">for</span> <span class="o">(</span><span class="nc">String</span> <span class="n">authority</span> <span class="o">:</span> <span class="n">withUser</span><span class="o">.</span><span class="na">authorities</span><span class="o">())</span> <span class="o">{</span>
<span class="n">grantedAuthorities</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="k">new</span> <span class="nc">SimpleGrantedAuthority</span><span class="o">(</span><span class="n">authority</span><span class="o">));</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="n">grantedAuthorities</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
<span class="k">for</span> <span class="o">(</span><span class="nc">String</span> <span class="n">role</span> <span class="o">:</span> <span class="n">withUser</span><span class="o">.</span><span class="na">roles</span><span class="o">())</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">role</span><span class="o">.</span><span class="na">startsWith</span><span class="o">(</span><span class="s">"ROLE_"</span><span class="o">))</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"roles cannot start with ROLE_ Got "</span>
<span class="o">+</span> <span class="n">role</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">grantedAuthorities</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="k">new</span> <span class="nc">SimpleGrantedAuthority</span><span class="o">(</span><span class="s">"ROLE_"</span> <span class="o">+</span> <span class="n">role</span><span class="o">));</span>
<span class="o">}</span>
<span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(!(</span><span class="n">withUser</span><span class="o">.</span><span class="na">roles</span><span class="o">().</span><span class="na">length</span> <span class="o">==</span> <span class="mi">1</span> <span class="o">&&</span> <span class="s">"USER"</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">withUser</span><span class="o">.</span><span class="na">roles</span><span class="o">()[</span><span class="mi">0</span><span class="o">])))</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalStateException</span><span class="o">(</span><span class="s">"You cannot define roles attribute "</span><span class="o">+</span> <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="n">withUser</span><span class="o">.</span><span class="na">roles</span><span class="o">())+</span><span class="s">" with authorities attribute "</span><span class="o">+</span> <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="n">withUser</span><span class="o">.</span><span class="na">authorities</span><span class="o">()));</span>
<span class="o">}</span>
<span class="nc">User</span> <span class="n">principal</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="n">username</span><span class="o">,</span> <span class="n">withUser</span><span class="o">.</span><span class="na">password</span><span class="o">(),</span> <span class="kc">true</span><span class="o">,</span> <span class="kc">true</span><span class="o">,</span> <span class="kc">true</span><span class="o">,</span> <span class="kc">true</span><span class="o">,</span>
<span class="n">grantedAuthorities</span><span class="o">);</span>
<span class="nc">Authentication</span> <span class="n">authentication</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">UsernamePasswordAuthenticationToken</span><span class="o">(</span>
<span class="n">principal</span><span class="o">,</span> <span class="n">principal</span><span class="o">.</span><span class="na">getPassword</span><span class="o">(),</span> <span class="n">principal</span><span class="o">.</span><span class="na">getAuthorities</span><span class="o">());</span>
<span class="nc">SecurityContext</span> <span class="n">context</span> <span class="o">=</span> <span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">createEmptyContext</span><span class="o">();</span>
<span class="n">context</span><span class="o">.</span><span class="na">setAuthentication</span><span class="o">(</span><span class="n">authentication</span><span class="o">);</span>
<span class="k">return</span> <span class="n">context</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>일단 들어온 username이 비어있는지 확인한다. Role에 <code class="language-plaintext highlighter-rouge">ROLE_</code> 이라는 prefix가 있는지 확인한다.(있으면 exception) 그 다음 Authority를 하나만 지정했으면서 그 권한을 User로 설정했다면 exception을 던진다.</p>
<p>그 다음으로 본격적인 과정이 있다. annotation 설정 값으로부터 들어온 값을 이용하여 User 객체(import org.springframework.security.core.userdetails.User;
)를 만든다. 그리고 UsernamePasswordAuthenticationToken(Authentication 객체)을 만들어 SecurityContext에 넣어준 후 그 SecurityContext를 return 한다.</p>
<p>그러면 이와 비슷한 과정으로 Custom annotation과 Factory를 만들어보면 아래와 같다. (import 부분도 포함했다.)</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// WithMockCustomUser.java</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.test.context.support.WithSecurityContext</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.lang.annotation.Retention</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.lang.annotation.RetentionPolicy</span><span class="o">;</span>
<span class="nd">@Retention</span><span class="o">(</span><span class="nc">RetentionPolicy</span><span class="o">.</span><span class="na">RUNTIME</span><span class="o">)</span>
<span class="nd">@WithSecurityContext</span><span class="o">(</span><span class="n">factory</span> <span class="o">=</span> <span class="nc">WithMockCustomUserSecurityContextFactory</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">public</span> <span class="nd">@interface</span> <span class="nc">WithMockCustomUser</span> <span class="o">{</span>
<span class="nc">String</span> <span class="nf">username</span><span class="o">()</span> <span class="k">default</span> <span class="s">"martin"</span><span class="o">;</span>
<span class="nc">UserRole</span> <span class="nf">role</span><span class="o">()</span> <span class="k">default</span> <span class="nc">UserRole</span><span class="o">.</span><span class="na">BUYER</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">// WithMockCustomUserSecurityContextFactory.java</span>
<span class="kn">import</span> <span class="nn">dev.smjeon.commerce.security.UserContext</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">dev.smjeon.commerce.security.token.PostAuthorizationToken</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.core.context.SecurityContext</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.core.context.SecurityContextHolder</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.test.context.support.WithSecurityContextFactory</span><span class="o">;</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">WithMockCustomUserSecurityContextFactory</span> <span class="kd">implements</span> <span class="nc">WithSecurityContextFactory</span><span class="o"><</span><span class="nc">WithMockCustomUser</span><span class="o">></span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">SecurityContext</span> <span class="nf">createSecurityContext</span><span class="o">(</span><span class="nc">WithMockCustomUser</span> <span class="n">customUser</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">SecurityContext</span> <span class="n">context</span> <span class="o">=</span> <span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">createEmptyContext</span><span class="o">();</span>
<span class="nc">UserContext</span> <span class="n">userContext</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">UserContext</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="s">"aabb"</span><span class="o">,</span> <span class="n">customUser</span><span class="o">.</span><span class="na">username</span><span class="o">(),</span> <span class="n">customUser</span><span class="o">.</span><span class="na">role</span><span class="o">());</span>
<span class="nc">PostAuthorizationToken</span> <span class="n">token</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">PostAuthorizationToken</span><span class="o">(</span><span class="n">userContext</span><span class="o">);</span>
<span class="n">context</span><span class="o">.</span><span class="na">setAuthentication</span><span class="o">(</span><span class="n">token</span><span class="o">);</span>
<span class="k">return</span> <span class="n">context</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>WithMockUserSecurityContextFactory에서 본 Exception 처리 부분은 제외하고 SecurityContext를 채우는 부분만 구현해서 만들었다. 프로젝트에서 로그인 했을 때 PostAuthorizationToken을 Authentication의 principal로 사용한다. 그래서 WithMockCustomUser annotation으로부터 가져올 수 있는 유저 이름과 권한을 이용해서 PostAuthorizationToken을 만들어서 SecurityContext에 채우고 그 SecurityContext를 return 하는 방식으로 구현했다.</p>
<p>이제 이 <code class="language-plaintext highlighter-rouge">@WithMockCustomUser</code>를 이용해서 테스트를 작성해보면 다음과 같다.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="nd">@WithAnonymousUser</span>
<span class="nd">@DisplayName</span><span class="o">(</span><span class="s">"권한 없이 모든 상품을 조회할 수 있습니다."</span><span class="o">)</span>
<span class="kt">void</span> <span class="nf">findAll</span><span class="o">()</span> <span class="o">{</span>
<span class="nc">List</span><span class="o"><</span><span class="nc">Product</span><span class="o">></span> <span class="n">products</span> <span class="o">=</span> <span class="nc">Collections</span><span class="o">.</span><span class="na">singletonList</span><span class="o">(</span><span class="n">product</span><span class="o">);</span>
<span class="n">given</span><span class="o">(</span><span class="n">productRepository</span><span class="o">.</span><span class="na">findAll</span><span class="o">()).</span><span class="na">willReturn</span><span class="o">(</span><span class="n">products</span><span class="o">);</span>
<span class="n">productService</span><span class="o">.</span><span class="na">findAll</span><span class="o">();</span>
<span class="n">verify</span><span class="o">(</span><span class="n">productRepository</span><span class="o">).</span><span class="na">findAll</span><span class="o">();</span>
<span class="o">}</span>
<span class="nd">@Test</span>
<span class="nd">@WithMockCustomUser</span><span class="o">(</span><span class="n">role</span> <span class="o">=</span> <span class="nc">UserRole</span><span class="o">.</span><span class="na">ADMIN</span><span class="o">)</span>
<span class="nd">@DisplayName</span><span class="o">(</span><span class="s">"관리자가 삭제요청을 하면 Blocked 상태로 변경됩니다."</span><span class="o">)</span>
<span class="kt">void</span> <span class="nf">deleteWithAdmin</span><span class="o">()</span> <span class="o">{</span>
<span class="n">given</span><span class="o">(</span><span class="n">productRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">anyLong</span><span class="o">())).</span><span class="na">willReturn</span><span class="o">(</span><span class="nc">Optional</span><span class="o">.</span><span class="na">ofNullable</span><span class="o">(</span><span class="n">product</span><span class="o">));</span>
<span class="n">given</span><span class="o">(</span><span class="n">userService</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">anyLong</span><span class="o">())).</span><span class="na">willReturn</span><span class="o">(</span><span class="n">admin</span><span class="o">);</span>
<span class="n">productService</span><span class="o">.</span><span class="na">delete</span><span class="o">(</span><span class="mi">1L</span><span class="o">);</span>
<span class="n">verify</span><span class="o">(</span><span class="n">productRepository</span><span class="o">).</span><span class="na">findById</span><span class="o">(</span><span class="mi">1L</span><span class="o">);</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="n">product</span><span class="o">.</span><span class="na">getStatus</span><span class="o">(),</span> <span class="nc">ProductStatus</span><span class="o">.</span><span class="na">BLOCKED</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">@WithAnonymousUser</code>는 기본 어노테이션으로 아무 권한이 없더라도 접근할 수 있는 로직에 대한 테스트이다. <code class="language-plaintext highlighter-rouge">@WithMockCustomUser(role = UserRole.ADMIN)</code>는 ADMIN 권한이 있어야만 접근할 수 있는 로직에 대한 테스트이다. 지금 구현은 ServiceTest지만 User를 Mocking해서 쓰는게 아니라 ADMIN권한으로 실제로 만들어서 테스트를 해서 원래 <code class="language-plaintext highlighter-rouge">@WithMockUser</code>의 원래 사용 목적과는 다를 수 있다. 하지만 <code class="language-plaintext highlighter-rouge">@WithMockCustomUser</code> 가 없으면 SecurityContext 내부 객체가 비어있어서 테스트가 실패할 것이다.</p>
<p>ProductService의 delete 메서드를 살펴보면 사실 User의 권한에 따라 product의 상태가 다르게 바뀌게 구현되어있다.(ADMIN이면 BLOCKED, 본인이 삭제하면 REMOVED)</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Transactional</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">delete</span><span class="o">(</span><span class="nc">Long</span> <span class="n">productId</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Product</span> <span class="n">product</span> <span class="o">=</span> <span class="n">productRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">productId</span><span class="o">).</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-></span> <span class="k">new</span> <span class="nc">NotFoundProductException</span><span class="o">(</span><span class="n">productId</span><span class="o">));</span>
<span class="nc">User</span> <span class="n">owner</span> <span class="o">=</span> <span class="n">getUserFromAuthentication</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">owner</span><span class="o">.</span><span class="na">isAdmin</span><span class="o">())</span> <span class="o">{</span>
<span class="n">product</span><span class="o">.</span><span class="na">block</span><span class="o">();</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="n">product</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="n">owner</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="nc">User</span> <span class="nf">getUserFromAuthentication</span><span class="o">()</span> <span class="o">{</span>
<span class="nc">Authentication</span> <span class="n">authentication</span> <span class="o">=</span> <span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">().</span><span class="na">getAuthentication</span><span class="o">();</span>
<span class="nc">UserContext</span> <span class="n">user</span> <span class="o">=</span> <span class="o">(</span><span class="nc">UserContext</span><span class="o">)</span> <span class="n">authentication</span><span class="o">.</span><span class="na">getPrincipal</span><span class="o">();</span>
<span class="k">return</span> <span class="n">userService</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
<span class="o">}</span>
</code></pre></div></div>
<p>지금 테스트 코드에서 <code class="language-plaintext highlighter-rouge">@WithMockCustomUser</code>에서 UserRole을 변경해도 상관없이 돌아간다.(SecurityContext의 Authentication만 들어있다면..) 이는 실제 <code class="language-plaintext highlighter-rouge">@WithMockUser</code>의 사용 목적과는 다른 것 같기 때문에, 변경이 필요할 것 같다.</p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://docs.spring.io/spring-security/site/docs/current/reference/html5/#test">Spring Security Docs - Testing</a></li>
</ul>Martinoeeen3@gmail.comSpring Security를 사용하면 권한을 이용한 테스트를 할 때가 있다. @WithMockUser 어노테이션을 사용하고 싶은데 커스텀 Authentication 객체를 사용할 때, 이를 그대로 사용할 수는 없다. 이걸 어떻게 쓰려고 했는지 경험을 기록하려고 한다.23주차 회고2020-06-07T13:00:59+00:002020-06-07T13:00:59+00:00https://smjeon.dev/retrospective/weekly-WW23<p>23주차 회고</p>
<h2 id="이번-주-ww23">이번 주 (WW23)</h2>
<h3 id="이번주-하기로-목표-했던-일">이번주 하기로 목표 했던 일</h3>
<ul class="task-list">
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Real MySQL 7장</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Spring Security 공식 문서 번역</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />상품 기능 완성</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />구매 로직 개발</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />ipv4 글 내용 보충 - 특수한 주소들</li>
</ul>
<h3 id="계획-하지-않았지만-했던-일">계획 하지 않았지만 했던 일</h3>
<ul>
<li>코딩테스트 보기</li>
<li>맥북 수리, 데스크탑에 개발환경 셋팅</li>
<li>Spring Security 기본 필터 포스팅</li>
</ul>
<h3 id="나에게-칭찬-해주고-싶은-것">나에게 칭찬 해주고 싶은 것</h3>
<ul>
<li>묵혀두었던 맥북 수리를 맡겨버렸다.</li>
</ul>
<h3 id="공부한-내용">공부한 내용</h3>
<ul>
<li>스프링 시큐리티 구조, 기본 필터들</li>
</ul>
<h3 id="아쉬운점">아쉬운점</h3>
<ul>
<li>맥북을 수리 맡겼다. 지난번에 빠진 커맨드키..</li>
<li>수리를 맡겨서 데스크탑에 개발환경 셋팅 하고 한다고 별로 개발을 못했다.<del>(핑계지만)</del></li>
<li><strong>목표를 이룰 수 있을 수준으로 잡자.</strong> 나를 너무 믿지말자….</li>
</ul>
<hr />
<h2 id="다음-주-ww24">다음 주 (WW24)</h2>
<h3 id="다음주-목표">다음주 목표</h3>
<p>이번주에 하기로 했던 것들 재탕</p>
<ul>
<li>상품 기능 완성</li>
<li>구매 로직 개발</li>
<li>Real MySQL 7장</li>
</ul>
<h3 id="가까운-미래의-목표-최대-2달-기한으로-시작까지">가까운 미래의 목표 (최대 2달 기한으로 시작까지)</h3>
<ul>
<li>java-wiki 다시 시작</li>
<li>JPA 독학</li>
<li><a href="https://academy.nomadcoders.co/p/cssnext-css-layout-masterclass">css 기초 공부</a></li>
</ul>Martinoeeen3@gmail.com23주차 회고Spring Security Default Filter2020-06-07T09:30:59+00:002020-06-07T09:30:59+00:00https://smjeon.dev/etc/spring-security-default-filter<p>Spring Security의 구조를 살펴보면, 사용자의 요청이 들어온 후 가장 먼저 처리되는 곳이 바로 Filter(FilterChain)이다.</p>
<p>Spring Boot 기반으로 Spring Security를 사용하다보면, 시큐리티에서 기본적으로 생성하는 filter 들이 있다. 그 filter에 대해 정리해보기로 한다. 전체 구조를 한번에 이해하고 사용할 수 있으면 좋겠지만, 그렇게 기억력이 좋지 않기 때문에 기억이 안날 때마다 참고하기 위해 글로 작성합니다.</p>
<p>그 전에 일단 스프링 시큐리티의 기본 구조에 대해서 알아보자.</p>
<h2 id="delegatingfilterproxy">DelegatingFilterProxy</h2>
<p>스프링은 DelegatingFilterProxy라는 필터 구현체를 제공하는데, 이는 Servlet Container의 생명 주기와 스프링의 ApplicationContext사이를 연결해주는 역할을 한다. 서블릿 컨테이너는 서블릿 표준에 따라 필터를 등록하기 때문에, 스프링에서 정의된 빈들을 인지하지 못한다. DelegatingFilterProxy는 표준 서블릿 컨테이너 메커니즘을 통해 필터를 등록하지만, 그 필터로 구현된 스프링 빈에게 모든 일을 위임한다.</p>
<p>아래 그림은 필터와 필터체인에 어떻게 적용하는지 나타내는 그림이다.(출처: <a href="https://docs.spring.io/spring-security/site/docs/current/reference/html5/#servlet-filters-review">Spring Security 공식문서</a>)</p>
<p><img src="/assets/img/spring_security_filter/delegatingfilterproxy.png" alt="DelegatingFilterProxy" /></p>
<p>DelegatingFilterProxy는 Bean Filter 0을 ApplicationContext에서 찾고, Bean Filter 0을 호출한다. DelegatingFilterProxy의 코드를 가져와보면 아래와 같다.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">doFilter</span><span class="o">(</span><span class="nc">ServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="nc">ServletResponse</span> <span class="n">response</span><span class="o">,</span> <span class="nc">FilterChain</span> <span class="n">filterChain</span><span class="o">)</span>
<span class="kd">throws</span> <span class="nc">ServletException</span><span class="o">,</span> <span class="nc">IOException</span> <span class="o">{</span>
<span class="c1">// Lazily initialize the delegate if necessary.</span>
<span class="nc">Filter</span> <span class="n">delegateToUse</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">delegate</span><span class="o">;</span>
<span class="k">if</span> <span class="o">(</span><span class="n">delegateToUse</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="kd">synchronized</span> <span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">delegateMonitor</span><span class="o">)</span> <span class="o">{</span>
<span class="n">delegateToUse</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">delegate</span><span class="o">;</span>
<span class="k">if</span> <span class="o">(</span><span class="n">delegateToUse</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">WebApplicationContext</span> <span class="n">wac</span> <span class="o">=</span> <span class="n">findWebApplicationContext</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">wac</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalStateException</span><span class="o">(</span><span class="s">"No WebApplicationContext found: "</span> <span class="o">+</span>
<span class="s">"no ContextLoaderListener or DispatcherServlet registered?"</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">delegateToUse</span> <span class="o">=</span> <span class="n">initDelegate</span><span class="o">(</span><span class="n">wac</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">this</span><span class="o">.</span><span class="na">delegate</span> <span class="o">=</span> <span class="n">delegateToUse</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c1">// Let the delegate perform the actual doFilter operation.</span>
<span class="n">invokeDelegate</span><span class="o">(</span><span class="n">delegateToUse</span><span class="o">,</span> <span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">,</span> <span class="n">filterChain</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>ApplicationContext를 찾고, <code class="language-plaintext highlighter-rouge">initDelegate(wac)</code>를 통해 위임할 필터를 찾는다. 그 코드도 보면 아래와 같다.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">protected</span> <span class="nc">Filter</span> <span class="nf">initDelegate</span><span class="o">(</span><span class="nc">WebApplicationContext</span> <span class="n">wac</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">ServletException</span> <span class="o">{</span>
<span class="nc">String</span> <span class="n">targetBeanName</span> <span class="o">=</span> <span class="n">getTargetBeanName</span><span class="o">();</span>
<span class="nc">Assert</span><span class="o">.</span><span class="na">state</span><span class="o">(</span><span class="n">targetBeanName</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">,</span> <span class="s">"No target bean name set"</span><span class="o">);</span>
<span class="nc">Filter</span> <span class="n">delegate</span> <span class="o">=</span> <span class="n">wac</span><span class="o">.</span><span class="na">getBean</span><span class="o">(</span><span class="n">targetBeanName</span><span class="o">,</span> <span class="nc">Filter</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">isTargetFilterLifecycle</span><span class="o">())</span> <span class="o">{</span>
<span class="n">delegate</span><span class="o">.</span><span class="na">init</span><span class="o">(</span><span class="n">getFilterConfig</span><span class="o">());</span>
<span class="o">}</span>
<span class="k">return</span> <span class="n">delegate</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>targetBeanName으로 된 Filter를 찾고 그 필터를 Delegate로 갖는다. 그리고 <code class="language-plaintext highlighter-rouge">invokeDelegate</code>를 통해 <code class="language-plaintext highlighter-rouge">delegate.doFilter</code>를 호출하여 해당 필터에 모든 일을 위임한다.</p>
<p>공식문서에 나온 DelegatingFilterProxy의 또 다른 이점은 Filter Bean의 인스턴스화를 지연시킬 수 있다는 것이다. 서블릿 컨테이너가 시작되기 전에 필터 인스턴스를 등록해야하기 때문에 중요하다. 그러나 스프링은 일반적으로 필터 인스턴스를 등록해야 할 때까지는 미리 등록 하지 않는 Spring Bean을 로드하기 위해 ContextLoaderListener를 사용한다.</p>
<h2 id="filterchainproxy">FilterChainProxy</h2>
<p>스프링 시큐리티의 서블릿 지원은 FilterChainProxy안에 있다. FilterChainProxy는 스프링 시큐리티에서 제공하는 특별한 필터로, SecurityFilterChain을 통해 많은 필터 인스턴스에 위임할 수 있게 한다. FilterChainProxy도 Spring Bean이기 때문에 위에서 말한 DelegatingFilterProxy로 포장되어 있다. SpringSecurityFilterChain 이름으로 생성되는 Bean으로 DelegatingFilterProxy로부터 요청을 위임받고 <strong>실제</strong> 보안 처리를 한다. 여기서 스프링 시큐리티가 생성하는 필터와 사용자가 설정 클래스를 통해 생성한 필터들의 체인을 순서대로 돈다.</p>
<h2 id="securityfilterchain">SecurityFilterChain</h2>
<p>위에서 본 FilterChainProxy에서 사용자의 요청에 맞는 필터들을 결정하기 위해 SecurityFilterChain은 사용된다.</p>
<p>이 SecurityFilterChain 내의 필터들은 일반적으로 bean이지만, DelegatingFilterProxy가 아닌 FilterChainProxy를 통해 등록된다. FilterChainProxy는 DelegatingFilterProxy나 Servlet Container를 통해 직접 등록하는 것보다 많은 이점이 있다.</p>
<ol>
<li>스프링 시큐리티의 모든 서블릿 지원을 위한 시작점을 제공한다. 그래서 시큐리티의 서블릿 관련 문제를 해결하려면 여기에 디버깅 포인트를 잡고 살펴보는 것이 좋다.</li>
<li>FilterChainProxy는 SpringSecurity 사용의 중심이기 때문에 옵션으로 보이지 않는 작업을 수행할 수 있다. 예를 들어 메모리 릭을 피하기 위해 SecurityContext를 지운다. 또는 스프링 시큐리티의 HttpFirewall을 적용해서 어떤 타입의 공격에서 어플리케이션을 보호할 수 있다.</li>
<li>SecurityFilterChain을 호출할 때 더 많은 유연함을 제공한다. Servlet Container에서 Filter는 URL기반으로<strong>만</strong> 호출된다. 그러나 FilterChainProxy는 RequestMatcher를 통해 HttpServletRequest의 어떤 것을 기반으로 호출되게 만들 수 있다.</li>
</ol>
<p>실제로 FilterChainProxy는 사용될 SecurityFilterChain을 결정하는데 사용될 수 있다. 아래 그림처럼 어플리케이션의 다른 부분에 대해 아예 다른 구성을 해서 분리할 수 있다.</p>
<p><img src="/assets/img/spring_security_filter/multi-securityfilterchain.png" alt="SecurityFilterChain" /></p>
<h2 id="스프링-시큐리티가-생성하는-필터들">스프링 시큐리티가 생성하는 필터들</h2>
<p>그러면 위에서 알아본 FilterChainProxy에 들어가는 시큐리티 필터들에 대해 알아보자. DefaultSecurityFilterchain에 들어가는 시큐리티 필터들의 리스트들은 다음과 같다. 이걸 순서대로 알아보자.</p>
<p><img src="/assets/img/spring_security_filter/default_filter.png" alt="defaultSecurityFilter" /></p>
<h3 id="webasyncmanagerintegrationfilter">WebAsyncManagerIntegrationFilter</h3>
<p>SecurityContextCallableProcessingInterceptor.beforeConcurrentHandling을 사용하여 Callable에 SecurityContext를 채우기 위해 SecurityContext와 Spring Web의 WebAsyncManager 간의 통합을 제공한다.</p>
<p>SecurityContextCallableProcessingInterceptor는 preProcess, postProcess, SecurityContextHolder.clearContext 메서드 같들에 의해 SecurityContextHolder에 주입된 SecurityContext를 설정하는 클래스다.</p>
<p>WebAsyncManager는 asynchronous reqeust를 관리하기 위한 클래스다.</p>
<p>결국 WebAsyncManagerIntegrationFilter는 Spring Web의 Async request와 SecurityContext를 연결해주는 역할을 하는 필터이다.</p>
<h3 id="securitycontextpersistencefilter">SecurityContextPersistenceFilter</h3>
<ol>
<li>SecurityContext 객체 생성, 저장, 조회</li>
<li>최종 응답 후 SecurityContextHolder.clearContext() 한다.</li>
</ol>
<p>인증 시 새로운 SecurityContext 객체를 생성하여 SecurityContextHolder에 저장한다. Filter를 통해 인증 성공 후 SecurityContext에 Authentication 객체를 저장한다. 인증이 최종 완료되면 HttpSession에 SecurityContext를 저장한다.</p>
<p>인증 후에는,</p>
<ol>
<li>Session에서 SecurityContext를 꺼내서 SecurityContextHolder 에 저장</li>
<li>SecurityContext 내에 Authentication 객체가 존재하면 인증된 상태를 유지한다.</li>
</ol>
<h4 id="동작-흐름">동작 흐름</h4>
<ol>
<li>요청이 SecurityContextPersistenceFilter로 들어온다.</li>
<li>HttpSecurityContextRepository에서 인증이 되었는지 확인한다.</li>
<li>인증이 안된 상태라면 SecurityContext 객체를 생성하여 SecurityContextHolder에 저장한다.
<ol>
<li>FilterChain을 돌면서 인증 절차를 거친 후 인증 성공 후 SecurityContext에 Authentication 객체를 저장한다.</li>
<li>인증 최종 완료 후 HttpSession에 SecurityContext를 저장한다.</li>
<li>SecurityContextHolder에서 SecurityContext를 clear한다.</li>
</ol>
</li>
<li>인증이 된 상태라면 Session에서 SecurityContext를 꺼내서 SecurityContextHolder에 저장한다.
<ol>
<li>SecurityContext 내에 Authentication 객체가 있는지 확인 한 후 있다면, 인증된 상태를 유지한다.</li>
</ol>
</li>
</ol>
<h3 id="headerwriterfilter">HeaderWriterFilter</h3>
<p>현재 응답에 헤더를 추가하기 위해 구현된 필터, X-Frame-Options, X-XSS-Protection, X-Content-Type-Options 같은 브라우저 보호를 켜는 헤더들을 추가하는데 유용하다.</p>
<p>필드로 <code class="language-plaintext highlighter-rouge">List<HeaderWriter> headerWriters</code>를 가지고 있고 내부에 <code class="language-plaintext highlighter-rouge">shouldWriteHeadersEagerly</code> 값을 설정하여 request의 시작에 헤더를 쓸지 말지를 설정할 수 있다.</p>
<h3 id="csrsfilter">CsrsFilter</h3>
<p>CSRF 취약점을 방지할 수 있도록 제공하는 필터다. 모든 요청에 랜덤하게 생성된 토큰을 HTTP 파라미터로 요구한다. 요청 시 전달되는 토큰 값과 서버에 저장된 토큰 값이 일치하지 않으면 요청이 실패한다. 요청 실패시 <code class="language-plaintext highlighter-rouge">accessDeniedHandler</code>가 CsrfTokenException 같은 예외를 처리하게 된다.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Override</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">doFilterInternal</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span>
<span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span> <span class="nc">FilterChain</span> <span class="n">filterChain</span><span class="o">)</span>
<span class="kd">throws</span> <span class="nc">ServletException</span><span class="o">,</span> <span class="nc">IOException</span> <span class="o">{</span>
<span class="n">request</span><span class="o">.</span><span class="na">setAttribute</span><span class="o">(</span><span class="nc">HttpServletResponse</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getName</span><span class="o">(),</span> <span class="n">response</span><span class="o">);</span>
<span class="nc">CsrfToken</span> <span class="n">csrfToken</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">tokenRepository</span><span class="o">.</span><span class="na">loadToken</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
<span class="kd">final</span> <span class="kt">boolean</span> <span class="n">missingToken</span> <span class="o">=</span> <span class="n">csrfToken</span> <span class="o">==</span> <span class="kc">null</span><span class="o">;</span>
<span class="k">if</span> <span class="o">(</span><span class="n">missingToken</span><span class="o">)</span> <span class="o">{</span>
<span class="n">csrfToken</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">tokenRepository</span><span class="o">.</span><span class="na">generateToken</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
<span class="k">this</span><span class="o">.</span><span class="na">tokenRepository</span><span class="o">.</span><span class="na">saveToken</span><span class="o">(</span><span class="n">csrfToken</span><span class="o">,</span> <span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">request</span><span class="o">.</span><span class="na">setAttribute</span><span class="o">(</span><span class="nc">CsrfToken</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getName</span><span class="o">(),</span> <span class="n">csrfToken</span><span class="o">);</span>
<span class="n">request</span><span class="o">.</span><span class="na">setAttribute</span><span class="o">(</span><span class="n">csrfToken</span><span class="o">.</span><span class="na">getParameterName</span><span class="o">(),</span> <span class="n">csrfToken</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(!</span><span class="k">this</span><span class="o">.</span><span class="na">requireCsrfProtectionMatcher</span><span class="o">.</span><span class="na">matches</span><span class="o">(</span><span class="n">request</span><span class="o">))</span> <span class="o">{</span>
<span class="n">filterChain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="nc">String</span> <span class="n">actualToken</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="n">csrfToken</span><span class="o">.</span><span class="na">getHeaderName</span><span class="o">());</span>
<span class="k">if</span> <span class="o">(</span><span class="n">actualToken</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">actualToken</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getParameter</span><span class="o">(</span><span class="n">csrfToken</span><span class="o">.</span><span class="na">getParameterName</span><span class="o">());</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">csrfToken</span><span class="o">.</span><span class="na">getToken</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="n">actualToken</span><span class="o">))</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">logger</span><span class="o">.</span><span class="na">isDebugEnabled</span><span class="o">())</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span><span class="s">"Invalid CSRF token found for "</span>
<span class="o">+</span> <span class="nc">UrlUtils</span><span class="o">.</span><span class="na">buildFullRequestUrl</span><span class="o">(</span><span class="n">request</span><span class="o">));</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="n">missingToken</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">accessDeniedHandler</span><span class="o">.</span><span class="na">handle</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">,</span>
<span class="k">new</span> <span class="nf">MissingCsrfTokenException</span><span class="o">(</span><span class="n">actualToken</span><span class="o">));</span>
<span class="o">}</span>
<span class="k">else</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">accessDeniedHandler</span><span class="o">.</span><span class="na">handle</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">,</span>
<span class="k">new</span> <span class="nf">InvalidCsrfTokenException</span><span class="o">(</span><span class="n">csrfToken</span><span class="o">,</span> <span class="n">actualToken</span><span class="o">));</span>
<span class="o">}</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="n">filterChain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>위에서 부터 따라가보면, 토큰이 tokenRepository에 있는지 확인하고, 없으면 새로 만든다.(<code class="language-plaintext highlighter-rouge">generateToken(request)</code>) generateToken 메서드는 다음과 같다.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">CsrfToken</span> <span class="nf">generateToken</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">DefaultCsrfToken</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">headerName</span><span class="o">,</span> <span class="k">this</span><span class="o">.</span><span class="na">parameterName</span><span class="o">,</span>
<span class="n">createNewToken</span><span class="o">());</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="nc">String</span> <span class="nf">createNewToken</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="no">UUID</span><span class="o">.</span><span class="na">randomUUID</span><span class="o">().</span><span class="na">toString</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<p>이처럼 랜덤 UUID로 새로운 토큰을 만들어서 tokenRepository에 넣는다. 그리고 요청 시에 전달된 토큰 값과 tokenRepository에 저장된 토큰값이 일치하지 않으면(<code class="language-plaintext highlighter-rouge">!csrfToken.getToken().equals(actualToken)</code>) 요청이 실패하고, accessDeniedHandler에서 처리하게 된다.</p>
<h3 id="logoutfilter">LogoutFilter</h3>
<p>로그아웃 요청을 하면 이 필터에서 처리한다. Logout시에는 세션 무효화, 인증토큰 삭제, 쿠키정보 삭제, 로그인 페이지로 리다이렉트 한다.</p>
<ol>
<li>로그아웃 요청이 들어온다.</li>
<li>RequestMatcher가 로그아웃 URL이 맞는지 확인한다.(일치하지 않으면 filterChain의 다음 필터로 이동)</li>
<li>SecurityContext에서 Authentication 객체를 꺼내와서 SecurityContextLogoutHandler로 전달</li>
<li>세션 무효화, 쿠키 삭제, SecurityContextHolder.clearContext() 한다.</li>
<li>LogoutFilter는 SimpleUrlLogoutSuccessHandler에서 성공 이후 처리를 한다.(redirect:/)</li>
</ol>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// LogoutFilter.java</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">doFilter</span><span class="o">(</span><span class="nc">ServletRequest</span> <span class="n">req</span><span class="o">,</span> <span class="nc">ServletResponse</span> <span class="n">res</span><span class="o">,</span> <span class="nc">FilterChain</span> <span class="n">chain</span><span class="o">)</span>
<span class="kd">throws</span> <span class="nc">IOException</span><span class="o">,</span> <span class="nc">ServletException</span> <span class="o">{</span>
<span class="nc">HttpServletRequest</span> <span class="n">request</span> <span class="o">=</span> <span class="o">(</span><span class="nc">HttpServletRequest</span><span class="o">)</span> <span class="n">req</span><span class="o">;</span>
<span class="nc">HttpServletResponse</span> <span class="n">response</span> <span class="o">=</span> <span class="o">(</span><span class="nc">HttpServletResponse</span><span class="o">)</span> <span class="n">res</span><span class="o">;</span>
<span class="k">if</span> <span class="o">(</span><span class="n">requiresLogout</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">))</span> <span class="o">{</span>
<span class="nc">Authentication</span> <span class="n">auth</span> <span class="o">=</span> <span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">().</span><span class="na">getAuthentication</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">logger</span><span class="o">.</span><span class="na">isDebugEnabled</span><span class="o">())</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span><span class="s">"Logging out user '"</span> <span class="o">+</span> <span class="n">auth</span>
<span class="o">+</span> <span class="s">"' and transferring to logout destination"</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">this</span><span class="o">.</span><span class="na">handler</span><span class="o">.</span><span class="na">logout</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">,</span> <span class="n">auth</span><span class="o">);</span>
<span class="n">logoutSuccessHandler</span><span class="o">.</span><span class="na">onLogoutSuccess</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">,</span> <span class="n">auth</span><span class="o">);</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="n">chain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">// SecurityContextLogoutHandler.java</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">logout</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span>
<span class="nc">Authentication</span> <span class="n">authentication</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Assert</span><span class="o">.</span><span class="na">notNull</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="s">"HttpServletRequest required"</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">invalidateHttpSession</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">HttpSession</span> <span class="n">session</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getSession</span><span class="o">(</span><span class="kc">false</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">session</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span><span class="s">"Invalidating session: "</span> <span class="o">+</span> <span class="n">session</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
<span class="n">session</span><span class="o">.</span><span class="na">invalidate</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="n">clearAuthentication</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">SecurityContext</span> <span class="n">context</span> <span class="o">=</span> <span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">();</span>
<span class="n">context</span><span class="o">.</span><span class="na">setAuthentication</span><span class="o">(</span><span class="kc">null</span><span class="o">);</span>
<span class="o">}</span>
<span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">clearContext</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<ol>
<li><code class="language-plaintext highlighter-rouge">requiresLogout(req, res)</code> 부분에서 requestMatcher에서 match하는지 확인하고, handler에서 로그아웃 요청을 한다.</li>
<li>SecurityContextLogoutHandler에서 세션 무효화, Authentication 초기화, clearContext() 를 처리한다.</li>
<li>그 다음 <code class="language-plaintext highlighter-rouge">logoutSuccessHandler.onLogoutSuccess(request, response, auth);</code>에서 로그아웃 이후 처리를 한다.</li>
</ol>
<h3 id="usernamepasswordauthenticationfilter">UsernamePasswordAuthenticationFilter</h3>
<p>Form Login시 요청을 처리하는 필터다.</p>
<ol>
<li>AntPathRequestMatcher(“/login”) - 요청 정보가 매칭되는지 확인
<ul>
<li>No: chain.doFilter</li>
<li>Yes: 2으로 이동</li>
</ul>
</li>
<li>Authentication (Username + Password)을 가지고 4에 인증 요청</li>
<li>AuthenticationManager는 5에 인증 위임</li>
<li>AuthenticationProvider - 실제 인증 작업이 이뤄지는 곳
<ul>
<li>인증 실패 - AuthenticationException</li>
<li>인증 성공 - 인증 성공한 Authentication 객체를 AuthenticationManager에 반환, 6으로 이동</li>
</ul>
</li>
<li>SecurtyContext에 인증 성공한 객체를 저장</li>
<li>SuccessHandler에서 성공 이후 흐름 처리</li>
</ol>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">Authentication</span> <span class="nf">attemptAuthentication</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span>
<span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">AuthenticationException</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">postOnly</span> <span class="o">&&</span> <span class="o">!</span><span class="n">request</span><span class="o">.</span><span class="na">getMethod</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="s">"POST"</span><span class="o">))</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">AuthenticationServiceException</span><span class="o">(</span>
<span class="s">"Authentication method not supported: "</span> <span class="o">+</span> <span class="n">request</span><span class="o">.</span><span class="na">getMethod</span><span class="o">());</span>
<span class="o">}</span>
<span class="nc">String</span> <span class="n">username</span> <span class="o">=</span> <span class="n">obtainUsername</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
<span class="nc">String</span> <span class="n">password</span> <span class="o">=</span> <span class="n">obtainPassword</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">username</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">username</span> <span class="o">=</span> <span class="s">""</span><span class="o">;</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="n">password</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">password</span> <span class="o">=</span> <span class="s">""</span><span class="o">;</span>
<span class="o">}</span>
<span class="n">username</span> <span class="o">=</span> <span class="n">username</span><span class="o">.</span><span class="na">trim</span><span class="o">();</span>
<span class="nc">UsernamePasswordAuthenticationToken</span> <span class="n">authRequest</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">UsernamePasswordAuthenticationToken</span><span class="o">(</span>
<span class="n">username</span><span class="o">,</span> <span class="n">password</span><span class="o">);</span>
<span class="c1">// Allow subclasses to set the "details" property</span>
<span class="n">setDetails</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">authRequest</span><span class="o">);</span>
<span class="k">return</span> <span class="k">this</span><span class="o">.</span><span class="na">getAuthenticationManager</span><span class="o">().</span><span class="na">authenticate</span><span class="o">(</span><span class="n">authRequest</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>이게 스프링 시큐리티에서 기본적으로 적용하는 필터의 인증 코드이다. 이 클래스에서 하는 역할을 나는 다음과 같이 다시 작성하였다.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">Authentication</span> <span class="nf">attemptAuthentication</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">req</span><span class="o">,</span> <span class="nc">HttpServletResponse</span> <span class="n">res</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">AuthenticationException</span><span class="o">,</span> <span class="nc">IOException</span><span class="o">,</span> <span class="nc">ServletException</span> <span class="o">{</span>
<span class="nc">String</span> <span class="n">email</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="na">getParameter</span><span class="o">(</span><span class="s">"email"</span><span class="o">);</span>
<span class="nc">String</span> <span class="n">password</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="na">getParameter</span><span class="o">(</span><span class="s">"password"</span><span class="o">);</span>
<span class="nc">UserLoginRequest</span> <span class="n">userLoginRequest</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">UserLoginRequest</span><span class="o">(</span><span class="k">new</span> <span class="nc">Email</span><span class="o">(</span><span class="n">email</span><span class="o">),</span> <span class="k">new</span> <span class="nc">Password</span><span class="o">(</span><span class="n">password</span><span class="o">));</span>
<span class="nc">PreAuthorizationToken</span> <span class="n">token</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">PreAuthorizationToken</span><span class="o">(</span><span class="n">userLoginRequest</span><span class="o">);</span>
<span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span><span class="s">"Requested Login Email: {}"</span><span class="o">,</span> <span class="n">email</span><span class="o">);</span>
<span class="k">return</span> <span class="kd">super</span><span class="o">.</span><span class="na">getAuthenticationManager</span><span class="o">().</span><span class="na">authenticate</span><span class="o">(</span><span class="n">token</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>코드의 흐름 자체는 차이가 없고, 사용하는 principal, credential만 다르게 사용했음을 알 수 있다.</p>
<h3 id="defaultloginpagegeneratingfilter-defaultlogoutpagegeneratingfilter">DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter</h3>
<p>이름에서 유추할 수 있는 그대로의 필터다. 우리가 설정에서 따로 로그인, 로그아웃 페이지를 설정하지 않으면 기본적으로 제공하는 로그인 페이지, 로그아웃 페이지를 만드는 필터다. 각 필터에 들어가서 내용을 확인해보면 html 파일을 StringBuilder로 작성해놓은 것을 볼 수 있다.</p>
<h3 id="basicauthenticationfilter">BasicAuthenticationFilter</h3>
<p>HTTP 요청의 Basic 인증 헤더를 처리하고 그 결과를 SecurityContextHolder에 넣는 일을 하는 필터이다. 요약해서 Authorization의 HTTP reqeust header를 가진 어떤 요청을 처리하는 필터이다. (Basic authentication scheme, Base64 encoding)</p>
<ol>
<li>자원 접근 시 Client에 401 Unauthorized 응답, WWW-Authenticate header로 인증 요청을 보낸다.</li>
<li>Client는 Base64 encoding된 ID, Password를 Authorization header에 추가 후 서버에 자원을 요청한다. (Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==)</li>
</ol>
<h3 id="requestcacheawarefilter">RequestCacheAwareFilter</h3>
<p>저장된 요청이 현재 요청과 일치하는 경우(캐시된 경우) 저장된 요청을 재사용하는 필터다.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">doFilter</span><span class="o">(</span><span class="nc">ServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="nc">ServletResponse</span> <span class="n">response</span><span class="o">,</span>
<span class="nc">FilterChain</span> <span class="n">chain</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span><span class="o">,</span> <span class="nc">ServletException</span> <span class="o">{</span>
<span class="nc">HttpServletRequest</span> <span class="n">wrappedSavedRequest</span> <span class="o">=</span> <span class="n">requestCache</span><span class="o">.</span><span class="na">getMatchingRequest</span><span class="o">(</span>
<span class="o">(</span><span class="nc">HttpServletRequest</span><span class="o">)</span> <span class="n">request</span><span class="o">,</span> <span class="o">(</span><span class="nc">HttpServletResponse</span><span class="o">)</span> <span class="n">response</span><span class="o">);</span>
<span class="n">chain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">wrappedSavedRequest</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">?</span> <span class="n">request</span> <span class="o">:</span> <span class="n">wrappedSavedRequest</span><span class="o">,</span>
<span class="n">response</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>보면 requestCache에 현재 요청과 일치하는게 있는지 확인하고, 일치하는 것이 있으면 그것을 사용하고 없으면 현재 요청이 들어온 것을 그대로 사용해서 다음 필터로 넘기는 것을 알 수 있다.</p>
<h3 id="securitycontextholderawarerequestfilter">SecurityContextHolderAwareRequestFilter</h3>
<p>Servlet API의 security method를 구현한 request wrapper로 ServletRequest를 채우는 필터다.</p>
<h3 id="anonnymousauthenticationfilter">AnonnymousAuthenticationFilter</h3>
<p>익명사용자에 대한 인증 처리 필터다. SecurityContextHolder에 아무 객체도 없는지(익명사용자) 체크하고, 필요하다면 채우는 필터다.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">doFilter</span><span class="o">(</span><span class="nc">ServletRequest</span> <span class="n">req</span><span class="o">,</span> <span class="nc">ServletResponse</span> <span class="n">res</span><span class="o">,</span> <span class="nc">FilterChain</span> <span class="n">chain</span><span class="o">)</span>
<span class="kd">throws</span> <span class="nc">IOException</span><span class="o">,</span> <span class="nc">ServletException</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">().</span><span class="na">getAuthentication</span><span class="o">()</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">().</span><span class="na">setAuthentication</span><span class="o">(</span>
<span class="n">createAuthentication</span><span class="o">((</span><span class="nc">HttpServletRequest</span><span class="o">)</span> <span class="n">req</span><span class="o">));</span>
<span class="k">if</span> <span class="o">(</span><span class="n">logger</span><span class="o">.</span><span class="na">isDebugEnabled</span><span class="o">())</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span><span class="s">"Populated SecurityContextHolder with anonymous token: '"</span>
<span class="o">+</span> <span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">().</span><span class="na">getAuthentication</span><span class="o">()</span> <span class="o">+</span> <span class="s">"'"</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="k">else</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">logger</span><span class="o">.</span><span class="na">isDebugEnabled</span><span class="o">())</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span><span class="s">"SecurityContextHolder not populated with anonymous token, as it already contained: '"</span>
<span class="o">+</span> <span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">().</span><span class="na">getAuthentication</span><span class="o">()</span> <span class="o">+</span> <span class="s">"'"</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">chain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">req</span><span class="o">,</span> <span class="n">res</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">protected</span> <span class="nc">Authentication</span> <span class="nf">createAuthentication</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">AnonymousAuthenticationToken</span> <span class="n">auth</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AnonymousAuthenticationToken</span><span class="o">(</span><span class="n">key</span><span class="o">,</span>
<span class="n">principal</span><span class="o">,</span> <span class="n">authorities</span><span class="o">);</span>
<span class="n">auth</span><span class="o">.</span><span class="na">setDetails</span><span class="o">(</span><span class="n">authenticationDetailsSource</span><span class="o">.</span><span class="na">buildDetails</span><span class="o">(</span><span class="n">request</span><span class="o">));</span>
<span class="k">return</span> <span class="n">auth</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>코드를 쭉 읽어보면 SecurityContextHolder에서 SecurityContext를 꺼내서 Authentication 객체가 없는 경우 새로 만들어 주고(<code class="language-plaintext highlighter-rouge">AnonymousAuthenticationToken</code>), Authentication 객체가 있는 경우는 아무 것도 하지 않고(log만 남기고) 다음 필터로 넘어가는 것을 볼 수 있다.</p>
<h3 id="sessionmanagementfilter">SessionManagementFilter</h3>
<p>세션과 관련된 여러 역할을 수행하는 필터다.</p>
<ol>
<li>세션관리</li>
<li>동시적 세션 제어</li>
<li>세션 고정 보호</li>
<li>세션 정책 생성</li>
</ol>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">doFilter</span><span class="o">(</span><span class="nc">ServletRequest</span> <span class="n">req</span><span class="o">,</span> <span class="nc">ServletResponse</span> <span class="n">res</span><span class="o">,</span> <span class="nc">FilterChain</span> <span class="n">chain</span><span class="o">)</span>
<span class="kd">throws</span> <span class="nc">IOException</span><span class="o">,</span> <span class="nc">ServletException</span> <span class="o">{</span>
<span class="nc">HttpServletRequest</span> <span class="n">request</span> <span class="o">=</span> <span class="o">(</span><span class="nc">HttpServletRequest</span><span class="o">)</span> <span class="n">req</span><span class="o">;</span>
<span class="nc">HttpServletResponse</span> <span class="n">response</span> <span class="o">=</span> <span class="o">(</span><span class="nc">HttpServletResponse</span><span class="o">)</span> <span class="n">res</span><span class="o">;</span>
<span class="k">if</span> <span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getAttribute</span><span class="o">(</span><span class="no">FILTER_APPLIED</span><span class="o">)</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">chain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="n">request</span><span class="o">.</span><span class="na">setAttribute</span><span class="o">(</span><span class="no">FILTER_APPLIED</span><span class="o">,</span> <span class="nc">Boolean</span><span class="o">.</span><span class="na">TRUE</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">securityContextRepository</span><span class="o">.</span><span class="na">containsContext</span><span class="o">(</span><span class="n">request</span><span class="o">))</span> <span class="o">{</span>
<span class="nc">Authentication</span> <span class="n">authentication</span> <span class="o">=</span> <span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">()</span>
<span class="o">.</span><span class="na">getAuthentication</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">authentication</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&&</span> <span class="o">!</span><span class="n">trustResolver</span><span class="o">.</span><span class="na">isAnonymous</span><span class="o">(</span><span class="n">authentication</span><span class="o">))</span> <span class="o">{</span>
<span class="c1">// The user has been authenticated during the current request, so call the</span>
<span class="c1">// session strategy</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">sessionAuthenticationStrategy</span><span class="o">.</span><span class="na">onAuthentication</span><span class="o">(</span><span class="n">authentication</span><span class="o">,</span>
<span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">catch</span> <span class="o">(</span><span class="nc">SessionAuthenticationException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// The session strategy can reject the authentication</span>
<span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span>
<span class="s">"SessionAuthenticationStrategy rejected the authentication object"</span><span class="o">,</span>
<span class="n">e</span><span class="o">);</span>
<span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">clearContext</span><span class="o">();</span>
<span class="n">failureHandler</span><span class="o">.</span><span class="na">onAuthenticationFailure</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">,</span> <span class="n">e</span><span class="o">);</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">// Eagerly save the security context to make it available for any possible</span>
<span class="c1">// re-entrant</span>
<span class="c1">// requests which may occur before the current request completes.</span>
<span class="c1">// SEC-1396.</span>
<span class="n">securityContextRepository</span><span class="o">.</span><span class="na">saveContext</span><span class="o">(</span><span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">(),</span>
<span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">else</span> <span class="o">{</span>
<span class="c1">// No security context or authentication present. Check for a session</span>
<span class="c1">// timeout</span>
<span class="k">if</span> <span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getRequestedSessionId</span><span class="o">()</span> <span class="o">!=</span> <span class="kc">null</span>
<span class="o">&&</span> <span class="o">!</span><span class="n">request</span><span class="o">.</span><span class="na">isRequestedSessionIdValid</span><span class="o">())</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">logger</span><span class="o">.</span><span class="na">isDebugEnabled</span><span class="o">())</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span><span class="s">"Requested session ID "</span>
<span class="o">+</span> <span class="n">request</span><span class="o">.</span><span class="na">getRequestedSessionId</span><span class="o">()</span> <span class="o">+</span> <span class="s">" is invalid."</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="n">invalidSessionStrategy</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">invalidSessionStrategy</span>
<span class="o">.</span><span class="na">onInvalidSessionDetected</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">chain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>위부터 쭉 코드를 따라가보면, 기존에 Session Management Filter가 적용되지 않았다면 아무것도 하지 않고 다음 필터로 넘어간다. 기존에 적용되고 있었다면 다음 로직을 수행한다.</p>
<ol>
<li>SecurityContext에 Authentication 객체가 있고, anonymous가 아닌경우 세션 정책에 맞게 인증을 하게된다. (이번 요청에서 인증을 받았다.)
<ul>
<li>인증 성공 시, SecurityContextRepository에 saveContext한다.</li>
<li>인증이 실패할 경우, SecurityContext clear하고 failureHandler로 넘긴다.</li>
</ul>
</li>
<li>1번이 아닐 경우, 요청에서 SessionID를 체크하고 유효한지 점검한다.</li>
</ol>
<p>SessionAuthenticationStrategy에 설정된 세션 인증 전략으로 이 필터의 행동을 수행한다.</p>
<h3 id="exceptiontranslationfilter">ExceptionTranslationFilter</h3>
<p>FilterChain에서 발생하는 AccessDeniedException(인가예외)와 AuthenticationException(인증예외)을 처리하는 필터다. 이 필터에서 하는 일은 다음 메서드에서 확인할 수 있다.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">handleSpringSecurityException</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span>
<span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span> <span class="nc">FilterChain</span> <span class="n">chain</span><span class="o">,</span> <span class="nc">RuntimeException</span> <span class="n">exception</span><span class="o">)</span>
<span class="kd">throws</span> <span class="nc">IOException</span><span class="o">,</span> <span class="nc">ServletException</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">exception</span> <span class="k">instanceof</span> <span class="nc">AuthenticationException</span><span class="o">)</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span>
<span class="s">"Authentication exception occurred; redirecting to authentication entry point"</span><span class="o">,</span>
<span class="n">exception</span><span class="o">);</span>
<span class="n">sendStartAuthentication</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">,</span> <span class="n">chain</span><span class="o">,</span>
<span class="o">(</span><span class="nc">AuthenticationException</span><span class="o">)</span> <span class="n">exception</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">else</span> <span class="nf">if</span> <span class="o">(</span><span class="n">exception</span> <span class="k">instanceof</span> <span class="nc">AccessDeniedException</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Authentication</span> <span class="n">authentication</span> <span class="o">=</span> <span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">().</span><span class="na">getAuthentication</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">authenticationTrustResolver</span><span class="o">.</span><span class="na">isAnonymous</span><span class="o">(</span><span class="n">authentication</span><span class="o">)</span> <span class="o">||</span> <span class="n">authenticationTrustResolver</span><span class="o">.</span><span class="na">isRememberMe</span><span class="o">(</span><span class="n">authentication</span><span class="o">))</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span>
<span class="s">"Access is denied (user is "</span> <span class="o">+</span> <span class="o">(</span><span class="n">authenticationTrustResolver</span><span class="o">.</span><span class="na">isAnonymous</span><span class="o">(</span><span class="n">authentication</span><span class="o">)</span> <span class="o">?</span> <span class="s">"anonymous"</span> <span class="o">:</span> <span class="s">"not fully authenticated"</span><span class="o">)</span> <span class="o">+</span> <span class="s">"); redirecting to authentication entry point"</span><span class="o">,</span>
<span class="n">exception</span><span class="o">);</span>
<span class="n">sendStartAuthentication</span><span class="o">(</span>
<span class="n">request</span><span class="o">,</span>
<span class="n">response</span><span class="o">,</span>
<span class="n">chain</span><span class="o">,</span>
<span class="k">new</span> <span class="nf">InsufficientAuthenticationException</span><span class="o">(</span>
<span class="n">messages</span><span class="o">.</span><span class="na">getMessage</span><span class="o">(</span>
<span class="s">"ExceptionTranslationFilter.insufficientAuthentication"</span><span class="o">,</span>
<span class="s">"Full authentication is required to access this resource"</span><span class="o">)));</span>
<span class="o">}</span>
<span class="k">else</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span>
<span class="s">"Access is denied (user is not anonymous); delegating to AccessDeniedHandler"</span><span class="o">,</span>
<span class="n">exception</span><span class="o">);</span>
<span class="n">accessDeniedHandler</span><span class="o">.</span><span class="na">handle</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">,</span>
<span class="o">(</span><span class="nc">AccessDeniedException</span><span class="o">)</span> <span class="n">exception</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>인증 예외가 발생한 경우(<code class="language-plaintext highlighter-rouge">exception instanceof AuthenticationException</code>) 다음 메서드가 실행된다.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">protected</span> <span class="kt">void</span> <span class="nf">sendStartAuthentication</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span>
<span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span> <span class="nc">FilterChain</span> <span class="n">chain</span><span class="o">,</span>
<span class="nc">AuthenticationException</span> <span class="n">reason</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">ServletException</span><span class="o">,</span> <span class="nc">IOException</span> <span class="o">{</span>
<span class="c1">// SEC-112: Clear the SecurityContextHolder's Authentication, as the</span>
<span class="c1">// existing Authentication is no longer considered valid</span>
<span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">().</span><span class="na">setAuthentication</span><span class="o">(</span><span class="kc">null</span><span class="o">);</span>
<span class="n">requestCache</span><span class="o">.</span><span class="na">saveRequest</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
<span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span><span class="s">"Calling Authentication entry point."</span><span class="o">);</span>
<span class="n">authenticationEntryPoint</span><span class="o">.</span><span class="na">commence</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">,</span> <span class="n">reason</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<ol>
<li>Authentication 객체 초기화</li>
<li>requestCache에 예외 발생 전의 요청정보 저장</li>
<li>AuthenticationEntryPoint를 호출, 이 클래스를 구현한 구현체에서 이후 처리를 할 수 있도록 함(로그인 페이지로 이동한다거나, 401 코드 전달한다거나)</li>
</ol>
<p>인가 예외가 발생한 경우(<code class="language-plaintext highlighter-rouge">exception instanceof AccessDeniedException</code>) 다음 두 가지 방향으로 처리한다.</p>
<ol>
<li>Anonymous 또는 RememberMe일 경우에는 InsufficientAuthenticationException를 발생시키는데 이는 AuthenticationException를 상속받은 예외이다. 그래서 다시 인증 예외쪽으로 빠진다.</li>
<li>그 외의 경우에는 AccessDeniedHandler를 구현한 구현체에서 이후 처리를 한다.</li>
</ol>
<h3 id="filtersecurityinterceptor">FilterSecurityInterceptor</h3>
<p>마지막에 위치한 필터, 인증된 사용자에 대해 요청의 승인/거부 여부를 결정한다.</p>
<ol>
<li>인증 객체 없이 자원에 접근 시도할 경우 AuthenticationException 발생</li>
<li>권한 없이 자원에 접근할 경우 AccessDeniedException 발생</li>
<li>권한 처리는 AccessDecisionManager에서 처리한다.</li>
</ol>
<p>즉, SecurityContext내에 Authentication 객체가 존재하면, SecurityMetadataSource에서 요청한 자원에 필요한 권한 정보를 찾아서 AccessDecisionManager에 전달한다.</p>
<p>이해를 돕기위한 그림으로 스프링 시큐리티 공식문서의 그림을 첨부한다.</p>
<p><img src="/assets/img/spring_security_filter/filtersecurityinterceptor.png" alt="FilterSecurityInterceptor" /></p>
<p>공식 문서에 나온 순서는 다음과 같다.</p>
<ol>
<li>FilterSecurityInterceptor는 SecurityContextHolder에서 Authentication 객체를 얻는다.</li>
<li>FilterSecurityInterceptor HttpServletRequest, HttpServletResponse, FilterChain으로부터 FilterInvocation을 만든다.</li>
<li>ConfigAttributes를 얻기 위해 SecurityMetadataSource에 FilterInvocation을 넘긴다.</li>
<li>Authentication, FilterInvocation, ConfigAttributes를 AccessDecisionManager로 넘긴다.
<ul>
<li>5 인증이 실패하면, AccessDeniedException을 발생시킨다. 이 경우엔 위에서 언급했던 ExceptionTranslationFilter가 AccessDeniedException를 처리한다.</li>
<li>6 인증 성공시, FilterSecurityInterceptor는 어플리케이션을 다음 동작으로 진행시킨다.</li>
</ul>
</li>
</ol>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">doFilter</span><span class="o">(</span><span class="nc">ServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="nc">ServletResponse</span> <span class="n">response</span><span class="o">,</span>
<span class="nc">FilterChain</span> <span class="n">chain</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span><span class="o">,</span> <span class="nc">ServletException</span> <span class="o">{</span>
<span class="nc">FilterInvocation</span> <span class="n">fi</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">FilterInvocation</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">,</span> <span class="n">chain</span><span class="o">);</span>
<span class="n">invoke</span><span class="o">(</span><span class="n">fi</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">invoke</span><span class="o">(</span><span class="nc">FilterInvocation</span> <span class="n">fi</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span><span class="o">,</span> <span class="nc">ServletException</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">((</span><span class="n">fi</span><span class="o">.</span><span class="na">getRequest</span><span class="o">()</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span>
<span class="o">&&</span> <span class="o">(</span><span class="n">fi</span><span class="o">.</span><span class="na">getRequest</span><span class="o">().</span><span class="na">getAttribute</span><span class="o">(</span><span class="no">FILTER_APPLIED</span><span class="o">)</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span>
<span class="o">&&</span> <span class="n">observeOncePerRequest</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// filter already applied to this request and user wants us to observe</span>
<span class="c1">// once-per-request handling, so don't re-do security checking</span>
<span class="n">fi</span><span class="o">.</span><span class="na">getChain</span><span class="o">().</span><span class="na">doFilter</span><span class="o">(</span><span class="n">fi</span><span class="o">.</span><span class="na">getRequest</span><span class="o">(),</span> <span class="n">fi</span><span class="o">.</span><span class="na">getResponse</span><span class="o">());</span>
<span class="o">}</span>
<span class="k">else</span> <span class="o">{</span>
<span class="c1">// first time this request being called, so perform security checking</span>
<span class="k">if</span> <span class="o">(</span><span class="n">fi</span><span class="o">.</span><span class="na">getRequest</span><span class="o">()</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&&</span> <span class="n">observeOncePerRequest</span><span class="o">)</span> <span class="o">{</span>
<span class="n">fi</span><span class="o">.</span><span class="na">getRequest</span><span class="o">().</span><span class="na">setAttribute</span><span class="o">(</span><span class="no">FILTER_APPLIED</span><span class="o">,</span> <span class="nc">Boolean</span><span class="o">.</span><span class="na">TRUE</span><span class="o">);</span>
<span class="o">}</span>
<span class="nc">InterceptorStatusToken</span> <span class="n">token</span> <span class="o">=</span> <span class="kd">super</span><span class="o">.</span><span class="na">beforeInvocation</span><span class="o">(</span><span class="n">fi</span><span class="o">);</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">fi</span><span class="o">.</span><span class="na">getChain</span><span class="o">().</span><span class="na">doFilter</span><span class="o">(</span><span class="n">fi</span><span class="o">.</span><span class="na">getRequest</span><span class="o">(),</span> <span class="n">fi</span><span class="o">.</span><span class="na">getResponse</span><span class="o">());</span>
<span class="o">}</span>
<span class="k">finally</span> <span class="o">{</span>
<span class="kd">super</span><span class="o">.</span><span class="na">finallyInvocation</span><span class="o">(</span><span class="n">token</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">super</span><span class="o">.</span><span class="na">afterInvocation</span><span class="o">(</span><span class="n">token</span><span class="o">,</span> <span class="kc">null</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>코드를 쭉 따라가다보면, <code class="language-plaintext highlighter-rouge">super.beforeInvocation(fi)</code>부분에서 AbstractSecurityInterceptor에서 수행하는 것을 볼 수 있다. AbstractSecurityInterceptor의 해당 메서드 부분을 보면 accessDecisionManager에서 권한 처리를 하는 것을 볼 수 있다.</p>
<h4 id="accessdecisionmanager">AccessDecisionManager</h4>
<p>인가 여부를 결정하고 접근을 승인/거부 하는 클래스, 여러개의 Voter들을 가질 수 있고 각 Voter로부터 승인/거부/보류를 리턴 받고 판단한다.</p>
<h4 id="접근결정-유형">접근결정 유형</h4>
<ol>
<li>AffirmativeBased - Voter 중 하나라도 허가나면 허가</li>
<li>ConsensusBased - 다수결로 결정, 동수일 경우 기본은 접근허가, allowIfEqualGrantedDeniedDecisions = false로 하면 접근 거부</li>
<li>UnanimousBased - 만장일치로 허가가 나야 접근 허가</li>
</ol>
<p>각 클래스들의 decide메서드의 구현을 보고 읽기만 해도 어떤 내용인지 이해가 된다. 코드는 불필요하게 내용이 길어지는 것 같아 생략하지만, 읽기만 해도 어떤 의미인지 이해할 수 있다.</p>
<h4 id="accessdecisionvoter">AccessDecisionVoter</h4>
<p>접근을 허가/거부할 지 판단하는 곳, Authentication, FilterInvocation, ConfigAttributes를 받아서 판단한다.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">AccessDecisionVoter</span><span class="o"><</span><span class="no">S</span><span class="o">></span> <span class="o">{</span>
<span class="c1">// ~ Static fields/initializers</span>
<span class="c1">// =====================================================================================</span>
<span class="kt">int</span> <span class="no">ACCESS_GRANTED</span> <span class="o">=</span> <span class="mi">1</span><span class="o">;</span>
<span class="kt">int</span> <span class="no">ACCESS_ABSTAIN</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
<span class="kt">int</span> <span class="no">ACCESS_DENIED</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="o">;</span>
</code></pre></div></div>
<h3 id="이외의-필터">이외의 필터</h3>
<p>공식 문서에는 나와있지만, 아무 설정도 안했을 때는 들어가지 않는 filter들이 있다.</p>
<ol>
<li>ChannelProcessingFilter</li>
<li>ConcurrentSessionFilter</li>
<li>CorfFilter</li>
<li>OAuth2AuthorizationRequestRedirectFilter</li>
<li>Saml2WebSsoAuthenticationRequestFilter</li>
<li>X509AuthenticationFilter</li>
<li>AbstractPreAuthenticatedProcessingFilter</li>
<li>CasAuthenticationFilter</li>
<li>OAuth2LoginAuthenticationFilter</li>
<li>Saml2WebSsoAuthenticationFilter</li>
<li>OpenIDAuthenticationFilter</li>
<li>DigestAuthenticationFilter</li>
<li>BearerTokenAuthenticationFilter</li>
<li>RequestCacheAwareFilter</li>
<li>JaasApiIntegrationFilter</li>
<li>RememberMeAuthenticationFilter</li>
<li>OAuth2AuthorizationCodeGrantFilter</li>
<li>SwitchUserFilter</li>
</ol>
<p>이 부분은 나중에 알아보자.</p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://docs.spring.io/spring-security/site/docs/current/reference/html5/#servlet-filters-review">Spring Security Docs - 9. Servlet Security</a></li>
<li>Spring Security 코드</li>
<li>인프런 강의</li>
</ul>Martinoeeen3@gmail.comSpring Security의 구조를 살펴보면, 사용자의 요청이 들어온 후 가장 먼저 처리되는 곳이 바로 Filter(FilterChain)이다.22주차 회고2020-05-31T13:00:59+00:002020-05-31T13:00:59+00:00https://smjeon.dev/retrospective/weekly-WW22<p>22주차 회고</p>
<h2 id="이번-주-ww22">이번 주 (WW22)</h2>
<h3 id="이번주-하기로-목표-했던-일">이번주 하기로 목표 했던 일</h3>
<ul class="task-list">
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />시큐리티 강의 다 보고 따라하기
<ul>
<li>이론은 다 들었으나, 따라해서 내 프로젝트에 적용하진 못했다.</li>
</ul>
</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />css 기초 공부
<ul>
<li>노마드코더 강의 보고 따라하려고 했는데, 아직 못했다.</li>
</ul>
</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />DB 기초 공부
<ul>
<li>DB 정규화 공부</li>
</ul>
</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />이전 프로젝트 리뷰, 스프링 기초 개념 정리
<ul>
<li>DispatcherServlet</li>
<li>Spring에서 요청 -> 응답 흐름</li>
</ul>
</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />상품 기능 완성, 구매 로직 개발
<ul>
<li>시큐리티 적용하느라 못했다.</li>
<li>일부 상품 기능은 구현 중이다.</li>
<li>이벤트소싱에 대해 공부해보니, 구매 로직은 이 방식으로 개발하는건 어떨까 생각이 들었다….(엄청 오래걸릴 것 같은데..)</li>
</ul>
</li>
</ul>
<h3 id="계획-하지-않았지만-했던-일">계획 하지 않았지만 했던 일</h3>
<ul>
<li>IPv4 관련 네트워크 공부</li>
<li>CQRS, 이벤트소싱 개념 공부</li>
<li>Spring Security 내용 정리</li>
</ul>
<h3 id="나에게-칭찬-해주고-싶은-것">나에게 칭찬 해주고 싶은 것</h3>
<ul>
<li>기초 공부를 조금 했다.</li>
</ul>
<h3 id="공부한-내용">공부한 내용</h3>
<ul>
<li>CQRS, 이벤트소싱</li>
<li>Network 기초</li>
<li>DB 기초</li>
<li>Spring Security</li>
</ul>
<h3 id="아쉬운점">아쉬운점</h3>
<ul>
<li>시큐리티 로직이 들어가면서 테스트 부분이 권한문제로 굉장히 어려워져서 개발속도가 떨어졌다.</li>
<li>이번 주 목표를 거의 이루지 못했다. 어떤 이유일까?
<ul>
<li>잘 알고 있다고 생각하는 내용들도 정확하게 설명하려고 하니 잘 알지 못하는 경우가 많았다.</li>
</ul>
</li>
<li>나중에 하자 == 안한다</li>
</ul>
<h3 id="아카이빙">아카이빙</h3>
<ul>
<li><a href="https://velog.io/@tedigom/MSA-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-1-MSA%EC%9D%98-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90-3sk28yrv0e">MSA 제대로 이해하기 -(1) MSA의 기본 개념</a></li>
<li><a href="https://brunch.co.kr/@sbhwriter/128">꿈이 후회로 바뀔 때 사람은 늙는다</a></li>
</ul>
<hr />
<h2 id="다음-주-ww23">다음 주 (WW23)</h2>
<h3 id="다음주-목표">다음주 목표</h3>
<ul>
<li>Real MySQL 7장</li>
<li>Spring Security 공식 문서 번역</li>
<li>상품 기능 완성</li>
<li>구매 로직 개발</li>
<li>ipv4 글 내용 보충 - 특수한 주소들</li>
</ul>
<h3 id="가까운-미래의-목표-최대-2달-기한으로-시작까지">가까운 미래의 목표 (최대 2달 기한으로 시작까지)</h3>
<ul>
<li>java-wiki 다시 시작</li>
<li>JPA 독학</li>
<li><a href="https://academy.nomadcoders.co/p/cssnext-css-layout-masterclass">css 기초 공부</a></li>
</ul>Martinoeeen3@gmail.com22주차 회고IPv4 Address2020-05-30T16:30:59+00:002020-05-30T16:30:59+00:00https://smjeon.dev/etc/ipv4<blockquote>
<p>IPv4는 인터넷 프로토콜의 4번째 판이며, 전 세계적으로 사용된 첫 번째 인터넷 프로토콜이다. 과거에 인터넷에서 사용되는 유일한 프로토콜이였으나 오늘날에는 IPv6이 대중화되었다. IETF RFC 7969.(1981년 9월)에 기술되어 있다. (위키백과)</p>
</blockquote>
<p>IP 주소는 32bit address다. IP주소는 유일하며, 2진수 표현으로 했을 때 32bit로 표현하고, <code class="language-plaintext highlighter-rouge">.</code>와 10진수 표기법으로 하면 우리가 흔히 보는 <code class="language-plaintext highlighter-rouge">xxx.xxx.xxx.xxx</code> 형식으로 표현할 수 있다.</p>
<p>32비트 IPv4 주소는 계층적이지만, 두 부분으로 나눌 수 있다.</p>
<ol>
<li>첫 부분은 prefix라고 부르고, network를 정의 한다.</li>
<li>두번째 부분은 suffix라고 부르고, node connection를 정의한다. (Host IP)</li>
</ol>
<p><img src="/assets/img/ipv4/ipv4.png" alt="IPv4" /></p>
<h2 id="classful-addressing">Classful addressing</h2>
<p>IP 주소 공간은 5개의 클래스로 나누어진다.(A, B, C, D, E)</p>
<p><img src="/assets/img/ipv4/classful_address.png" alt="classful address" /></p>
<table>
<thead>
<tr>
<th>Class</th>
<th>Prefixes</th>
<th>구성</th>
<th>First byte</th>
<th>범위</th>
</tr>
</thead>
<tbody>
<tr>
<td>A</td>
<td>n = 8 bits</td>
<td><strong>xxx</strong>.xxx.xxx.xxx</td>
<td>0 ~ 127</td>
<td>1.0.0.1 ~ 126.255.255.254</td>
</tr>
<tr>
<td>B</td>
<td>n = 16 bits</td>
<td><strong>xxx.xxx</strong>.xxx.xxx</td>
<td>128 ~ 191</td>
<td>128.0.0.1 ~ 191.255.255.254</td>
</tr>
<tr>
<td>C</td>
<td>n = 24 bits</td>
<td><strong>xxx.xxx.xxx</strong>.xxx</td>
<td>192 ~ 223</td>
<td>192.0.0.1 ~ 223.255.255.254</td>
</tr>
<tr>
<td>D</td>
<td>X</td>
<td>X</td>
<td>224 ~ 239</td>
<td>224.0.0.0 ~ 239.255.255.255</td>
</tr>
<tr>
<td>E</td>
<td>X</td>
<td>X</td>
<td>240 ~ 255</td>
<td>240.0.0.0 ~ 254.255.255.254</td>
</tr>
</tbody>
</table>
<p><img src="/assets/img/ipv4/ipv4_class.png" alt="ipv4 class" /></p>
<p>이런 주소들은 빠르게 소진되었고, 인터넷에 연결하려고 하는 개인이나 조직을 위해 더이상 가용한 주소가 없다.</p>
<ul>
<li>Class A는 16,777,216개의 IP를 갖는다. 이 규모의 조직은 소수 밖에 없어서 대부분의 주소는 낭비됨</li>
<li>Class B는 중간 사이즈의 기업을 위해 디자인되었으나, 많은 주소들이 사용하지 않는 상태로 남아있다.</li>
<li>Class C는 256개의 IP밖에 없다. 이 주소들을 사용하는 회사들은 너무 적어서 불편함.</li>
<li>Class E는 거의 사용되지 않았다. 전체 클래스가 낭비되고 있음. (미래를 위해 남겨두긴 했지만..)</li>
</ul>
<h2 id="서브넷-슈퍼넷">서브넷, 슈퍼넷</h2>
<p>주소고갈을 완화하기 위해서 Subnetting, Supernetting이 권장되었다.</p>
<ul>
<li>Subnetting에서 Class A나 Class B는 몇개의 subnet으로 나누어진다. 각 서브넷들은 원래 네트워크보다 더 긴 prefix를 갖는다. 즉, 큰 블록을 더 작은 블록으로 나눈다.
<ol>
<li>대부분의 큰 조직들이 자신들이 사용하지 않는 블록을 더 작은 조직을 위해 나누어주는 것을 좋아하지 않았기 때문에 효과적이지 않았다.</li>
</ol>
</li>
<li>Supernetting에서는 여러 Class C 블록들을 모아서 더 큰 블록으로 결합해서 사용한다. 256개보다 많은 아이피를 필요로 하는 조직에 적합했다.
<ol>
<li>이 아이디어는 패킷 라우팅을 더 어렵게 만들어서 효과적이지 않았다.</li>
</ol>
</li>
</ul>
<h2 id="classless-addressing">Classless addressing</h2>
<p>Classless addressing에서는 클래스에 속하지 않는 가변길이 블록이 사용되었다. 1, 2, 4, 128 주소 등의 블록을 가질수 있다.</p>
<p>주소의 prefix는 network를 정의한다. suffix는 node(device)를 정의한다. 그러므로 2^0, 2^1, …, 2^32 주소의 블럭을 가질 수 있다. (작은 prefix는 큰 네트워크, 큰 prefix는 작은 네트워크를 의미한다. - 내부 node의 개수가 많은 것이니까)</p>
<h3 id="제한">제한</h3>
<ol>
<li>블록 내의 주소 수
<ul>
<li>2의 승수개, 2^0, 2^1 …</li>
</ul>
</li>
<li>첫 주소
<ul>
<li>첫 번째 주소는 주소의 개수로 균등하게 나눌 수 있어야한다</li>
</ul>
</li>
<li>Mask
<ul>
<li>주소는 반드시 mask를 가져야한다.</li>
<li>마스크는 *CIDR(classless inter-domain routing) 표기법으로 주어진다. Classless 주소의 포맷은 <code class="language-plaintext highlighter-rouge">X.Y.Z.t/n</code> 형식이고, 슬래시 다음의 n은 블록의 모든 주소에서 동일한 비트 수를 정의한다.</li>
</ul>
</li>
</ol>
<h4 id="cidr">*CIDR</h4>
<blockquote>
<p>사이더(Classless Inter-Domain Routing, CIDR)는 클래스 없는 도메인 간 라우팅 기법으로 1993년 도입되기 시작한, 최신의 IP 주소 할당 방법이다. 사이더는 기존의 IP 주소 할당 방식이었던 네트워크 클래스를 대체하였다. 사이더는 IP 주소의 영역을 여러 네트워크 영역으로 나눌 때 기존방식에 비해 유연성을 더해준다. 특히 다음과 같은 장점이 있다.</p>
</blockquote>
<ol>
<li>급격히 부족해지는 IPv4 주소를 보다 효율적으로 사용하게 해준다.</li>
<li>접두어를 이용한 주소 지정 방식을 가지는 계층적 구조를 사용함으로써 인터넷 광역 라우팅의 부담을 줄여준다.</li>
</ol>
<p>출처: <a href="https://ko.wikipedia.org/wiki/%EC%82%AC%EC%9D%B4%EB%8D%94_(%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9)">위키백과 - 사이더(네트워킹)</a></p>
<h3 id="mask">Mask</h3>
<ul>
<li>Prefix는 주소 범위의 공통 부분의 또다른 이름이다. classful 주소 지정 방식에서 netid와 비슷하다.</li>
<li>Prefix 길이는 CIDR 표기법에서의 n</li>
<li>Classful 주소지정 방식은 classless 주소지정 방식의 특별한 케이스다.</li>
<li>Suffix는 hostid와 비슷하다.</li>
<li>Suffix 길이는 CIDR 표기법에서 suffix(32-n)의 길이다.</li>
</ul>
<table>
<thead>
<tr>
<th>/n</th>
<th>Mask</th>
</tr>
</thead>
<tbody>
<tr>
<td>/1</td>
<td>128.0.0.0</td>
</tr>
<tr>
<td>/2</td>
<td>192.0.0.0</td>
</tr>
<tr>
<td>/3</td>
<td>224.0.0.0</td>
</tr>
<tr>
<td>…</td>
<td>…</td>
</tr>
<tr>
<td>/31</td>
<td>255.255.255.254</td>
</tr>
<tr>
<td>/32</td>
<td>255.255.255.255</td>
</tr>
</tbody>
</table>
<h3 id="mask---예시">Mask - 예시</h3>
<p>classless 주소 167.199.170.82/27 이 있다. 여기서 알 수 있는 것은 무엇일까?</p>
<p><img src="/assets/img/ipv4/mask_1.png" alt="mask example - 1" /></p>
<ol>
<li>이 네트워크 주소의 개수는 2^(32-27) = 2^5 = 32개다</li>
<li>이 네트워크의 첫 주소는 앞의 27비트는 고정, 뒤의 5비트는 0인 값이다.
<ul>
<li>3번째 . 까지가 24비트고 네번째 숫자인 82는 bit로 바꾸면 <strong>010</strong>10010 이므로, 여기서 앞에 표기한 010까지만 고정, 뒤의 숫자는 모두 0인 01000000 = 64 이다.</li>
<li>167.199.170.64/27</li>
</ul>
</li>
<li>이 네트워크의 마지막 주소는 앞의 27비트는 고정, 뒤의 5비트는 1인 값이다.
<ul>
<li>위에서 계산한 방식대로 <strong>010</strong>11111 이므로, 이는 95 이다.</li>
<li>167.199.170.95/27</li>
</ul>
</li>
</ol>
<p><img src="/assets/img/ipv4/mask_1_range.png" alt="mask example - 1 - range" /></p>
<p>위에서 계산한 것을 NOT, AND, OR을 이용해서 구할 수도 있다. (/27은 255.255.255.224이다)</p>
<ol>
<li>네트워크 주소 개수: NOT(mask) + 1 = 0.0.0.31 + 1 = 32개</li>
<li>첫 주소: address AND mask = 167.199.170.82 AND 255.255.255.224 = 167.199.170.64/27</li>
<li>마지막 주소: address OR (NOT mask) = 167.199.170.95/27</li>
</ol>
<h3 id="예시-2">예시 2</h3>
<p>classless 주소지정 방식에서 특정 주소는 한 주소 블록에만 속한 것이 아니다. 예를 들어 주소 230.8.24.56은 많은 블록에 속할 수 있다. 아래처럼..</p>
<table>
<thead>
<tr>
<th>prefix length</th>
<th>block</th>
</tr>
</thead>
<tbody>
<tr>
<td>16</td>
<td>230.8.0.0 ~ 230.8.255.255</td>
</tr>
<tr>
<td>20</td>
<td>230.8.16.0 ~ 230.8.31.255</td>
</tr>
<tr>
<td>26</td>
<td>230.8.24.0 ~ 230.8.24.63</td>
</tr>
<tr>
<td>28</td>
<td>230.8.24.48 ~ 230.8.24.63</td>
</tr>
<tr>
<td>29</td>
<td>230.8.24.56 ~ 230.8.24.63</td>
</tr>
<tr>
<td>31</td>
<td>230.8.24.56 ~ 230.8.24.57</td>
</tr>
</tbody>
</table>
<h3 id="예시-3---aws-보안-그룹">예시 3 - AWS 보안 그룹</h3>
<p><img src="/assets/img/ipv4/aws_sg_example.png" alt="aws example" /></p>
<ol>
<li>121.123.254.0/24</li>
<li>221.148.253.119/32</li>
</ol>
<p>실제 IP로 예시를 들 수는 없지만, AWS 보안그룹에 다음과 같은 인바운드 규칙을 추가했다고 해보자. 그러면 이 규칙은 어떤 IP를 허용하는 것일까?</p>
<p>1번 네트워크</p>
<ol>
<li>이 네트워크 주소의 개수는 2^(32-24) = 2^8 = 256개다.</li>
<li>이 네트워크의 첫 주소는 121.123.254.0/24이다.</li>
<li>이 네트워크의 마지막 주소는 121.123.254.255/24이다.</li>
</ol>
<p>2번 네트워크</p>
<ol>
<li>이 네트워크 주소의 개수는 2^(32-32) = 2^0 = 1개다.</li>
<li>이 네트워크의 첫 주소이자 마지막 주소는 221.148.253.119/32</li>
</ol>
<p>그러니까 prefix의 숫자는 이 네트워크에 속하는 주소가 몇개나 되는지를 결정하는 것이다. 결론적으로</p>
<p>1번 네트워크는 121.123.254.0/24 ~ 121.123.254.255/24의 256개의 주소를 모두 인바운드로 허용한다.</p>
<p>2번 네트워크는 221.148.253.119/32 에 해당하는 IP와 포트로부터 들어오는 요청만 허용한다.</p>
<p>결론적으로는 집에서 개인적으로 프로젝트를 하면서, EC2에 SSH로 접속하기 위해 집의 IP, 22번 포트를 인바운드 규칙에 포함시키고자 한다면 n을 32로 제한하자. 그렇지 않으면<del>(그럴리는 거의 없겠지만)</del> 해당 네트워크에 속한 본인 외의 IP에서도 접속이 가능할 것이다.</p>
<h2 id="network-address">Network Address</h2>
<p>주소 블럭의 첫 주소인 network address는 목적지 네트워크의 패킷을 라우팅하는데 사용되기 때문에 특히 중요하다.</p>
<p>첫 주소 = <code class="language-plaintext highlighter-rouge">(10진수 prefix) * 2^(32-n) = (10진수 prefix) * N</code></p>
<h3 id="network-address---예시">Network Address - 예시</h3>
<p>어떤 조직이 14.24.74.0/24로 시작하는 주소 블럭을 할당 받았다. 해당 조직은 3개의 서브 주소 블럭을 가져야하고, 그래서 세개의 서브넷에 사용할 3개의 서브블록이 필요하다. 만약 서브블록을 30, 40, 120개로 할당해야 한다고 하면 이 서브블록들을 디자인 해보자.</p>
<p><img src="/assets/img/ipv4/network_address.png" alt="network address" /></p>
<ol>
<li>이 블록에는 2^(32-24) = 2^8 = 256 개의 주소가 있다.</li>
<li>첫 주소는 14.24.74.0/24</li>
<li>마지막 주소는 14.24.74.255/24</li>
</ol>
<p><img src="/assets/img/ipv4/network_address_range.png" alt="network address range" /></p>
<p>이 256개의 주소들을 할당하기 위해 일단 가장 큰 120개부터 할당한다.</p>
<ol>
<li>120개를 할당하기 위해서는 128개(2의 승수)를 할당한다.</li>
<li>subnet mask는 n = 25</li>
<li>첫 주소는 14.24.74.0/25, 마지막 주소는 14.24.74.127/25</li>
</ol>
<p><img src="/assets/img/ipv4/address_allocation_1.png" alt="address allocation - 120" /></p>
<p>그 다음 40개를 할당해보자</p>
<ol>
<li>40개를 할당하기 위해서는 64개(2의 승수)를 할당</li>
<li>subnet mask는 n = 26</li>
<li>첫 주소는 14.24.74.128/26, 마지막 주소는 14.24.74.191/26</li>
</ol>
<p><img src="/assets/img/ipv4/address_allocation_2.png" alt="address allocation - 40" /></p>
<p>그 다음 30개를 할당</p>
<ol>
<li>30개를 할당하기 위해서는 32개를 할당</li>
<li>subnet mask는 n = 27</li>
<li>첫 주소는 14.24.74.192/27, 마지막 주소는 14.24.74.223/27</li>
</ol>
<p><img src="/assets/img/ipv4/address_allocation_3.png" alt="address allocation - 30" /></p>
<p>이렇게 할당한다면, 128 + 64 + 32 = 224개의 주소를 할당 했다. 그래서 나머지 32개의 주소가 남는다.</p>
<p><img src="/assets/img/ipv4/address_allocation_remain.png" alt="address allocation - remain" /></p>
<h2 id="특수-주소들">특수 주소들</h2>
<table>
<thead>
<tr>
<th>주소</th>
<th>설명</th>
</tr>
</thead>
<tbody>
<tr>
<td>0.0.0.0/8</td>
<td>자체 네트워크</td>
</tr>
<tr>
<td>0.0.0.0/32</td>
<td>This-host, host가 IP datagram을 보내야 할 때 사용되지만, 소스 주소로 사용될 주소를 모른다.</td>
</tr>
<tr>
<td>10.0.0.0/8</td>
<td>사설 네트워크</td>
</tr>
<tr>
<td>127.0.0.0/8</td>
<td>루프백(자기 자신)</td>
</tr>
<tr>
<td>169.254.0.0/16</td>
<td>링크 로컬</td>
</tr>
<tr>
<td>172.16.0.0/12</td>
<td>사설 네트워크</td>
</tr>
<tr>
<td>192.0.2.0/24</td>
<td>예제 등 문서에서 사용</td>
</tr>
<tr>
<td>192.88.99.0/24</td>
<td>6to4 릴레이 애니캐스트</td>
</tr>
<tr>
<td>192.168.0.0/16</td>
<td>사설 네트워크</td>
</tr>
<tr>
<td>198.18.0.0/16</td>
<td>네트워크 장비 벤치마킹 테스트</td>
</tr>
<tr>
<td>224.0.0.0/4</td>
<td>멀티캐스트</td>
</tr>
<tr>
<td>240.0.0.0/4</td>
<td>reserved</td>
</tr>
<tr>
<td>255.255.255.255/32</td>
<td>Limited-broadcast, router가 모든 device에 데이터를 보내고 싶을 때</td>
</tr>
</tbody>
</table>
<ol>
<li>0.0.0.0/8 - This-Network
<ul>
<li>송신 네트워크 자체</li>
</ul>
</li>
<li>0.0.0.0/32 - This-host
<ul>
<li>위에서 말하는 네트워크 안에 송신 호스트</li>
</ul>
</li>
<li>10.0.0.0/8 - 사설 네트워크(private network)
<ul>
<li>인터넷과 연동되지 않은 사적인 독립 네트워크에 권고되는 주소</li>
</ul>
</li>
<li>127.0.0.0/8 - 루프백
<ul>
<li>통상적으로 디버깅을 목적으로 네트워크 상에서 자기 자신을 나타내는 인터페이스</li>
<li>루프백 인터페이스 IP주소를 갖는 데이터그램은 외부 네트워크쪽으로 이를 전송하지 않음</li>
<li>라우터 등에서는 이를 수신하면, 전송하지 않고 바로 폐기함</li>
</ul>
</li>
<li>169.254.0.0/16 - Link Local
<ul>
<li>DHCP 서버를 찾지 못했을 경우 클라이언트끼리 IP를 할당할 때 쓰기 위해 예약된 주소</li>
</ul>
</li>
<li>172.16.0.0/12
<ul>
<li>위와 동일하게 사설 네트워크에 쓰기 위해 예약된 IP주소, 네트워크는 16개 만들 수 있고(16~31) 각 네트워크의 호스트는 65534개</li>
</ul>
</li>
<li>192.0.0.0/24
<ul>
<li>이 블록은 IANA에 예약되어 있지만, 지금은 필요 없어졌기 때문에, 정상적인 등록이 가능하도록 RIR(Regional Internet Registry)로 할당 될 것이다.</li>
</ul>
</li>
<li>192.0.2.0/24
<ul>
<li>TEST-NET-1</li>
<li>문서와 예제 코드에서 사용되기 위해 예약됨.</li>
<li>public internet에 나타나면 안된다.</li>
<li><code class="language-plaintext highlighter-rouge">example.net</code> 이나 <code class="language-plaintext highlighter-rouge">example.com</code>와 같은 도메인 네임과 같이 쓰인다.</li>
</ul>
</li>
<li>192.88.99.0/24
<ul>
<li>IPv6를 IPv4로 연결할 때 사용하기 위해 예약된 IP주소</li>
</ul>
</li>
<li>192.168.0.0/16
<ul>
<li>위와 동일하게 사설 네트워크를 위해 예약된 주소(공유기에서 가장 많이 쓰는 대역, RFC1918 참조)</li>
</ul>
</li>
<li>198.18.0.0/16
<ul>
<li>네트워크 장비의 벤치마크 테스트를 위해 사용된다.(RFC2544 참조)</li>
</ul>
</li>
<li>198.51.100.0/24
<ul>
<li>TEST-NET-2</li>
</ul>
</li>
<li>203.0.113.0/24
<ul>
<li>TEST-NET-3</li>
</ul>
</li>
<li>224.0.0.0/4 - 멀티캐스트 용
<ul>
<li>멀티캐스트 그룹에 참여하는 구성원들을 확인하기 위한 주소</li>
<li>이 주소로 전송하게되면, 이에 참여하는 여러 호스트들이 이를 동시에 수신하게 된다.</li>
</ul>
</li>
<li>240.0.0.0/4
<ul>
<li>예전에는 예약된 주소였으나, 현재는 모두 사용됨</li>
</ul>
</li>
<li>255.255.255.255/32 - 제한된 브로드캐스트
<ul>
<li>이 브로드캐스트 주소는 라우터를 넘어가는 것이 불가능하다.</li>
</ul>
</li>
</ol>
<h2 id="참고자료">참고자료</h2>
<ul>
<li>약 5년 전 배웠던 컴퓨터 네트워크 강의자료</li>
<li><a href="https://tools.ietf.org/html/rfc3330">rfc3330 - Special-Use IPv4 Address</a></li>
<li><a href="https://tools.ietf.org/html/rfc5737">rfc5737</a></li>
</ul>Martinoeeen3@gmail.comIPv4는 인터넷 프로토콜의 4번째 판이며, 전 세계적으로 사용된 첫 번째 인터넷 프로토콜이다. 과거에 인터넷에서 사용되는 유일한 프로토콜이였으나 오늘날에는 IPv6이 대중화되었다. IETF RFC 7969.(1981년 9월)에 기술되어 있다. (위키백과)