<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>roh-j.log</title>
        <link>https://velog.io/</link>
        <description>roh-j@naver.com</description>
        <lastBuildDate>Sat, 16 Jul 2022 15:34:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. roh-j.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/roh-j" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[VNC로 라즈베리파이 4 원격제어하기 (Ubuntu 22.04)]]></title>
            <link>https://velog.io/@roh-j/VNC%EB%A1%9C-%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4-4-%EC%9B%90%EA%B2%A9%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0-Ubuntu-22.04</link>
            <guid>https://velog.io/@roh-j/VNC%EB%A1%9C-%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4-4-%EC%9B%90%EA%B2%A9%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0-Ubuntu-22.04</guid>
            <pubDate>Sat, 16 Jul 2022 15:34:33 GMT</pubDate>
            <description><![CDATA[<h2 id="1-realvnc-설치">1. RealVNC 설치</h2>
<p><img src="https://velog.velcdn.com/images/roh-j/post/20726f3b-c4c7-4c6b-ac21-ce560acca850/image.png" alt=""></p>
<p><a href="https://www.realvnc.com/en/connect/download/vnc/">https://www.realvnc.com/en/connect/download/vnc/</a></p>
<p>위 링크에서 Raspberry Pi / arm64 를 선택 후 다운로드합니다.</p>
<pre><code class="language-shell">sudo dpkg -i VNC-Server-&lt;버전&gt;-Linux-ARM64.deb</code></pre>
<h2 id="2-realvnc-실행-오류-해결">2. RealVNC 실행 오류 해결</h2>
<p>설치를 진행하다가 아래와 같은 오류가 발생할 수 있습니다.</p>
<h3 id="오류">오류</h3>
<pre><code>Selecting previously unselected package realvnc-vnc-server.
(Reading database ... 188247 files and directories currently installed.)
Preparing to unpack VNC-Server-6.9.1-Linux-ARM64.deb ...
Unpacking realvnc-vnc-server (6.9.1.46706) ...
Setting up realvnc-vnc-server (6.9.1.46706) ...
Updating /etc/pam.d/vncserver
Updating /etc/pam.conf... done

NOTICE: common configuration in /etc/pam.d contains the following modules:
   pam_sss.so
The default vncserver PAM configuration only enables pam_unix. See
`man vncinitconfig&#39; for details on any manual configuration required.

Looking for font path... not found.
/usr/bin/vncserver-x11: error while loading shared libraries: libbcm_host.so.0: cannot open shared object file: No such file or directory
/usr/bin/vncserver-x11: error while loading shared libraries: libbcm_host.so.0: cannot open shared object file: No such file or directory
Installed systemd unit for VNC Server in Service Mode daemon
Start or stop the service with:
  systemctl (start|stop) vncserver-x11-serviced.service
Mark or unmark the service to be started at boot time with:
  systemctl (enable|disable) vncserver-x11-serviced.service

Installed systemd unit for VNC Server in Virtual Mode daemon
Start or stop the service with:
  systemctl (start|stop) vncserver-virtuald.service
Mark or unmark the service to be started at boot time with:
  systemctl (enable|disable) vncserver-virtuald.service

Processing triggers for mailcap (3.70+nmu1ubuntu1) ...
Processing triggers for gnome-menus (3.36.0-1ubuntu3) ...
Processing triggers for desktop-file-utils (0.26-1ubuntu3) ...
Processing triggers for hicolor-icon-theme (0.17-2) ...
Processing triggers for man-db (2.10.2-1) ...
Processing triggers for shared-mime-info (2.1-2) ...</code></pre><h3 id="해결법">해결법</h3>
<pre><code class="language-shell">cd /usr/lib/aarch64-linux-gnu/

sudo ln libvcos.so /usr/lib/libvcos.so.0
sudo ln libvchiq_arm.so /usr/lib/libvchiq_arm.so.0
sudo ln libbcm_host.so /usr/lib/libbcm_host.so.0</code></pre>
<pre><code class="language-shell">sudo nano /etc/gdm3/custom.conf

WaylandEnable=false # 주석을 해제합니다.</code></pre>
<pre><code class="language-shell">sudo systemctl enable vncserver-virtuald.service
sudo systemctl enable vncserver-x11-serviced.service
sudo systemctl start vncserver-virtuald.service
sudo systemctl start vncserver-x11-serviced.service

sudo reboot</code></pre>
<h2 id="3-hdmi가-연결되어-있지-않을-때-vnc-화면이-안뜨는-문제">3. HDMI가 연결되어 있지 않을 때, VNC 화면이 안뜨는 문제</h2>
<p>RealVNC는 화면을 미러링 하는 방식으로, 본체에 디스플레이 출력이 존재해야 정상적으로 VNC 화면이 출력됩니다.
소프트웨어적으로 디스플레이 출력을 만듦으로써 해당 문제를 해결할 수 있습니다.</p>
<pre><code class="language-shell">sudo apt update
sudo apt install xserver-xorg-video-dummy
sudo cp /etc/X11/vncserver-virtual-dummy.conf /etc/X11/xorg.conf
sudo reboot</code></pre>
<h2 id="4-dummy-디스플레이-추가-후-hdmi-출력이-안될-때">4. Dummy 디스플레이 추가 후, HDMI 출력이 안될 때</h2>
<p>Dummy 디스플레이 설정 이후에는 Dummy 디스플레이로 출력되기 때문에, 다시 HDMI로 디스플레이를 출력하기 위해서는 Dummy 디스플레이 설정을 해제하여야 합니다!</p>
<pre><code class="language-shell">sudo mv /etc/X11/xorg.conf /etc/X11/xorg.conf.dummy
sudo reboot</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[세션과 JWT]]></title>
            <link>https://velog.io/@roh-j/%EC%84%B8%EC%85%98%EA%B3%BC-JWT</link>
            <guid>https://velog.io/@roh-j/%EC%84%B8%EC%85%98%EA%B3%BC-JWT</guid>
            <pubDate>Sat, 16 Jul 2022 15:29:15 GMT</pubDate>
            <description><![CDATA[<h1 id="세션과-jwt">세션과 JWT</h1>
<p>JWT는 세션의 한계를 극복하기 위해 만들어졌습니다. 그렇다면 세션은 무엇이고, 어떠한 한계가 있을까요? 세션과 JWT를 아래와 같은 목차로 알아보도록 하겠습니다.</p>
<ol>
<li>세션</li>
<li>Embedded Tomcat의 세션</li>
<li>세션의 한계와 JWT 탄생</li>
<li>JWT ( +JWT 구조)</li>
</ol>
<h2 id="1-세션">1. 세션</h2>
<p>HTTP는 클라이언트와 서버의 통신이 독립적으로 이루어집니다. 즉, 이전의 요청에 무관한 응답을 보내는데 이러한 특성 때문에 <strong>Stateless</strong>한 프로토콜이라고 합니다.</p>
<p>로그인 기능을 구현하기 위해서는 각각의 요청에 대해서 서버가 사용자를 식별할 수 있어야 합니다. 하지만 HTTP의 <strong>Stateless</strong> 특성으로 인해 사용자를 식별할 수 없습니다. 이를 해결하기 위해 세션이 탄생되었습니다.</p>
<h2 id="2-embedded-tomcat의-세션">2. Embedded Tomcat의 세션</h2>
<p>세션은 앞서 설명과 같이 사용자를 식별하기 위해 사용됩니다. Embedded Tomcat이 사용자를 식별하는 과정을 통해 세션이 동작하는 방식을 이해할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/8e48982f-fb3d-4cb2-833c-a842ea4d8eaf/image.png" alt=""></p>
<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody><tr>
<td>1.</td>
<td>클라이언트가 인증을 시도한다.</td>
</tr>
<tr>
<td>2.</td>
<td>인증된 사용자라면 Embedded Tomcat은 고유한 세션 ID (JSESSIONID)를 클라이언트에게 전달한다.</td>
</tr>
<tr>
<td>3.</td>
<td>클라이언트는 JSESSIONID를 쿠키, 로컬 스토리지에 저장한다.</td>
</tr>
<tr>
<td>4.</td>
<td>클라이언트는 서버에 요청 시 JSESSIONID를 함께 전달한다.</td>
</tr>
<tr>
<td>5.</td>
<td>서버에 저장되어 있는 JSESSIONID와 클라이언트가 보낸 JSESSIONID를 비교해 클라이언트의 상태를 확인한다.</td>
</tr>
</tbody></table>
<h2 id="3-세션의-한계와-jwt-탄생">3. 세션의 한계와 JWT 탄생</h2>
<p>만약, 여러개의 서버를 증설하면서 로드밸런서를 이용해 트래픽을 분산하였을 경우, 각각 서버는 세션의 정보를 공유하고 있지 않기 때문에 사용자를 식별할 수 없는 문제가 발생합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/47527b9f-4887-4128-b8bf-1c7cc2af1d92/image.png" alt=""></p>
<p>[트래픽 분산 시 사용자를 식별할 수 없음]</p>
<p>이를 해결하기 위해서는 서버와 공유하고 있는 별도의 세션 저장소를 구축하여야 하는데, 이 때 세션 저장소에 과도한 부하가 발생할 수 있습니다. (세션의 한계) JWT는 이러한 세션의 한계를 극복하고자 탄생되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/b65580b8-6fd2-413b-8f65-54fd09cfbb89/image.png" alt=""></p>
<p>[세션 저장소를 공유함으로써 문제를 해결할 수 있음]</p>
<h2 id="4-jwt">4. JWT</h2>
<p><img src="https://velog.velcdn.com/images/roh-j/post/d274d5a5-7edc-43c7-93e8-81344e2a93fc/image.png" alt=""></p>
<p>JWT는 매번 저장소에서 사용자의 식별 정보를 조회해야 하는 세션과 달리 별도의 저장소 조회 없이 토큰 그 자체로 인증, 인가를 구현할 수 있습니다. 다시말해 각각의 서버가 하나의 저장소를 공유하지 않아도 자체적으로 사용자를 식별할 수 있습니다.</p>
<h3 id="41-jwt-구조">4.1 JWT 구조</h3>
<p>어떻게 JWT는 토큰 그 자체로 인증, 인가를 구현할 수 있을까요? JWT의 구조와 원리를 알아봅시다. JWT는 Header, Payload, Signature 로 구성되어 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/802a445f-593d-4f2a-841f-e5c2fe148a62/image.png" alt=""></p>
<h4 id="header">Header</h4>
<p>먼저, Header는 JWT가 어떤 해시 알고리즘으로 토큰을 만들고 있는지에 대한 정보를 담습니다.</p>
<ul>
<li>alg: 해시 알고리즘</li>
<li>typ: 토큰의 타입</li>
</ul>
<h4 id="payload">Payload</h4>
<p>그 다음 Payload에 사용자를 식별할 수 있는 정보를 담습니다. Payload에 담겨있는 각각의 정보를 클레임이라 부릅니다. 클레임은 Registered, Public, Private가 있습니다.</p>
<h5 id="registered-클레임">Registered 클레임</h5>
<p>토큰 자체의 정보를 담는 클레임입니다. Registered 클레임은 이미 정해진 이름이 있으며 모두 Optional입니다.</p>
<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody><tr>
<td>iss</td>
<td>(issuer) 토큰 발급자</td>
</tr>
<tr>
<td>sub</td>
<td>(subject) 토큰 제목</td>
</tr>
<tr>
<td>aud</td>
<td>(audience) 토큰 대상자</td>
</tr>
<tr>
<td>exp</td>
<td>(expiration) 토큰 만료 시간</td>
</tr>
<tr>
<td>nbf</td>
<td>(Not Before)을 의미하고 정해진 시간에 토큰을 활성화하기 위해 사용합니다.</td>
</tr>
<tr>
<td>iat</td>
<td>(issued at) 토큰이 발급된 시간</td>
</tr>
<tr>
<td>jti</td>
<td>(JWT ID) JWT 고유 식별자</td>
</tr>
</tbody></table>
<h5 id="public-클레임">public 클레임</h5>
<p>토큰의 충돌을 막기 위해 담는 클레임입니다.</p>
<h5 id="private-클레임">Private 클레임</h5>
<p>서버와 클라이언트 간의 사용자 식별을 위해 담는 클레임입니다.
정해진 이름이 없으며 서버와 클라이언트간의 약속으로 클레임을 만듭니다.</p>
<h4 id="signature">Signature</h4>
<p>Signature는 JWT의 핵심으로 Signature는 부인 방지, 무결성, 사용자 식별을 담당합니다. 이로 인해 인증, 인가를 구현할 수 있습니다.</p>
<h5 id="signature-원리">Signature 원리</h5>
<p>JWT 자체로 인증, 인가를 구현할 수 있었던 비밀은 Signature 때문입니다.
Signature는 토큰 그 자체로 변조되지 않고 유효한 토큰임을 증명합니다. Signature는 어떻게 동작하는 것일까요?</p>
<ol>
<li>Header + Payload + 서버만 알고 있는 Secret Key 를 묶어 Header에 선언되어 있는 해시 알고리즘으로 해싱을 진행합니다.</li>
<li>위에서 해싱된 결과와 JWT Signature를 비교해 일치하는지 확인합니다.</li>
<li>일치한다면 토큰이 유효합니다.</li>
</ol>
<p>Signature로 토큰이 유효한지만을 증명합니다. 서버는 Payload에 담겨있는 토큰 발급자, 토큰 대상자, 토큰 만료 시간 등을 최종 확인해 인증, 인가를 합니다.</p>
<h3 id="정리">정리</h3>
<p>세션의 어떠한 한계로 인해 JWT가 탄생되었는지 또 JWT는 어떻게 세션의 한계를 극복해 별도의 저장소 없이 유효함을 증명할 수 있었는지에 대해 알아보았습니다. JWT를 이해하는데 많은 도움이 되었으면 합니다.</p>
<h3 id="레퍼런스">레퍼런스</h3>
<ul>
<li><a href="https://brunch.co.kr/@springboot/491">https://brunch.co.kr/@springboot/491</a></li>
<li><a href="https://velopert.com/2389">https://velopert.com/2389</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[CORS (Cross-Origin Resource Sharing)가 무엇일까?]]></title>
            <link>https://velog.io/@roh-j/CORS-Cross-Origin-Resource-Sharing%EA%B0%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@roh-j/CORS-Cross-Origin-Resource-Sharing%EA%B0%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Thu, 29 Apr 2021 05:18:27 GMT</pubDate>
            <description><![CDATA[<h1 id="cors-cross-origin-resource-sharing">CORS (Cross-Origin Resource Sharing)</h1>
<h2 id="1-cors-개요">1. CORS 개요</h2>
<p>CORS (교차 출처 자원 공유)는 보안 상의 이유로, JavaScript에서 보내는 교차 출처 (자신과 다른 출처) HTTP 요청을 제한하기 위한 정책입니다. 대다수 브라우저에는 CORS가 적용되어 있는데, 이는 자신의 출처와 동일한 리소스만 불러올 수 있도록 하여, 악의적인 자원 접근과 탈취를 막기 위함입니다.</p>
<h2 id="2-cors-에러">2. CORS 에러</h2>
<p>웹을 개발하면서 아래와 같은 에러 메시지를 한번쯤 보셨을 것 같습니다. CORS 에러가 발생하는 이유는 [CORS 개요]에서 소개했듯이, JavaScript 코드 상에서 동일한 출처가 아닌 곳에서 요청을 하였기 때문입니다. 아래의 에러 내용을 살펴보면 &quot;<a href="http://localhost:3000">http://localhost:3000</a> 출처에서 보낸 <a href="https://www.example.com">https://www.example.com</a> 의 자원 접근 요청을 CORS 정책에 의해 차단되었습니다.&quot; 라는 문구를 확인할 수 있습니다.</p>
<blockquote>
<p>Access to fetch at ‘<a href="http://www.example.com%E2%80%99">http://www.example.com’</a> from origin ‘<a href="http://localhost:3000%E2%80%99">http://localhost:3000’</a> has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.</p>
</blockquote>
<pre><code class="language-js">/*
 * main.js
 * local에서 main.js가 http://www.example.com에 데이터를 요청함
 * 출처가 다르기 때문에 CORS 에러 발생
 */

$.get(&quot;http://www.example.com&quot;, function (data) {
  alert(&quot;Data Loaded: &quot; + data);
});</code></pre>
<h2 id="3-cors-동작-과정">3. CORS 동작 과정</h2>
<p>CORS는 브라우저에서 이루어집니다. 때문에, 브라우저의 옵션을 수정하면 CORS를 회피할 수 있습니다. 예를 들어, 크롬 브라우저에서 --disable-web-security 옵션을 추가하면 CORS 에러 없이 여러 출처의 리소스에 대해 접근할 수 있습니다.</p>
<p>그렇다면, 브라우저는 어떻게 CORS를 동작하고 있을까요? CORS는 Preflight Request, Simple Request 두 가지 방식으로 동작됩니다. Preflight Request, Simple Request에 대해 한번 알아봅시다.</p>
<h3 id="31-preflight-request">3.1. Preflight Request</h3>
<p>본 요청을 보내기 이전에 보내는 예비 요청을 Preflight라고 부릅니다. Preflight는 HTTP의 OPTIONS 메소드를 이용해 서버에 보내집니다. 이러한 예비 요청을 통해 본 요청을 보내기 전, CORS를 위반하고 있는지를 확인합니다. Preflight에 대한 서버 응답이 안전하다면 브라우저는 본 요청을 서버에 다시 보냅니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/4a667342-e896-4e7d-b480-9edce1eb0c42/image.png" alt=""></p>
<p>[그림 1] Preflight Request 동작</p>
<h3 id="32-simple-request">3.2. Simple Request</h3>
<p>Simple Request (단순 요청)는 Preflight Request 방식과 달리 예비 요청을 보내지 않습니다. 대신 Access-Control-Allow-Origin 헤더를 이용해 CORS 위반 여부를 검사합니다.
클라이언트의 요청에 대해 서버는 Access-Control-Allow-Origin 헤더와 함께 응답합니다. Access-Control-Allow-Origin 헤더에는 CORS 정책이 담겨있고, 브라우저는 Access-Control-Allow-Origin 헤더의 내용을 토대로 정책을 위반했는지 확인합니다. 이상이 없다면 본 요청을 서버에 보냅니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/2df2148c-1459-49df-8359-96ff5d7f0457/image.png" alt=""></p>
<p>[그림 2] Simple Request 동작</p>
<h3 id="4-cors-해결-방법">4. CORS 해결 방법</h3>
<p>CORS는 서버, 클라이언트 한쪽만 의 적용으로 해결할 수 있습니다. 서버, 클라이언트 각각에서 CORS를 처리하는 대표적인 방법 두 가지를 소개합니다. (다만, 서버에서 CORS 정책을 제어하는 것을 권장합니다.)</p>
<h4 id="41-crossorigin-어노테이션-이용-api">4.1. @CrossOrigin 어노테이션 이용 (API)</h4>
<p>스프링 기준, 스프링 4.2 이상부터 지원되는 @CrossOrigin을 이용하여 CORS 정책을 설정할 수 있습니다. Controller에 어노테이션을 추가하면 적용됩니다.</p>
<ul>
<li>@CrossOrigin : 모든 도메인, 모든 요청 방식에 대해 허용</li>
<li>@CrossOrigin(origins = &quot;<a href="http://www.example1.com">http://www.example1.com</a>, <a href="http://www.example2.com&quot;">http://www.example2.com&quot;</a>)<ul>
<li><a href="http://www.example1.com">http://www.example1.com</a>, <a href="http://www.example2.com">http://www.example2.com</a> 도메인에 대해서만 허용</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@CrossOrigin
public class ProjectController {

    @GetMapping(value=&quot;/projects/list&quot;, produces = &quot;application/hal+json&quot;)
    public ResponseEntity&lt;List&lt;Project&gt;&gt; getProjectList(@ModelAttribute KeystoneProject project){
        return ResponseEntity.ok(projectService.getProjectList(project));
    }

}</code></pre>
<h4 id="42-프록시-이용-client">4.2. 프록시 이용 (Client)</h4>
<p>CORS 에러는 근본적으로 자신과 다른 출처에서 HTTP 요청을 하였을 때 발생한다는 점을 기억해 보면, 자신의 출처를 프록싱하면 CORS를 회피할 수 있을 것 같습니다. 클라이언트는 이 같은 아이디어로 CORS 에러를 해결합니다. 프록싱을 통해 자신의 출처를 CORS가 허용되는 출처로 바꿔 HTTP 요청을 하는 것이죠. 프록싱을 하면 최소한의 설정으로 CORS를 회피할 수 있어 개발 시 이용되곤 합니다.</p>
<h3 id="레퍼런스">레퍼런스</h3>
<ul>
<li><a href="https://medium.com/@buddhiv/what-is-cors-or-cross-origin-resource-sharing-eccbfacaaa30">https://medium.com/@buddhiv/what-is-cors-or-cross-origin-resource-sharing-eccbfacaaa30</a></li>
<li><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/CORS">https://developer.mozilla.org/ko/docs/Web/HTTP/CORS</a></li>
<li><a href="https://evan-moon.github.io/2020/05/21/about-cors/#preflight-request">https://evan-moon.github.io/2020/05/21/about-cors/#preflight-request</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[VirtualBox를 이용해 OpenStack 설치하기 (네트워크 구성편)]]></title>
            <link>https://velog.io/@roh-j/VirtualBox%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-OpenStack-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B5%AC%EC%84%B1%ED%8E%B8</link>
            <guid>https://velog.io/@roh-j/VirtualBox%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-OpenStack-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B5%AC%EC%84%B1%ED%8E%B8</guid>
            <pubDate>Tue, 30 Mar 2021 00:59:14 GMT</pubDate>
            <description><![CDATA[<h2 id="virtualbox-기본-설정">VirtualBox 기본 설정</h2>
<h3 id="virtualbox-다운로드">VirtualBox 다운로드</h3>
<p><img src="https://velog.velcdn.com/images/roh-j/post/05ea748d-874c-454d-8c5f-60a1a377826c/image.png" alt=""></p>
<p><a href="https://www.virtualbox.org/wiki/Downloads">https://www.virtualbox.org/wiki/Downloads</a> 에서 PC의 OS에 맞는 VirtualBox 설치 파일을 다운로드 받습니다. (본 글은 Ubuntu 18.04 기준으로 설명하며, 설치 과정은 동일합니다!)</p>
<p>VirtualBox 6.1.18 platform packages &gt; Linux distributions 링크를 클릭합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/3d55db46-062b-4567-b7da-2bcb62508fb0/image.png" alt=""></p>
<p>Ubuntu 18.04 / 18.10 / 19.04 링크를 클릭해 설치파일을 다운로드합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/32e13b0f-0b39-4cc3-b463-fe219318fce8/image.png" alt=""></p>
<p>VirtualBox 6.1.18 Oracle VM VirtualBox Extension Pack &gt; All supported platforms 링크를 클릭합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/2daef73a-efc7-4f3e-b4d9-49b644a140c7/image.png" alt=""></p>
<p>VirtualBox 설치 파일과 Extension Pack 파일이 정상적으로 다운로드 되었는지 확인합니다.</p>
<h3 id="virtualbox-설치">VirtualBox 설치</h3>
<p><img src="https://velog.velcdn.com/images/roh-j/post/58d3e87d-97a1-48b2-9081-062fabcdce87/image.png" alt=""></p>
<p>virtualbox-6.1_6.1.18-142142_Ubuntu_bionic_amd64.deb 파일을 설치합니다.</p>
<h3 id="virtualbox-extension-설치">VirtualBox Extension 설치</h3>
<p><img src="https://velog.velcdn.com/images/roh-j/post/7d4bebcc-076e-42db-9fce-f618bb659e55/image.png" alt=""></p>
<p>VirtualBox 메인 화면에서 File &gt; Preferences 를 클릭합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/9bf978e8-31fa-479f-8a7e-d0fd1a233cd0/image.png" alt=""></p>
<p>Extensions 탭 클릭 후 Version 우측에 있는 [+] 모양의 아이콘을 클릭합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/dc117b5c-acc1-434f-a0d0-d53842e6cb04/image.png" alt=""></p>
<p>Oracle_VM_VirtualBox_Extension_Pack-6.1.18.vbox-extpack 파일을 선택합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/e87b819e-ee36-462f-949b-4e6e53a25f96/image.png" alt=""></p>
<p>Install을 진행합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/01783a52-b0e3-4687-bc50-4358a6587fa3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/c145cbbd-0495-4902-a957-80f3361d34b9/image.png" alt=""></p>
<p>성공적으로 Extension이 추가되었는지 확인합니다.</p>
<h3 id="virtualbox-vm-생성-ubuntu-1804">VirtualBox VM 생성 (Ubuntu 18.04)</h3>
<p><img src="https://velog.velcdn.com/images/roh-j/post/b6bdb4aa-43a4-4b9f-9ec4-984730d8d2a4/image.png" alt=""></p>
<p>Machine &gt; New 클릭합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/13b2ae45-ba27-4b34-aae4-94623281f156/image.png" alt=""></p>
<pre><code>Name: OpenStack (임의로 작성 가능)
Type: Linux
Version: Ubuntu (64-bit)</code></pre><p>으로 설정하고 [Next] 를 클릭합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/58520298-2e76-40ca-aef2-eb58ca95b7ad/image.png" alt=""></p>
<p>Memory size는 최소 2 GB 이상 설정을 권장합니다.
Memory size를 설정하고 [Next] 를 클릭합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/1a0cdbcf-0e6f-4495-9015-af17a38ba445/image.png" alt=""></p>
<p>가상 하드 드라이브를 만듭니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/d1d1f37a-4592-47f3-b92c-f9f2856d2b02/image.png" alt=""></p>
<p>가상 하드 디스크 파일 타입을 VDI(VirtualBox Disk Image)로 설정하고 [Next]를 클릭합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/ce56afd5-32b8-4c34-91d6-1e5f456f67f3/image.png" alt=""></p>
<ul>
<li>Dynamically allocated (동적 할당)</li>
<li>Fixed size (고정 할당)</li>
</ul>
<p>두가지 방식으로 가상 하드 디스크를 만들 수 있습니다.</p>
<p>동적할당의 경우 실제 사용량 만큼만 파일이 커지기 때문에 호스트 컴퓨터 (VirtualBox가 설치된 컴퓨터) 용량을 낭비없이 효율적으로 사용할 수 있는 장점이 있지만, 고정 크기 방식에 비해 속도가 떨어지는 단점이 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/310fc3ae-1296-4e36-ad8a-51159656ed2d/image.png" alt=""></p>
<p>가상 하드 디스크의 크기를 입력하고 생성합니다. (최소 8 GB 이상)</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/7ce2fe7f-858c-4d57-86e1-57188379a107/image.png" alt=""></p>
<p>Storage &gt; Contoller: IDE
IDE Secondary Device 0: [Optical Drive] Empty 를 클릭합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/fc1b349f-0f49-49f7-9e9e-63981f3ccab0/image.png" alt=""></p>
<p>Ubuntu 18.04 Server 이미지를 선택합니다.</p>
<h2 id="virtualbox-네트워크-설정">VirtualBox 네트워크 설정</h2>
<h3 id="virtualbox-host-network-설정">VirtualBox Host Network 설정</h3>
<p><img src="https://velog.velcdn.com/images/roh-j/post/a383536a-71b7-4d81-b03f-34fb5ab72ab5/image.png" alt=""></p>
<p>File &gt; Host Network Manager를 클릭합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/77133363-6378-447a-8d39-1656388df54b/image.png" alt=""></p>
<p>새로운 Host Network를 구성하기 위해 Create를 클릭합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/7a5c1ad3-d45b-4242-96b3-ee99f57b03db/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/e4c05594-024b-41f8-a34e-454339140129/image.png" alt=""></p>
<pre><code>IPv4 Address: 192.168.56.1
IPv4 Network Mask: 255.255.255.0</code></pre><p>으로 설정합니다.</p>
<blockquote>
<p>[참고]</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/557fa81a-3755-45f0-aa1c-909b0a438eea/image.png" alt=""></p>
<p>(VirtualBox가 설치되어 있는 호스트 컴퓨터에서 어댑터 정보를 확인해보면 VirtualBox Host-Only Network 어댑터로 IPv4가 192.168.56.1 로 설정되어 있는 것을 확인할 수 있습니다.)</p>
</blockquote>
<h3 id="virtualbox-adapter-설정">VirtualBox Adapter 설정</h3>
<p>OpenStack은 내부 통신을 위한 Internal용 NIC, 외부 통신을 위한 External용 NIC, 최소 2개 이상의 물리 NIC을 필요로 합니다. VirtualBox에서 제공하는 Adapter 설정을 통해 VM에 여러개의 물리 NIC이 연결된 것처럼 구성할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/81e1bfa4-8a41-4b88-b90f-4973cec2436a/image.png" alt=""></p>
<pre><code>[VirtualBox Adapter]
Adapter1: 내부 통신용 NIC
Adapter2: 외부 통신용 NIC (Bridge)
Adapter3: NAT</code></pre><pre><code>[Ubuntu]
enp0s3: (host-only) Internal
(Adapter1이 Ubuntu VM에서 enp0s3로 설정됨)

enp0s8: (Bridge) External
(Adapter2가 Ubuntu VM에서 enp0s8로 설정됨)

enp0s9: (NAT)
(Adapter3가 Ubuntu VM에서 enp0s9로 설정됨)</code></pre><p><img src="https://velog.velcdn.com/images/roh-j/post/73934ab9-0880-4779-9c10-234fc7696d97/image.png" alt=""></p>
<p>Name: Host Network Manager에서 만들었던 설정 이름으로 선택합니다.
Promiscuous Mode: Allow All로 설정합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/56a5ec31-b3e1-4bc6-b015-441dda6e3277/image.png" alt=""></p>
<p>Promiscuous Mode: Allow All로 설정합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/fb15fe5d-8146-46ab-b273-d3a659022a3d/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OpenStack Horizon은 어떻게 사용자를 인증할까?]]></title>
            <link>https://velog.io/@roh-j/OpenStack-Horizon%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%EC%9E%90%EB%A5%BC-%EC%9D%B8%EC%A6%9D%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@roh-j/OpenStack-Horizon%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%EC%9E%90%EB%A5%BC-%EC%9D%B8%EC%A6%9D%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Thu, 18 Feb 2021 14:11:23 GMT</pubDate>
            <description><![CDATA[<p>Horizon은 클라우드 관리자와 사용자들이 다양한 OpenStack 자원과 서비스를 관리할 수 있는 웹 인터페이스입니다. OpenStack Horizon은 어떻게 사용자를 인증, 인가하고 Keystone과 연동할까요? 이에 대한 과정을 시퀀스 다이어그램과 코드를 통해 알아봅시다.</p>
<h1 id="openstack-horizon-인증-과정-요약">OpenStack Horizon 인증 과정 요약</h1>
<table>
<thead>
<tr>
<th></th>
<th>과정</th>
</tr>
</thead>
<tbody><tr>
<td>1.</td>
<td>Horizon은 Django의 인증 로직을 커스터마이징하여 Keystone과 연동한다.</td>
</tr>
<tr>
<td>2.</td>
<td>Keystone에서는 크게 2가지 유형의 토큰을 발급한다. (Unscoped 토큰, Scoped 토큰)</td>
</tr>
<tr>
<td>3.</td>
<td>Unscoped 토큰은 Keystone이 인증된 사용자에 대해 발급하는 신분 증명용 토큰으로 Scoped 토큰을 발급받기 위해 사용된다.</td>
</tr>
<tr>
<td>4.</td>
<td>Scoped 토큰을 발급받는 이유는 Unscoped 토큰만으로는 서비스(e.g. Nova)에 요청할 수 없기 때문이다.</td>
</tr>
<tr>
<td>5.</td>
<td>Scoped 토큰은 클라이언트(e.g. 유저, Nova, Glance)가 실행할 수 있는 범위가 담겨있는 토큰으로 서비스(e.g. Nova)에 요청할 수 있다.</td>
</tr>
<tr>
<td>6.</td>
<td>결국, Keystone 연동에서의 핵심은 Scoped 토큰을 발급받는 것이다.</td>
</tr>
<tr>
<td>7.</td>
<td>사용자가 인증되면 Horizon은 Unscoped 토큰을 세션에 저장한다. (추후 Scoped 토큰을 발급 받기 위해)</td>
</tr>
<tr>
<td>8.</td>
<td>세션에 저장된 Unscoped 토큰을 이용해 Scoped 토큰을 발급 받는다.</td>
</tr>
<tr>
<td>9.</td>
<td>발급 받은 Scoped 토큰과 함께 서비스(e.g. Nova)에 요청한다.</td>
</tr>
</tbody></table>
<h1 id="사용자-로그인-시퀀스-다이어그램">사용자 로그인 (시퀀스 다이어그램)</h1>
<p>Horizon은 Django 인증 시스템을 통해 사용자를 인증합니다. Keystone과 연동하기 위해 Custom 인증이 추가되어 있는데, 이를 <code>settings.py</code>의 <code>AUTHENTICATION_BACKENDS</code> 옵션을 통해 확인할 수 있습니다. Custom 인증의 핵심은 Keystone에서 Unscoped 토큰을 발급받는 것으로, <code>openstack_auth.backend.KeystoneBackend</code> 모듈에서 진행합니다. Unscoped 토큰이 성공적으로 발급되면 해당 토큰을 Django Session에 저장합니다.</p>
<p>Unscoped 토큰:</p>
<ul>
<li>Keystone이 인증된 사용자에 대해 발급하는 신분 증명용 토큰</li>
<li>Scoped 토큰을 발급받기 위해 사용</li>
<li>서비스(e.g. Nova)에 요청 할 수 없음</li>
</ul>
<p>Scoped 토큰:</p>
<ul>
<li>클라이언트(e.g. 유저, Nova, Glance)가 실행할 수 있는 범위가 담겨있는 토큰</li>
<li>범위 내의 서비스(e.g. Nova)에 요청 할 수 있음</li>
</ul>
<p>아래는 이러한 사용자 로그인 과정을 시퀀스 다이어그램으로 표현하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/3721d779-30a2-4aba-928c-1506e64f2b04/image.png" alt=""></p>
<p>[그림 1] 사용자 로그인 시퀀스 다이어그램</p>
<h1 id="코드로-알아보자">코드로 알아보자</h1>
<p><img src="https://velog.velcdn.com/images/roh-j/post/78fa8430-ec15-4ee2-918c-76a07647bc00/image.png" alt=""></p>
<p>[그림 2] Horizon 콘솔 로그인</p>
<p>Horizon 콘솔에서 User Name과 Password를 입력하고 [Sign In] 버튼을 누르면</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/95fdf44f-2fff-44f3-9c49-c3152e472e69/image.png" alt=""></p>
<p>[그림 3] POST 파라미터</p>
<p>POST Method로 파라미터와 함께 <a href="http://localhost/auth/login">http://localhost/auth/login</a> URL을 요청합니다.</p>
<h2 id="horizon-back-end">Horizon (Back-end)</h2>
<h4 id="horizonopenstack_authurlspy">horizon/openstack_auth/urls.py</h4>
<pre><code class="language-python">urlpatterns = [
  url(r&quot;^login/$&quot;, views.login, name=&#39;login&#39;),

  ...

]</code></pre>
<h4 id="horizonopenstack_dashboardsettingspy">horizon/openstack_dashboard/settings.py</h4>
<pre><code class="language-python">AUTHENTICATION_BACKENDS = (&#39;openstack_auth.backend.KeystoneBackend&#39;,)</code></pre>
<p><code>AUTHENTICATION_BACKENDS</code> 옵션을 통해 Django 인증 로직을 커스터마이징할 수 있습니다.</p>
<h4 id="horizonopenstack_authviewspy">horizon/openstack_auth/views.py</h4>
<pre><code class="language-python">from django.contrib.auth import views as django_auth_views

...

@sensitive_post_parameters()
@csrf_protect
@never_cache
def login(request):

  ...

  try:
    res = django_auth_views.LoginView.as_view(
      template_name=template_name,
      redirect_field_name=auth.REDIRECT_FIELD_NAME,
      form_class=form,
      extra_context=extra_context,
      redirect_authenticated_user=False)(request)
  except exceptions.KeystonePassExpiredException as exc:
    res = django_http.HttpResponseRedirect(
      reverse(&#39;password&#39;, args=[exc.user_id]))
    msg = _(&quot;Your password has expired. Please set a new password.&quot;)
    res.set_cookie(&#39;logout_reason&#39;, msg, max_age=10)</code></pre>
<p><code>django_auth_views.LoginView.as_view()</code> 를 호출하면 사용자 인증 화면이 표시됩니다. 그 후, 사용자가 작성한 Form을 Submit 하게되면 Django 인증 시스템이 실행됩니다. (<code>AUTHENTICATION_BACKENDS</code> 옵션에 따라 Custom 인증인 <code>openstack_auth.backend.KeystoneBackend</code> 모듈을 호출)
<code>openstack_auth.backend.KeystoneBackend</code> 모듈의 핵심은 Unscoped 토큰을 Keystone에게서 발급 받는 것으로, 모듈이 호출되면 Unscoped 토큰 발급 요청을 진행합니다.</p>
<h4 id="horizonopenstack_authbackendpy">horizon/openstack_auth/backend.py</h4>
<pre><code class="language-python"># TODO(stephenfin): Subclass &#39;django.contrib.auth.backends.BaseBackend&#39; once we
# (only) support Django 3.0
class KeystoneBackend(object):
  &quot;&quot;&quot;Django authentication backend for use with ``django.contrib.auth``.&quot;&quot;&quot;

  ...

  def authenticate(self, request, auth_url=None, **kwargs):
    &quot;&quot;&quot;Authenticates a user via the Keystone Identity API.&quot;&quot;&quot;
    LOG.debug(&#39;Beginning user authentication&#39;)

    if not auth_url:
      auth_url = settings.OPENSTACK_KEYSTONE_URL

    auth_url, url_fixed = utils.fix_auth_url_version_prefix(auth_url)
    if url_fixed:
      LOG.warning(&quot;The OPENSTACK_KEYSTONE_URL setting points to a v2.0 &quot;
                  &quot;Keystone endpoint, but v3 is specified as the API &quot;
                  &quot;version to use by Horizon. Using v3 endpoint for &quot;
                  &quot;authentication.&quot;)

    plugin, unscoped_auth = self._get_auth_backend(auth_url, **kwargs)

    ...</code></pre>
<p>Custom 인증 모듈의 클래스에는 authenticate 메서드가 있어야 합니다. Django 인증 시스템이 Custom 인증을 실행할 때 authenticate 메서드를 호출합니다.</p>
<p>토큰 발급과 관련된 모듈은 2개로 아래와 같습니다.</p>
<p>keystoneauth1:</p>
<ul>
<li>OpenStack 기반 클라우드 인증을 위한 도구</li>
<li>오픈스택 인증 플러그인 포함 (password, token, and federation based)</li>
<li>클라이언트의 설정을 세션으로 유지하면서 요청할 수 있도록 함. (based on the requests Python library)</li>
</ul>
<p>keystoneclient:</p>
<ul>
<li>Keystone API의 클라이언트 (CLI로 Keystone을 이용할 수 있음)</li>
</ul>
<h4 id="horizonopenstack_authbackendpy-1">horizon/openstack_auth/backend.py</h4>
<pre><code class="language-python">from openstackit.openstack_auth import user as auth_user

...

# TODO(stephenfin): Subclass &#39;django.contrib.auth.backends.BaseBackend&#39; once we
# (only) support Django 3.0
class KeystoneBackend(object):
  &quot;&quot;&quot;Django authentication backend for use with ``django.contrib.auth``.&quot;&quot;&quot;

  ...

  def authenticate(self, request, auth_url=None, **kwargs):

    ...

    # If we made it here we succeeded. Create our User!
    unscoped_token = unscoped_auth_ref.auth_token

    user = auth_user.create_user_from_token(
        request,
        auth_user.Token(scoped_auth_ref, unscoped_token=unscoped_token),
        endpoint,
        services_region=region_name)

    if request is not None:
      # if no k2k providers exist then the function returns quickly
      utils.store_initial_k2k_session(auth_url, request, scoped_auth_ref,
                                      unscoped_auth_ref)
      request.session[&#39;unscoped_token&#39;] = unscoped_token
      if domain_auth_ref:
        # check django session engine, if using cookies, this will not
        # work, as it will overflow the cookie so don&#39;t add domain
        # scoped token to the session and put error in the log
        if utils.using_cookie_backed_sessions():
          LOG.error(&#39;Using signed cookies as SESSION_ENGINE with &#39;
                    &#39;OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT is &#39;
                    &#39;enabled. This disables the ability to &#39;
                    &#39;perform identity operations due to cookie size &#39;
                    &#39;constraints.&#39;)
        else:
          request.session[&#39;domain_token&#39;] = domain_auth_ref

      request.user = user

      ...</code></pre>
<h4 id="horizonopenstack_authpluginbasepy">horizon/openstack_auth/plugin/base.py</h4>
<pre><code class="language-python">def get_access_info(self, keystone_auth):
  &quot;&quot;&quot;Get the access info from an unscoped auth

  This function provides the base functionality that the
  plugins will use to authenticate and get the access info object.

  :param keystone_auth: keystoneauth1 identity plugin
  :raises: exceptions.KeystoneAuthException on auth failure
  :returns: keystoneclient.access.AccessInfo
  &quot;&quot;&quot;
  session = utils.get_session()

  try:
    unscoped_auth_ref = keystone_auth.get_access(session)
  except keystone_exceptions.ConnectFailure as exc:
    LOG.error(str(exc))
    msg = _(&#39;Unable to establish connection to keystone endpoint.&#39;)
    raise exceptions.KeystoneConnectionException(msg)
  except (keystone_exceptions.Unauthorized,
          keystone_exceptions.Forbidden,
          keystone_exceptions.NotFound) as exc:
    msg = str(exc)
    LOG.debug(msg)
    match = re.match(r&quot;The password is expired and needs to be changed&quot;
                      r&quot; for user: ([^.]*)[.].*&quot;, msg)
    if match:
        exc = exceptions.KeystonePassExpiredException(
            _(&#39;Password expired.&#39;))
        exc.user_id = match.group(1)
        raise exc
    msg = _(&#39;Invalid credentials.&#39;)
    raise exceptions.KeystoneCredentialsException(msg)
  except (keystone_exceptions.ClientException,
          keystone_exceptions.AuthorizationFailure) as exc:
    msg = _(&quot;An error occurred authenticating. &quot;
            &quot;Please try again later.&quot;)
    LOG.debug(str(exc))
    raise exceptions.KeystoneAuthException(msg)
  return unscoped_auth_ref</code></pre>
<p>User 인스턴스를 생성하는지에 따라 Django 인증 시스템이 인증 완료 여부를 판단합니다. 즉, 인증 로직이 성공적으로 끝났다면 User 인스턴스를 생성해 Django에게 사용자가 인증되었음을 알려주어야 합니다.
Horizon은 Unscoped 토큰 발급이 성공적으로 이루어지면 <code>auth_user.create_user_from_token()</code> 함수를 호출해 User 인스턴스를 생성합니다. 그리고 만들어진 User 인스턴스를 request.user에 저장합니다. (Unscoped 토큰은 request.session[&#39;unscoped_token&#39;] = unscoped_token 코드에 의해 Session에 저장)</p>
<h4 id="horizonopenstack_authuserpy">horizon/openstack_auth/user.py</h4>
<pre><code class="language-python">def create_user_from_token(request, token, endpoint, services_region=None):
  # if the region is provided, use that, otherwise use the preferred region
  svc_region = services_region or \
    utils.default_services_region(token.serviceCatalog, request, endpoint)
  return User(id=token.user[&#39;id&#39;],
              token=token,
              user=token.user[&#39;name&#39;],
              password_expires_at=token.user[&#39;password_expires_at&#39;],
              user_domain_id=token.user_domain_id,
              # We need to consider already logged-in users with an old
              # version of Token without user_domain_name.
              user_domain_name=getattr(token, &#39;user_domain_name&#39;, None),
              project_id=token.project[&#39;id&#39;],
              project_name=token.project[&#39;name&#39;],
              domain_id=token.domain[&#39;id&#39;],
              domain_name=token.domain[&#39;name&#39;],
              enabled=True,
              service_catalog=token.serviceCatalog,
              roles=token.roles,
              endpoint=endpoint,
              services_region=svc_region,
              is_federated=getattr(token, &#39;is_federated&#39;, False),
              unscoped_token=getattr(token, &#39;unscoped_token&#39;,
                                    request.session.get(&#39;unscoped_token&#39;)))</code></pre>
<p><code>create_user_from_token()</code> 함수는 User 인스턴스를 생성해 Return 합니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/e29a789f-3f7c-4103-88b8-b0951a9cdbb7/image.png" alt=""></p>
<p>[그림 4] token 파라미터 값 (openstackit.openstack_auth.user.Token 자료형)</p>
<h4 id="horizonopenstack_authviewspy-1">horizon/openstack_auth/views.py</h4>
<pre><code class="language-python">from openstack_auth import user as auth_user

...

@sensitive_post_parameters()
@csrf_protect
@never_cache
def login(request, template_name=None, extra_context=None, **kwargs):

  ...

  # Set the session data here because django&#39;s session key rotation
  # will erase it if we set it earlier.
  if request.user.is_authenticated:
    auth_user.set_session_from_user(request, request.user)
    regions = dict(forms.get_region_choices())
    region = request.user.endpoint
    login_region = request.POST.get(&#39;region&#39;)
    region_name = regions.get(login_region)
    request.session[&#39;region_endpoint&#39;] = region
    request.session[&#39;region_name&#39;] = region_name
    expiration_time = request.user.time_until_expiration()
    threshold_days = settings.PASSWORD_EXPIRES_WARNING_THRESHOLD_DAYS
    if (expiration_time is not None and
          expiration_time.days &lt;= threshold_days and
          expiration_time &gt; datetime.timedelta(0)):
      expiration_time = str(expiration_time).rsplit(&#39;:&#39;, 1)[0]
      msg = (_(&#39;Please consider changing your password, it will expire&#39;
               &#39; in %s minutes&#39;) %
            expiration_time).replace(&#39;:&#39;, &#39; Hours and &#39;)
      messages.warning(request, msg)</code></pre>
<p>Custom 인증이 완료되면 다시, horizon/openstack_auth/views.py 로 돌아옵니다. 그리고 여기에서 token, user_id, region_endpoint, services_region을 추가적으로 Session에 저장합니다.</p>
<h4 id="horizonopenstack_authuserpy-1">horizon/openstack_auth/user.py</h4>
<pre><code class="language-python">...

def set_session_from_user(request, user):
  request.session[&#39;token&#39;] = user.token
  request.session[&#39;user_id&#39;] = user.id
  request.session[&#39;region_endpoint&#39;] = user.endpoint
  request.session[&#39;services_region&#39;] = user.services_region
  # Update the user object cached in the request
  request._cached_user = user
  request.user = user</code></pre>
<p><img src="https://velog.velcdn.com/images/roh-j/post/f1983b58-d336-4638-8162-29c59803758d/image.png" alt=""></p>
<p>[그림 5] session_data 컬럼에 base64로 인코딩 되어 저장</p>
<h1 id="로그인-후-서비스-요청-시퀀스-다이어그램">로그인 후, 서비스 요청 (시퀀스 다이어그램)</h1>
<p>Unscoped 토큰은 신분만 증명할 뿐, 서비스(e.g. Nova)에 서비스를 요청할 수 없습니다. 다시말해, 서비스를 요청하기 위해서는 endpoint와 범위가 지정되어 있는 Scoped 토큰이 필요합니다. Horizon은 Scoped 토큰을 얻기 위해 Session에 저장되어 있는 Unscoped 토큰으로 Keystone에게 Scoped 토큰을 요청합니다. Unscoped 토큰이 검증되어 Scoped 토큰이 성공적으로 발급되면 서비스를 요청할 수 있는 권한이 부여됩니다. 다시, Horizon은 서비스 요청 내용과 Scoped 토큰을 함께 서비스에게 보냅니다.</p>
<blockquote>
<p>An unscoped token contains neither a service catalog, any roles, a project scope, nor a domain scope. Their primary use case is simply to prove your identity to keystone at a later time (usually to generate scoped tokens), without repeatedly presenting your original credentials.</p>
</blockquote>
<p>이후, 서비스는 Horizon이 보내온 Scoped 토큰을 검증하기 위해 Keystone에게 검증 의뢰를 합니다. 토큰 검증에 문제가 없다면 사용자의 서비스 요청을 실행합니다. 요청이 완료되면 그 결과를 사용자에게 보고합니다.</p>
<p>아래는 로그인 후, 서비스 요청 과정에 대한 다이어그램입니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/1451d565-1d7a-488d-8909-82bdfbc99cb3/image.png" alt=""></p>
<p>[그림 6] 로그인 후, 서비스 요청 시퀀스 다이어그램</p>
<h1 id="django-session">Django Session</h1>
<p>Session 기반 인증에서 Session 저장소의 위치는 중요한 이슈사항이 됩니다. 로드밸런서에 의해 트래픽이 분산되는 환경일 때, 서버 각각 Session 저장소를 분리되어 있으면 Session의 정보를 공유하고 있지 않아 사용자를 식별할 수 없는 문제가 발생됩니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/f26cbd10-e296-48f3-9bec-0d4a22e31352/image.png" alt=""></p>
<p>[그림 7] 노드 각각 Session 저장소를 분리할 경우, 트래픽 분산 시 사용자를 식별할 수 없음</p>
<p>때문에, Django Session은 기본 설정으로 한 곳의 데이터베이스에 Session 정보를 저장하도록 되어 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/a91a3395-9692-4ee4-b68c-3aaeb070c059/image.png" alt=""></p>
<p>[그림 8] Django Session은 한 곳의 데이터베이스에 Session 정보를 저장 (Session의 정보를 공유하고 있기에 사용자 식별이 유지됨)</p>
<p><img src="https://velog.velcdn.com/images/roh-j/post/d64a9aca-eea2-453f-ac0f-fae41bcb3ca1/image.png" alt=""></p>
<p>[그림 9] horizon 데이터베이스에 django_session 테이블에 저장</p>
<h2 id="session-cookie">Session Cookie</h2>
<p><img src="https://velog.velcdn.com/images/roh-j/post/e3d035bf-4b1a-4b72-986d-f3ac8e6b897e/image.png" alt=""></p>
<p>[그림 10] 쿠키에 저장되는 sessionid</p>
<p>클라이언트는 sessionid를 쿠키로 저장합니다. 그리고 서버 요청 시 쿠키에 저장된 sessionid와 함께 보내, 서버가 사용자를 식별할 수 있도록 합니다. 만약, sessionid Name을 가진 쿠키를 지우게 되면 서버는 사용자를 식별할 수 없어 로그인이 풀리게 됩니다.</p>
<p>다만, 로그아웃과 달리 세션 정보를 지운 것이 아니기 때문에 예전 sessionid 값을 다시 쿠키에 담아 요청하면 다시 로그인을 유지할 수 있습니다. 아래는 이에 대한 시나리오입니다.</p>
<h4 id="--쿠키를-제거하고-새로고침">- 쿠키를 제거하고 새로고침</h4>
<p>사용자를 식별할 수 없어 로그인이 풀림.</p>
<h4 id="--쿠키를-이전-값으로-설정하고-새로고침">- 쿠키를 이전 값으로 설정하고 새로고침</h4>
<pre><code>{
  Name: sessionid
  Value: yoho40qgmc9ivjy25yua78tu5pp7vpt8
  Expires / Max-Age: 2021-02-17T08:00:20.116Z
}</code></pre><p>인증이 유지됨.</p>
<h4 id="참고-사항">참고 사항</h4>
<blockquote>
<p>sessionid 값을 다시 쿠키에 담고 새로고침할 때 URL이 중요합니다.</p>
<p><a href="http://localhost/auth/login/">http://localhost/auth/login/</a>
의 URL에서는 성공적으로 인증이 이루어지지만,</p>
<p><a href="http://localhost/auth/login/?next=/project/">http://localhost/auth/login/?next=/project/</a>
next 쿼리 스트링이 존재하면
<img src="https://velog.velcdn.com/images/roh-j/post/9e733620-60bc-4d94-af27-c909ec08d3cc/image.png" alt="">
에러를 발생시킵니다.</p>
</blockquote>
<h4 id="base64로-디코딩한-session-정보">base64로 디코딩한 Session 정보</h4>
<pre><code>1ef052cd5f350f2437dc34bc3026fc722b98ffb1:�}q(Uunscoped_tokenU�gAAAAABgLMgIMtPsvMIBf4vH7kapIzHVaWItsBMFjRb4TAkKgr4rVPUhVXCG6YPkDp5OuRUHlVBcDtPEUv3cpaQgjefoM5Y4zCs__6myyolZ-COyoJaFDKykgQ_F5oo7cN74J9DteTMiQio298r5O-23JeYTfdZWBgqUuser_idX 2d9eb8b0ca4f4abc8bfb74cbf8ae5238qU
region_namecdjango.utils.functional
_lazy_proxy_unpickle
q(cdjango.utils.translation
ugettext
qUDefault Region�q}qc__builtin__
unicode
_auth_user_idhUregion_endpointXttp://172.16.0.250:5000/v3q
Uservices_regionX       RegionOneq
U
usage_startU
2021-02-16U_auth_user_backendU&amp;openstack_auth.backend.KeystoneBackendU  usage_endU
2021-02-17Utokencopenstack_auth.user
Token
q
}q(Uunscoped_tokenqhUdomainq}q(UidqNUnameqNuUserviceCatalogq]q(}q(X     endpointsq]q(}q(XurlqXhttp://172.16.0.250:8042X interfaceqXttp://210.207.104.171:8042hXhttp://172.16.0.250:8042hX&lt;http://172.16.0.250:8776/v2/273795270dc5449baf47b1dfbca19170hX?http://210.207.104.171:8776/v2/3b7d443449b64cf89e4d37a20b04a407hX&lt;http://172.16.0.250:8776/v2/273795270dc5449baf47b1dfbca19170hXhttp://172.16.0.250:9292hXhttp://172.16.0.250:9292hXttp://210.207.104.171:9292hX?http://210.207.104.171:8774/v2/3b7d443449b64cf89e4d37a20b04a407hX&lt;http://172.16.0.250:8774/v2/273795270dc5449baf47b1dfbca19170hX&lt;http://172.16.0.250:8774/v2/273795270dc5449baf47b1dfbca19170hXhttp://172.16.0.250:9876hXttp://210.207.104.171:9876hXhttp://172.16.0.250:9876hX&gt;http://172.16.0.250:8774/v2.1/273795270dc5449baf47b1dfbca19170hXAhttp://210.207.104.171:8774/v2.1/3b7d443449b64cf89e4d37a20b04a407hX&gt;http://172.16.0.250:8774/v2.1/273795270dc5449baf47b1dfbca19170hXhttp://172.16.0.250:9696hXhttp://172.16.0.250:9696hXttp://210.207.104.171:9696hXhttp://192.168.140.250:35357hXttp://210.207.104.171:5000hXhttp://172.16.0.250:5000hX?http://210.207.104.171:8004/v1/3b7d443449b64cf89e4d37a20b04a407hX&lt;http://172.16.0.250:8004/v1/273795270dc5449baf47b1dfbca19170hX&lt;http://172.16.0.250:8004/v1/273795270dc5449baf47b1dfbca19170hXttp://210.207.104.171:8041hXhttp://172.16.0.250:8041hXhttp://172.16.0.250:8041hXttp://210.207.104.171:8082hXhttp://172.16.0.250:8082hXhttp://172.16.0.250:8082hXhttp://172.16.0.250:8780hXhttp://172.16.0.250:8780hXttp://210.207.104.171:8780hXhttp://210.207.104.171:8000/v1hXttp://172.16.0.250:8000/v1hXttp://172.16.0.250:8000/v1hX&lt;http://172.16.0.250:8776/v3/273795270dc5449baf47b1dfbca19170hX?http://210.207.104.171:8776/v3/3b7d443449b64cf89e4d37a20b04a407hX&lt;http://172.16.0.250:8776/v3/273795270dc5449baf47b1dfbca19170h&amp;0ciso8601.iso8601
Utc
qo)Rqp�RqqU
is_federatedqr�Uprojectqs}qt(hXhhjangquU     domain_idXdefaultUis_admin_project�hX 273795270dc5449baf47b1dfbca19170qvuUuserqw}qx(Upassword_expires_atNhhhXadminqyuUidqzU�gAAAAABgLMgI5Ms6zzPhNZkJcK1yTR32aIjKYCBeNhA2aIMnc6R4350OFmUP5WmKO83KI45-Uh_mUBMRmHzoTHJ9x-B8kaq_mmS0hEKFzuRXRniDvnavIPLV8BOlRJGuEItrEK-WGWs4XDJ_SunDXgazWKPAipOWZqPTLKP2hgY413HvLi8I86buGyDYnFQP25WRygWu1UHvUtenantq{htubX_session_expiryMU_auth_user_hashU(506d93b80b3981000edb8e3e0552d17645989327u.</code></pre><p>base64로 디코딩한 Session 정보를 확인해보면 unscoped_token 값이 저장되어 있는 것을 확인할 수 있습니다.</p>
<h2 id="로그아웃">로그아웃</h2>
<p>로그아웃 역시 Django 인증 시스템을 이용합니다. logout_then_login() 은 로그아웃 후 다시 로그인 페이지로 이동시키는 <code>django.contrib.auth.views</code> 모듈의 함수이며 Session 정보를 데이터베이스에서 삭제합니다. 여기서 중요한 점은, Session에 저장되어 있던 Unscoped 토큰은 무효화 처리하지 않는다는 점입니다. (로그아웃 시 Unscoped 토큰을 무효화한 후 세션 정보를 지우는 것이 아니라 단순히 세션 정보를 DB에서 삭제)</p>
<h4 id="horizonopenstack_authviewspy-2">horizon/openstack_auth/views.py</h4>
<pre><code class="language-python">def logout(request, login_url=None, **kwargs):
  &quot;&quot;&quot;Logs out the user if he is logged in. Then redirects to the log-in page.

  :param login_url:
      Once logged out, defines the URL where to redirect after login

  :param kwargs:
      see django.contrib.auth.views.logout_then_login extra parameters.

  &quot;&quot;&quot;
  msg = &#39;Logging out user &quot;%(username)s&quot;.&#39; % \
      {&#39;username&#39;: request.user.username}
  LOG.info(msg)

  &quot;&quot;&quot; Securely logs a user out. &quot;&quot;&quot;
  if (utils.is_websso_enabled and utils.is_websso_default_redirect() and
        utils.get_websso_default_redirect_logout()):
    auth_user.unset_session_user_variables(request)
    return django_http.HttpResponseRedirect(
        utils.get_websso_default_redirect_logout())
  else:
    return django_auth_views.logout_then_login(request,
                                                login_url=login_url,
                                                **kwargs)</code></pre>
<h3 id="레퍼런스">레퍼런스</h3>
<ul>
<li><a href="https://galid1.tistory.com/207">https://galid1.tistory.com/207</a></li>
<li><a href="https://docs.djangoproject.com/en/3.1/topics/http/sessions/">https://docs.djangoproject.com/en/3.1/topics/http/sessions/</a></li>
<li><a href="https://documentation.suse.com/hpe-helion/8/html/hpe-helion-openstack-clm-all/ops-managing-identity.html">https://documentation.suse.com/hpe-helion/8/html/hpe-helion-openstack-clm-all/ops-managing-identity.html</a></li>
<li><a href="https://zenodo.org/record/34463/files/SummerStudentReport-PawelPamula.pdf?download=1">https://zenodo.org/record/34463/files/SummerStudentReport-PawelPamula.pdf?download=1</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>