검색

레이블이 인 게시물을 표시합니다. 모든 게시물 표시
레이블이 인 게시물을 표시합니다. 모든 게시물 표시

2020년 11월 8일

오라클 클라우드 무료 체험하기 - Part2 Web 서버 구축하기

MySql 구축과 동일한 방법으로 웹 서버 구축을 위한 VM 인스턴스를 추가한다. 예상되는 전체 작업 순서는 아래와 같다.


① VM 인스터스 생성 (CentOS 7.x)
② VM 인스턴스 공인 IP 및 방화벽 설정
③ 블록 스토이지 생성
④ 블록 스토이지 와 VM 인스턴스 연결
⑤ WAS/WAS 설치 (nginx, tomcat)
⑥ 웹 프로그램 설치 및 테스트


(인스턴스 생성 및 IP 설정은 이전 게시물 참조 :  오라클 클라우드 무료 체험하기 - Part1 MySql 구축하기)  


블록 스토이지 생성

블록 볼륨(Block Volume) 서비스는 블록 스토리지를 제공하고 관리하는 네트워크 기반 스토리지 서비스이다. VM 인스턴스는 이 블록 스토이지(Block Storage)를 마운트하여 스토리지를 확장하여 사용할 수 있다.  새로운 블록을 생성하고 이를 웹 서버 VM 인스턴스에서 마운트하여 웹 데이터 공간으로 사용할 계획이다. 블록 스토리지를 추가하는 방법은 아래와 같이 3단계가 필요하다.  

① 오라클 클라이우드 콘솔 (Web UI)에서 블록 볼륨 리소스 생성
② 블록 볼륨을 VM 인스턴스에 연결
③ VM 인스턴스에서 블록 볼륨을 파일 시스템으로 마운트



블록은 50GB 크기로 생성했다. 



블록 스토이지 연결

이제 웹 서버 용도로 생성한 VM 인스턴스 에서 생성한 블록을 연결한다.





블록 볼륨과 VM 이 연결되면 "iSCSI 명령 및 정보" 항목을 확인하고 연결을 위한 명령을 복사한다. 


  
  sudo iscsiadm -m node -o new -T iqn.2015-12.com.oracleiaas:b8949530-afa4-4546-a6ff-4b33f67e5a1e -p 169.254.2.2:3260
  
  sudo iscsiadm -m node -o update -T iqn.2015-12.com.oracleiaas:b8949530-afa4-4546-a6ff-4b33f67e5a1e -n node.startup -v automatic
  
  sudo iscsiadm -m node -T iqn.2015-12.com.oracleiaas:b8949530-afa4-4546-a6ff-4b33f67e5a1e -p 169.254.2.2:3260 -l

  
SSH 접속 하여 복사한 iSCSI 명령어를 모두 실행하고 lsblk 명령으로 블록 볼륨이 마운트 되어 있는지 확인한다. 아래의 예에서는 sdb 이름의 50GB 디스크를 확인할 수 있다.
$lsblk
NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda      8:0    0 46.6G  0 disk 
├─sda1   8:1    0  512M  0 part /boot/efi
├─sda2   8:2    0    8G  0 part [SWAP]
└─sda3   8:3    0 38.1G  0 part /
sdb      8:16   0   50G  0 disk 
다음으로 연결된 블록 볼륨을 사용하기 위하여 ext4 형식으로 디스크를 포맷한다.
$sudo mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/sdb
$sudo mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/sdb

mke2fs 1.42.9 (28-Dec-2013)
/dev/sdb is entire device, not just one partition!
Proceed anyway? (y,n) y
Filesystem label=
OS type: Linux
Block size=4096 (log=2)
Fragment size=4096 (log=2)
Stride=0 blocks, Stripe width=256 blocks
3276800 inodes, 13107200 blocks
0 blocks (0.00%) reserved for the super user
First data block=0
Maximum filesystem blocks=2162163712
400 block groups
32768 blocks per group, 32768 fragments per group
8192 inodes per group
Superblock backups stored on blocks: 
	32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 
	4096000, 7962624, 11239424

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (32768 blocks): done
Writing superblocks and filesystem accounting information: done 


이제 마운트할 디렉토리를 생성하고 마운트 한다. ( sdb 를 /data 에 마운트하고 권한을 부여한다. )
$ sudo mkdir /data
$ sudo mount -o discard,defaults /dev/sdb /data
$ sudo chmod a+w /data

마지막으로 /etc/fstab 파일을 수정하여 부팅과 동시에 마운트 되도록 한다.

$ sudo blkid /dev/sdb
/dev/sdb: UUID="3c50667e-98f3-46f9-acfe-1bc10151696c" TYPE="ext4" 
$vi /etc/fstab

UUID=3c50667e-98f3-46f9-acfe-1bc10151696c /data ext4 discard,defaults,noatime,_netdev 0 2


웹 서버 설치

웹서버는 nginx + tomcat 로 구성하여 설치한다. 가장 최신 open jdk 버전을 설치한다. (14.0.2)
   
$sudo yum install java-latest-openjdk-devel.x86_64

$java -version
openjdk version "14.0.2" 2020-07-14
OpenJDK Runtime Environment 20.3 (build 14.0.2+12)
OpenJDK 64-Bit Server VM 20.3 (build 14.0.2+12, mixed mode, sharing)

톰켓 설치는 홈페이지를 방문하여 원하는 버전의 다운로드 링크를 복사한 다음 wget 명령을 사용하여 설치 파일을 다운로드한다.
  

http://apache.tt.co.kr/tomcat/tomcat-8/v8.5.58/bin/apache-tomcat-8.5.58.zip 다운로드가 완료되면 압축을 풀고 원하는 위치로 복사한다. (여기에서는 /data 를 사용했다.)
$wget http://apache.tt.co.kr/tomcat/tomcat-8/v8.5.58/bin/apache-tomcat-8.5.58.zip
$tar -xzvf apache-tomcat-8.5.58.zip
$mv apache-tomcat-8.5.58.zip /data/

nginx 설치를 위해서는 CentOS 7 EPEL 레파지토리를 추가해야 한다. 다음 명령으로 레파지토리를 추가한다. 
 
$sudo yum install epel-release

Loaded plugins: fastestmirror, langpacks
Loading mirror speeds from cached hostfile
 * base: mirror.kakao.com
 * epel: ord.mirror.rackspace.com
 * extras: mirror.kakao.com
 * updates: mirror.kakao.com
Resolving Dependencies
--> Running transaction check
---> Package epel-release.noarch 0:7-11 will be updated
---> Package epel-release.noarch 0:7-12 will be an update
--> Finished Dependency Resolution

Dependencies Resolved

=========================================================================================================================================================================================
 Package                                           Arch                                        Version                                   Repository                                 Size
=========================================================================================================================================================================================
Updating:
 epel-release                                      noarch                                      7-12                                      epel                                       15 k

Transaction Summary
=========================================================================================================================================================================================
Upgrade  1 Package

Total download size: 15 k
Is this ok [y/d/N]: y
Downloading packages:
Delta RPMs disabled because /usr/bin/applydeltarpm not installed.
epel-release-7-12.noarch.rpm                                                                                                                                      |  15 kB  00:00:01     
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Updating   : epel-release-7-12.noarch                                                                                                                                              1/2 
  Cleanup    : epel-release-7-11.noarch                                                                                                                                              2/2 
  Verifying  : epel-release-7-12.noarch                                                                                                                                              1/2 
  Verifying  : epel-release-7-11.noarch                                                                                                                                              2/2 

Updated:
  epel-release.noarch 0:7-12                                                                                                                                                             
Complete!
이제 nginx 를 설치한다.
sudo yum install nginx
$sudo yum install nginx
Loaded plugins: fastestmirror, langpacks
Loading mirror speeds from cached hostfile
 * base: mirror.kakao.com
 * epel: ord.mirror.rackspace.com
 * extras: mirror.kakao.com
 * updates: mirror.kakao.com
Resolving Dependencies
--> Running transaction check
---> Package nginx.x86_64 1:1.16.1-2.el7 will be installed
--> Processing Dependency: nginx-all-modules = 1:1.16.1-2.el7 for package: 1:nginx-1.16.1-2.el7.x86_64
--> Processing Dependency: nginx-filesystem = 1:1.16.1-2.el7 for package: 1:nginx-1.16.1-2.el7.x86_64
--> Processing Dependency: nginx-filesystem for package: 1:nginx-1.16.1-2.el7.x86_64
--> Processing Dependency: libprofiler.so.0()(64bit) for package: 1:nginx-1.16.1-2.el7.x86_64
--> Running transaction check
---> Package gperftools-libs.x86_64 0:2.6.1-1.el7 will be installed
---> Package nginx-all-modules.noarch 1:1.16.1-2.el7 will be installed
--> Processing Dependency: nginx-mod-http-image-filter = 1:1.16.1-2.el7 for package: 1:nginx-all-modules-1.16.1-2.el7.noarch
--> Processing Dependency: nginx-mod-http-perl = 1:1.16.1-2.el7 for package: 1:nginx-all-modules-1.16.1-2.el7.noarch
--> Processing Dependency: nginx-mod-http-xslt-filter = 1:1.16.1-2.el7 for package: 1:nginx-all-modules-1.16.1-2.el7.noarch
--> Processing Dependency: nginx-mod-mail = 1:1.16.1-2.el7 for package: 1:nginx-all-modules-1.16.1-2.el7.noarch
--> Processing Dependency: nginx-mod-stream = 1:1.16.1-2.el7 for package: 1:nginx-all-modules-1.16.1-2.el7.noarch
---> Package nginx-filesystem.noarch 1:1.16.1-2.el7 will be installed
--> Running transaction check
---> Package nginx-mod-http-image-filter.x86_64 1:1.16.1-2.el7 will be installed
--> Processing Dependency: gd for package: 1:nginx-mod-http-image-filter-1.16.1-2.el7.x86_64
--> Processing Dependency: libgd.so.2()(64bit) for package: 1:nginx-mod-http-image-filter-1.16.1-2.el7.x86_64
---> Package nginx-mod-http-perl.x86_64 1:1.16.1-2.el7 will be installed
---> Package nginx-mod-http-xslt-filter.x86_64 1:1.16.1-2.el7 will be installed
---> Package nginx-mod-mail.x86_64 1:1.16.1-2.el7 will be installed
---> Package nginx-mod-stream.x86_64 1:1.16.1-2.el7 will be installed
--> Running transaction check
---> Package gd.x86_64 0:2.0.35-26.el7 will be installed
--> Processing Dependency: libXpm.so.4()(64bit) for package: gd-2.0.35-26.el7.x86_64
--> Running transaction check
---> Package libXpm.x86_64 0:3.5.12-1.el7 will be installed
--> Finished Dependency Resolution

Dependencies Resolved

=========================================================================================================================================================================================
 Package                                                   Arch                                 Version                                         Repository                          Size
=========================================================================================================================================================================================
Installing:
 nginx                                                     x86_64                               1:1.16.1-2.el7                                  epel                               562 k
Installing for dependencies:
 gd                                                        x86_64                               2.0.35-26.el7                                   base                               146 k
 gperftools-libs                                           x86_64                               2.6.1-1.el7                                     base                               272 k
 libXpm                                                    x86_64                               3.5.12-1.el7                                    base                                55 k
 nginx-all-modules                                         noarch                               1:1.16.1-2.el7                                  epel                                20 k
 nginx-filesystem                                          noarch                               1:1.16.1-2.el7                                  epel                                21 k
 nginx-mod-http-image-filter                               x86_64                               1:1.16.1-2.el7                                  epel                                30 k
 nginx-mod-http-perl                                       x86_64                               1:1.16.1-2.el7                                  epel                                39 k
 nginx-mod-http-xslt-filter                                x86_64                               1:1.16.1-2.el7                                  epel                                29 k
 nginx-mod-mail                                            x86_64                               1:1.16.1-2.el7                                  epel                                57 k
 nginx-mod-stream                                          x86_64                               1:1.16.1-2.el7                                  epel                                85 k

Transaction Summary
=========================================================================================================================================================================================
Install  1 Package (+10 Dependent packages)

Total download size: 1.3 M
Installed size: 4.0 M
Is this ok [y/d/N]: y
Downloading packages:
(1/11): gd-2.0.35-26.el7.x86_64.rpm                                                                                                                               | 146 kB  00:00:00     
(2/11): libXpm-3.5.12-1.el7.x86_64.rpm                                                                                                                            |  55 kB  00:00:00     
(3/11): gperftools-libs-2.6.1-1.el7.x86_64.rpm                                                                                                                    | 272 kB  00:00:00     
(4/11): nginx-1.16.1-2.el7.x86_64.rpm                                                                                                                             | 562 kB  00:00:01     
(5/11): nginx-all-modules-1.16.1-2.el7.noarch.rpm                                                                                                                 |  20 kB  00:00:00     
(6/11): nginx-filesystem-1.16.1-2.el7.noarch.rpm                                                                                                                  |  21 kB  00:00:00     
(7/11): nginx-mod-http-image-filter-1.16.1-2.el7.x86_64.rpm                                                                                                       |  30 kB  00:00:00     
(8/11): nginx-mod-http-perl-1.16.1-2.el7.x86_64.rpm                                                                                                               |  39 kB  00:00:00     
(9/11): nginx-mod-http-xslt-filter-1.16.1-2.el7.x86_64.rpm                                                                                                        |  29 kB  00:00:00     
(10/11): nginx-mod-mail-1.16.1-2.el7.x86_64.rpm                                                                                                                   |  57 kB  00:00:00     
(11/11): nginx-mod-stream-1.16.1-2.el7.x86_64.rpm                                                                                                                 |  85 kB  00:00:00     
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Total                                                                                                                                                    383 kB/s | 1.3 MB  00:00:03     
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Installing : gperftools-libs-2.6.1-1.el7.x86_64                                                                                                                                   1/11 
  Installing : 1:nginx-filesystem-1.16.1-2.el7.noarch                                                                                                                               2/11 
  Installing : libXpm-3.5.12-1.el7.x86_64                                                                                                                                           3/11 
  Installing : gd-2.0.35-26.el7.x86_64                                                                                                                                              4/11 
  Installing : 1:nginx-mod-stream-1.16.1-2.el7.x86_64                                                                                                                               5/11 
  Installing : 1:nginx-mod-http-xslt-filter-1.16.1-2.el7.x86_64                                                                                                                     6/11 
  Installing : 1:nginx-mod-http-perl-1.16.1-2.el7.x86_64                                                                                                                            7/11 
  Installing : 1:nginx-mod-mail-1.16.1-2.el7.x86_64                                                                                                                                 8/11 
  Installing : 1:nginx-1.16.1-2.el7.x86_64                                                                                                                                          9/11 
  Installing : 1:nginx-mod-http-image-filter-1.16.1-2.el7.x86_64                                                                                                                   10/11 
  Installing : 1:nginx-all-modules-1.16.1-2.el7.noarch                                                                                                                             11/11 
  Verifying  : libXpm-3.5.12-1.el7.x86_64                                                                                                                                           1/11 
  Verifying  : 1:nginx-mod-stream-1.16.1-2.el7.x86_64                                                                                                                               2/11 
  Verifying  : 1:nginx-filesystem-1.16.1-2.el7.noarch                                                                                                                               3/11 
  Verifying  : 1:nginx-all-modules-1.16.1-2.el7.noarch                                                                                                                              4/11 
  Verifying  : gperftools-libs-2.6.1-1.el7.x86_64                                                                                                                                   5/11 
  Verifying  : 1:nginx-mod-http-image-filter-1.16.1-2.el7.x86_64                                                                                                                    6/11 
  Verifying  : gd-2.0.35-26.el7.x86_64                                                                                                                                              7/11 
  Verifying  : 1:nginx-mod-http-xslt-filter-1.16.1-2.el7.x86_64                                                                                                                     8/11 
  Verifying  : 1:nginx-mod-http-perl-1.16.1-2.el7.x86_64                                                                                                                            9/11 
  Verifying  : 1:nginx-mod-mail-1.16.1-2.el7.x86_64                                                                                                                                10/11 
  Verifying  : 1:nginx-1.16.1-2.el7.x86_64                                                                                                                                         11/11 

Installed:
  nginx.x86_64 1:1.16.1-2.el7                                                                                                                                                            

Dependency Installed:
  gd.x86_64 0:2.0.35-26.el7               gperftools-libs.x86_64 0:2.6.1-1.el7               libXpm.x86_64 0:3.5.12-1.el7               nginx-all-modules.noarch 1:1.16.1-2.el7          
  nginx-filesystem.noarch 1:1.16.1-2.el7  nginx-mod-http-image-filter.x86_64 1:1.16.1-2.el7  nginx-mod-http-perl.x86_64 1:1.16.1-2.el7  nginx-mod-http-xslt-filter.x86_64 1:1.16.1-2.el7 
  nginx-mod-mail.x86_64 1:1.16.1-2.el7    nginx-mod-stream.x86_64 1:1.16.1-2.el7            

Complete!
설치가 완료되면 아래와 같이 http, https 포트에 대한 방화벽을 오픈한다.
#sudo firewall-cmd --zone=public --permanent --add-service=http
#sudo firewall-cmd --zone=public --permanent --add-service=https
#sudo firewall-cmd --reload 


참고로 nginx 의 웹 루트는 /data/www 로 변경하였고 톰켓의 웹 프로그램은 /data/webapps/ 로 변경하였다. 이제 nginx 를 시작한다.
sudo systemctl start nginx

웹 브라우져를 통하여 사이트를 접속하니 퍼미션 오류가 기다리고 있었다. 먼저 보안 관련 이슈를 확인하기 위하여 /var/log/audit/audit.log 로그를 확인했고 아래의 보안 이슈를 확인하게 되었다.
type=AVC msg=audit(1601820483.230:3808): avc:  denied  { read } for  pid=4741 comm="nginx" name="index.html" dev="sdb" ino=2359732 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:unlabeled_t:s0 tclass=file permissive=0
type=SYSCALL msg=audit(1601820483.230:3808): arch=c000003e syscall=2 success=no exit=-13 a0=5571a0b1bc1a a1=800 a2=0 a3=55719f96d110 items=0 ppid=4740 pid=4741 auid=4294967295 uid=994 gid=992 euid=994 suid=994 fsuid=994 egid=992 sgid=992 fsgid=992 tty=(none) ses=4294967295 comm="nginx" exe="/usr/sbin/nginx" subj=system_u:system_r:httpd_t:s0 key=(null)
type=PROCTITLE msg=audit(1601820483.230:3808): proctitle=6E67696E783A20776F726B65722070726F63657373

audit2why 명령을 사용하여 원인을 확인해 본다. 보다 정확한 원인 및 조치방법을 알기위하여 웹 검색 결과 SELinux 보안이 이슈의 원인인 것으로 확인 할 수 있었다.
  
$sudo grep 1601820483.230:3808 /var/log/audit/audit.log | audit2why

	Was caused by:
		Missing type enforcement (TE) allow rule.

		You can use audit2allow to generate a loadable module to allow this access.
 
$semanage fcontext -a -t httpd_sys_content_t /usr/share/nginx/html/studio(/.*)?
$restorecon -Rv /www

❶ 톰켓과 프록시 연결시 오류는 → (13: Permission denied) while connecting to upstream:[nginx] 아래 명령을 통하여 해결 할 수 있었다.
setsebool -P httpd_can_network_connect

❷ /data/www 경로에 대한 접근 권한 오류는 아래와 같은 명령을 통하여 해결 할 수 있었다.
  
$chcon -Rt httpd_sys_content_t /data/www


익숙한 httpd 를 사용하지 않은 것은 클라우드 서버의 성능을 고려하여 적은 자원을 사용한다고 알려진 nginx 를 사용하게 되었는데 nginx 설치는 처음이라 보안 관련 이슈를 해결하는 과정에서 많은 시간을 소요하였다. 


참고자료

2020년 10월 4일

오라클 클라우드 무료 체험하기 - Part1 MySql 구축하기

무료로 클라우드 서비스 이용이 가능하다는 글을 접하고 자체 서버를 이용하여 운영하던 개발 환경을 클라우드 환경으로 변경하고자 시도하였다. 클라우드라 하여 특별한 것은 없었고 용어 또는 논리적인 개념의 이해가 요구되었다. 처음 클라우드 서비스를 접하는 거지만 많은 인터넷 자료들이 있고 과거 물리적 서버 구축 경험이 있어 실재 구축은 1시간 정도 소요된 것 같다.

오라클 클라우드

본 글에서는 클라우드 서비스를 기반으로 WEB/WAS 서버와 DBMS 서버 구현 작업을 계획하였다. 무료 오라클 클라우드는 아래와 같은 순서의 작업을 통하여 이용할 수 있다.(→ MySql 서버 생성 포함) 

① Oracle Cloud 계정 생성

② VM 인스터스 생성 (CentOS 7.x)

③ 원격 접속 

④ MySql 설치

⑤ VM 인스턴스  공인 IP 및 방화벽 설정

⑥ 로컬에서 MySQLWorkbench 툴을 이용한 MySql 접속


Oracle Cloud 계정 생성

계정 생성을 위하여 아래 홈페이지를 방문한다. 

https://www.oracle.com/kr/cloud/


Oracle Cloud Tier 무료체험하기 버튼을 클릭하여 계정을 생성한다. (계정 생성을 위해서는 주소, 모바일 인증 그리고 신용카드 등록이 필요하다.)

계정 생성이 완료되면 자동으로 클라우드 서비스 관리를 위한 웹 콘솔로 이동된다.


인스턴스 생성하기

웹 콘솔에서 VM 인스턴스 생성 을 클릭한다.


OS 이미지는 디폴트로 Oracle Linux 선택되어 있지만 변경이 가능하고 평소 사용하던 CentOS 를 선택하였다. 


리눅스 인스턴스의 경우는 원격 접속을 위하여 비밀번호 대신 SSH 키쌍을 사용하도록 되어 있다. 원격 접속 작업을 위하여 전용 키를 다운로드 한다.



인스턴스 생성이 완료되면 IP 와 계정을 확인 한다.



리모트 접속

리모트 접속은 인스턴스 생성 과정에서 다운로드한 SSH 키파일, 아이디 그리고 IP 를 이용하여 아래와 같이 접속할 수 있다. 개발 환경이 맥을 사용하고 있어 내장된 ssh  프로그램을 사용하였다.

ssh -i [키파일 경로] [아이디]@[IP]
 
ssh -i ssh-key-2020-10-03.key opc@xxx.xxx.xxx.xxx
The authenticity of host 'xxx.xxx.xxx.xxx (xxx.xxx.xxx.xxx)' can't be established.
ECDSA key fingerprint is SHA256:cb0AAC47vug6I8sNC7NPaIswm1+Xulwx8SAATP9D4+c. Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added 'xxx.xxx.xxx.xxx' (ECDSA) to the list of known hosts.

MySql 설치

MySQL 을 yum 명령을 사용하여 설치하게되면 (실제) MariaDB 가 설치된다. 

sudo yum install mysql
 sudo yum install mysql
Loaded plugins: fastestmirror, langpacks
Loading mirror speeds from cached hostfile
 * base: mirror.kakao.com
 * epel: d2lzkl7pfhq30w.cloudfront.net
 * extras: mirror.kakao.com
 * updates: mirror.kakao.com
Resolving Dependencies
--> Running transaction check
---> Package mariadb.x86_64 1:5.5.65-1.el7 will be installed
--> Finished Dependency Resolution

Dependencies Resolved

========================================================================================================================================
 Package                        Arch                          Version                                 Repository                   Size
========================================================================================================================================
Installing:
 mariadb                        x86_64                        1:5.5.65-1.el7                          base                        8.7 M

Transaction Summary
========================================================================================================================================
Install  1 Package

Total download size: 8.7 M
Installed size: 49 M
Is this ok [y/d/N]:  y
Downloading packages:
mariadb-5.5.65-1.el7.x86_64.rpm                                                                                  | 8.7 MB  00:00:01     
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Installing : 1:mariadb-5.5.65-1.el7.x86_64                                                                                        1/1 
  Verifying  : 1:mariadb-5.5.65-1.el7.x86_64                                                                                        1/1 

Installed:
  mariadb.x86_64 1:5.5.65-1.el7                                                                                                         

Complete!

 
필요한것은 MySql Community Edition이기 때문에 설치된 Maria DB 를 삭제한다.
[opc@instance-20201003-1526 ~]$grep -i -e maria
mariadb.x86_64                         1:5.5.65-1.el7                 @base     
mariadb-libs.x86_64                    1:5.5.65-1.el7                 @anaconda 

[opc@instance-20201003-1526 ~]$yum remove mariadb-libs.x86_64 mariadb.x86_64
Loaded plugins: fastestmirror, langpackssudo 
Resolving Dependencies
--> Running transaction check
---> Package mariadb.x86_64 1:5.5.65-1.el7 will be erased
---> Package mariadb-libs.x86_64 1:5.5.65-1.el7 will be erased
--> Processing Dependency: libmysqlclient.so.18()(64bit) for package: 2:postfix-2.10.1-9.el7.x86_64
--> Processing Dependency: libmysqlclient.so.18(libmysqlclient_18)(64bit) for package: 2:postfix-2.10.1-9.el7.x86_64
--> Running transaction check
---> Package postfix.x86_64 2:2.10.1-9.el7 will be erased
--> Finished Dependency Resolution

Dependencies Resolved

===========================================================================================================================================================
 Package                               Arch                            Version                                    Repository                          Size
===========================================================================================================================================================
Removing:
 mariadb                               x86_64                          1:5.5.65-1.el7                             @base                               49 M
 mariadb-libs                          x86_64                          1:5.5.65-1.el7                             @anaconda                          4.4 M
Removing for dependencies:
 postfix                               x86_64                          2:2.10.1-9.el7                             @anaconda                           12 M

Transaction Summary
===========================================================================================================================================================
Remove  2 Packages (+1 Dependent package)

Installed size: 65 M
Is this ok [y/N]: y
Downloading packages:
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Erasing    : 1:mariadb-5.5.65-1.el7.x86_64                                                                                                           1/3 
  Erasing    : 2:postfix-2.10.1-9.el7.x86_64                                                                                                           2/3 
  Erasing    : 1:mariadb-libs-5.5.65-1.el7.x86_64                                                                                                      3/3 
  Verifying  : 1:mariadb-libs-5.5.65-1.el7.x86_64                                                                                                      1/3 
  Verifying  : 2:postfix-2.10.1-9.el7.x86_64                                                                                                           2/3 
  Verifying  : 1:mariadb-5.5.65-1.el7.x86_64                                                                                                           3/3 

Removed:
  mariadb.x86_64 1:5.5.65-1.el7                                             mariadb-libs.x86_64 1:5.5.65-1.el7                                            

Dependency Removed:
  postfix.x86_64 2:2.10.1-9.el7                                                                                                                            

Complete!


MySQL 8.0 Community Edition 설치는 설치 소스에 대한 YUM 레파지토리를 추가하고 진행하면 된다.

[opc@instance-20201003-1526 ~]$ wget http://repo.mysql.com/mysql80-community-release-el7-1.noarch.rpm
--2020-10-03 08:22:41--  http://repo.mysql.com/mysql80-community-release-el7-1.noarch.rpm
Resolving repo.mysql.com (repo.mysql.com)... 104.76.84.199
Connecting to repo.mysql.com (repo.mysql.com)|104.76.84.199|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 25820 (25K) [application/x-redhat-package-manager]
Saving to: ‘mysql80-community-release-el7-1.noarch.rpm’

100%[=================================================================================================================>] 25,820      --.-K/s   in 0.003s  

2020-10-03 08:22:41 (8.12 MB/s) - ‘mysql80-community-release-el7-1.noarch.rpm’ saved [25820/25820]

[opc@instance-20201003-1526 ~]$ sudo rpm -ivh mysql80-community-release-el7-1.noarch.rpm
경고: mysql80-community-release-el7-1.noarch.rpm: Header V3 DSA/SHA1 Signature, key ID 5072e1f5: NOKEY
준비 중...                         ################################# [100%]
Updating / installing...
   1:mysql80-community-release-el7-1  ################################# [100%]
   
[opc@instance-20201003-1526 ~]$ sudo yum install mysql-server
Loaded plugins: fastestmirror, langpacks
Loading mirror speeds from cached hostfile
 * base: mirror.kakao.com
 * epel: d2lzkl7pfhq30w.cloudfront.net
 * extras: mirror.kakao.com
 * updates: mirror.kakao.com
mysql-connectors-community                                                                                                          | 2.5 kB  00:00:00     
mysql-tools-community                                                                                                               | 2.5 kB  00:00:00     
mysql80-community                                                                                                                   | 2.5 kB  00:00:00     
(1/3): mysql-tools-community/x86_64/primary_db                                                                                      |  76 kB  00:00:00     
(2/3): mysql-connectors-community/x86_64/primary_db                                                                                 |  62 kB  00:00:00     
(3/3): mysql80-community/x86_64/primary_db                                                                                          | 115 kB  00:00:00     
Resolving Dependencies
--> Running transaction check
---> Package mysql-community-server.x86_64 0:8.0.21-1.el7 will be installed
--> Processing Dependency: mysql-community-common(x86-64) = 8.0.21-1.el7 for package: mysql-community-server-8.0.21-1.el7.x86_64
--> Processing Dependency: mysql-community-client(x86-64) >= 8.0.11 for package: mysql-community-server-8.0.21-1.el7.x86_64
--> Running transaction check
---> Package mysql-community-client.x86_64 0:8.0.21-1.el7 will be installed
--> Processing Dependency: mysql-community-libs(x86-64) >= 8.0.11 for package: mysql-community-client-8.0.21-1.el7.x86_64
---> Package mysql-community-common.x86_64 0:8.0.21-1.el7 will be installed
--> Running transaction check
---> Package mysql-community-libs.x86_64 0:8.0.21-1.el7 will be installed
--> Finished Dependency Resolution

Dependencies Resolved

===========================================================================================================================================================
 Package                                     Arch                        Version                              Repository                              Size
===========================================================================================================================================================
Installing:
 mysql-community-server                      x86_64                      8.0.21-1.el7                         mysql80-community                      499 M
Installing for dependencies:
 mysql-community-client                      x86_64                      8.0.21-1.el7                         mysql80-community                       48 M
 mysql-community-common                      x86_64                      8.0.21-1.el7                         mysql80-community                      617 k
 mysql-community-libs                        x86_64                      8.0.21-1.el7                         mysql80-community                      4.5 M

Transaction Summary
===========================================================================================================================================================
Install  1 Package (+3 Dependent packages)

Total download size: 551 M
Installed size: 2.5 G
Is this ok [y/d/N]: y
Downloading packages:
경고: /var/cache/yum/x86_64/7/mysql80-community/packages/mysql-community-common-8.0.21-1.el7.x86_64.rpm: Header V3 DSA/SHA1 Signature, key ID 5072e1f5: NOKEY
Public key for mysql-community-common-8.0.21-1.el7.x86_64.rpm is not installed
(1/4): mysql-community-common-8.0.21-1.el7.x86_64.rpm                                                                               | 617 kB  00:00:00     
(2/4): mysql-community-libs-8.0.21-1.el7.x86_64.rpm                                                                                 | 4.5 MB  00:00:04     
(3/4): mysql-community-client-8.0.21-1.el7.x86_64.rpm                                                                               |  48 MB  00:00:15     
(4/4): mysql-community-server-8.0.21-1.el7.x86_64.rpm                                                                                                                                                                          | 499 MB  00:01:44     
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Total                                                                                                                                                                                                                 5.0 MB/s | 551 MB  00:01:49     
Retrieving key from file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql
Importing GPG key 0x5072E1F5:
 Userid     : "MySQL Release Engineering "
 Fingerprint: a4a9 4068 76fc bd3c 4567 70c8 8c71 8d3b 5072 e1f5
 Package    : mysql80-community-release-el7-1.noarch (installed)
 From       : /etc/pki/rpm-gpg/RPM-GPG-KEY-mysql
Is this ok [y/N]:  y
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
Warning: RPMDB altered outside of yum.
  Installing : mysql-community-common-8.0.21-1.el7.x86_64                                                                                                                                                                                               1/4 
  Installing : mysql-community-libs-8.0.21-1.el7.x86_64                                                                                                                                                                                                 2/4 
  Installing : mysql-community-client-8.0.21-1.el7.x86_64                                                                                                                                                                                               3/4 
  Installing : mysql-community-server-8.0.21-1.el7.x86_64                                                                                                                                                                                               4/4 
  Verifying  : mysql-community-server-8.0.21-1.el7.x86_64                                                                                                                                                                                               1/4 
  Verifying  : mysql-community-client-8.0.21-1.el7.x86_64                                                                                                                                                                                               2/4 
  Verifying  : mysql-community-common-8.0.21-1.el7.x86_64                                                                                                                                                                                               3/4 
  Verifying  : mysql-community-libs-8.0.21-1.el7.x86_64                                                                                                                                                                                                 4/4 

Installed:
  mysql-community-server.x86_64 0:8.0.21-1.el7                                                                                                                                                                                                              
Dependency Installed:
  mysql-community-client.x86_64 0:8.0.21-1.el7  mysql-community-common.x86_64 0:8.0.21-1.el7 mysql-community-libs.x86_64 0:8.0.21-1.el7                                       

Complete!

자세한 설치는 CentOS 7.x 에서 MySQL 8.x 설치하기 참조.
 
사용된 mysql 설정 : /etc/my.cnf
데이터베이스 및 계정 생성 스크립트 : database-mysql.sql 

VM 인스턴스  공인 IP 및 방화벽 설정

mysql 서버가 설치된 인스턴스를 외부에서 사용할 수 있도록 공인 IP 와 방화벽 설정을 추가했다.

❶ 인스턴스에 공인 IP 를 할당하기 위하여  네크워킹> IP 관리 메뉴에서 새로운 공인 IP 주소를 예약한다. (Reserve Public IP Address)






❷ 컴퓨트 > 인스턴스 > 인스턴스 세부정보 > 연결된 VNIC(Virtual Network Interface Cards) 메뉴에서 새로운 VNIC 추가하여 인스턴스에 공인 IP 를 할당한다.




❸ 컴퓨트 > 인스턴스 > 인스턴스 세부정보 에서 공용 서브넷 을 클릭하여 공용 서브넷 관리 화면으로 이동한다.



디폴트로 생성되어 있는 보안 목록을 클릭하고 


MySql 서버에 대한 외부 접속 허용을 위하여 3306 포트에 대한 수신 규칙을 추가한다.
 


이로써 3306 포트로 외부에서 MySql 서버에 대한 통신이 허용된다. 

MySql Workbench 에서 접속한 화면 예시

MySql Workbench 에서 접속한 화면 예시


2020년 8월 31일

스프링기반 웹 프로그램에서 CKEditor 사용하기 - 이미지 업로드

CKEditor 는 WYSIWYG 편집기로 웹 환경에서 콘텐츠를 손쉽게 작성 및 수정을 가능하게 하는 웹 에디터다. 개발과 테스트 환경은 아래와 같다.

Development
  • Visual Studio Code 1.48.1
  • npm 6.14.5
  • webpack 4.44.1
  • HW : MacBook Pro 2019 
    • macOS Catalina 10.15.6
    • 2.3Ghz 8Core Intel Corei9 
    • 16GB 2667 Mhz DDR4 
Server 
  • Spring 4.3.23.RELEASE 
  • Spring Security 4.2.12.RELEASE
  • Tomcat 9.0.27.0
  • Open JDK 13.0.1 
  • mysql 8.0.17
  • HW : Mac mini Late 2012 
    • macOS Catalina 10.15.6
    •  2.3Ghz 4Core Intel Core i7
    • 16GB 1600 MHz DDR3


라이선스

라이선스를 확인해보면, CKEditor는 GPL, LGPL, MPL 등의 오픈소스 라이선스와 여러 등급의 상용 라이선스로 배포 및 판매되고 있다.

  • CKEditor 4: GPL-2, LGPL-2.1, MPL-1.1, Commercial License (https://github.com/ckeditor/ckeditor-dev)
  • CKEditor 5: GPL-2, Commercial License (https://github.com/ckeditor/ckeditor5)

GPL, LGPL, MPL 은 모두 각각의 라이선스 의무사항만 준수한다면 상업적 이용을 허용하고 있으며, 각 라이선스의 대표적 의무사항은 다음과 같다.
  • GPL: 응용프로그램의 소스코드를 포함 한 모든 소스코드를 수취자(고객)에게 제공. 링크되는 모든 코드를 GPL로 정의
  • LGPL: 정적링크 시 응용프로그램의 목적코드와 LGPL 자체의 소스코드(수정이 있을 경우 수정부분 포함)를 수취자(고객)에게 제공, 동적링크 시 LGPL 자체의 코드(수정이 있었다면 수정코드 포함)만 제공. 응용프로그램과 LGPL 코드를 분리하여 정의
  • MPL: MPL 코드가 포함된 파일만 수취자(고객)에게 제공. 실행파일은 별도의 라이선스(예를들어 자사의 상용라이선스)로 배포하는 것을 허용. 물론 MPL 소스코드는 MPL로 제공해야 함
참고로 공개SW 라이선스의 의무사항 준수 의무는 공개SW 원본 그 자체 혹은 이를 이용하여 개발한 소프트웨어를 타인에게 배포하는 경우에 발생한다.

Webpack 환경에서 CKEditor 5 사용하기

CKEditor Home : https://ckeditor.com/

CKEditor 5 를 사용할 예정이기 때문에 https://ckeditor.com/ckeditor-5/ 웹 페이지를 방문한다.  webpack 환경에서 build 된 Classic  버전을 설치하여 사용할 예정이기 때문에 아래 절차로 설치를 진행한다. 참고로 설치를 위한 webpack 기반 프로젝트가 필요하다. 새로운 프로젝트 생성이 필요하다면 Webpack Starter Template 를 참고하자.


CKEditor 5 Classic 를 선택하고 
② Commend line 를  복사하여 설치한다. 


③ VisualStudio Code 를 사용하여 개발을 진행하기 때문에 툴에서 명령어를 입력하며 추가한다. 

npm install --save @ckeditor/ckeditor5-build-classic

웹 프로그램에서 CKEditor 5 를 사용하려면 다음과 작업이 필요하다.

❶ 소스에서 CKEditor 5 Classic build 를 아래와 같이 import 한다.

   
// Using ES6 imports:
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';

   

❷ CKEditor 를 생성하는 코드를 추가한다. 

   
   ClassicEditor
    .create( document.querySelector( '#ckeditor' ) )
    .then( editor => {
        window.ckeditor = editor ;
    } )
    .catch( error => {
        console.error( error );
    } );
   
   

❸ HTML 코드에 CKEditor 5 가 위치할 태그를 추가한다. <textarea/> 아닌 태그를 사용해도 무관하다.

    
   
      
   
  
   
   


빌드 후 해당 웹 페이지를 확인해보면 이미지 업로드를 시도하면 내부적으로 "filerepository-no-upload-adapter: Upload adapter is not defined." 오류로 인하여 아무런 반응이 없다. (→ 개발자 콘솔에서 확인 가능)




Custom Image Upload Adaptor 구현을 통한 이미지 업로드

CKEditor5 에서 이미지를 업로드 하는 방법은 공식 업로드 어뎁터들을 사용하거나 직접 커스텀 어뎁터를 만들어 자유롭게 사용하는 방법이 있다.  자료를 검색해보니 어려워보이지 않아 커스텀 어뎁터를 만들어 사용해 보았다.

The visualization of the image upload process in a WYSIWYG editor.


가이드 및 Stackoverflow 내용을 바탕으로 구현해보았으나 이미지 파일이 [object Promise] 형태로 업로드 되는 이슈가 발생되었다. CKEditor 12.0.0 버전의 경우 커스텀 이미지 업로드 관련하여 유사한  버그가 있었다는 것을 확인할 수 있었다.  버전 문제가 아닌가 의심하여 @ckeditor/ckeditor5-build-classic 21.0.0, 20.0.0, 19.0.2 버전에서 테스트를 해보았으나 이미지 파일이 [object Promise] 형태로 업로드 되는 동일한 이슈가 발생되었다. 


결국 아래의 게시물에서 제시하는 트릭을 발견하여 문제를 해결할 수 있었다. 


❶ upload() , abort() 을 구현하는 CustomUploadAdaptor 클래스를 구현한다. 

class StudioUploadAdapter {
  constructor(loader) {
    // CKEditor5's FileLoader instance.
    this.loader = loader;
    // URL where to send files.
    this.url = `${API_ROOT_URL}/data/images/0/upload`;
  }
  // Starts the upload process.
  upload() {
    return new Promise((resolve, reject) => {
      this._initRequest();
      this._initListeners(resolve, reject);
      this._sendRequest();
    });
  }
  // Aborts the upload process.
  abort() {
    if (this.xhr) {
      this.xhr.abort();
    }
  }
  // Example implementation using XMLHttpRequest.
  _initRequest() {
    const xhr = (this.xhr = new XMLHttpRequest());
    xhr.open("POST", this.url, true);
    xhr.responseType = "json";
  }
  // Initializes XMLHttpRequest listeners.
  _initListeners(resolve, reject) {
    const xhr = this.xhr;
    const loader = this.loader;
    const genericErrorText = "Couldn't upload file:" + ` ${loader.file.name}.`;
    xhr.addEventListener("error", () => reject(genericErrorText));
    xhr.addEventListener("abort", () => reject());
    xhr.addEventListener("load", () => {
      const response = xhr.response;
      console.log(response);
      if (!response || response.error) {
        return reject(
          response && response.error ? response.error.message : genericErrorText
        );
      }
      // If the upload is successful, resolve the upload promise with an object containing
      // at least the "default" URL, pointing to the image on the server.
      resolve({
        default: getDownloadUrl(response[0]),
      });
    });
    if (xhr.upload) {
      xhr.upload.addEventListener("progress", (evt) => {
        if (evt.lengthComputable) {
          loader.uploadTotal = evt.total;
          loader.uploaded = evt.loaded;
        }
      });
    }
  }
  // Prepares the data and sends the request.
  _sendRequest() {
    const data = new FormData();
    let that = this;
    let file = that.loader.file;
    
    // set jwt token if required.
    that.xhr.setRequestHeader("Authorization", authHeader().Authorization);
    file.then(function (result) {
      //wait for the promise to finish then continue
      data.append("upload", result);
      that.xhr.send(data);
    });
  }
}

/**
 * return full image url with responsed JSON object
 * 
 * @param {*} item 
 */
function getDownloadUrl(item) {
  return encodeURI(`${API_ROOT_URL}/download/images/${item.linkId}`);
}


❷ 에디터에서 CustomUploadAdaptor 클래스를 업로드 어댑터로 활성화하는 FileRepository.createUploadAdapter() 팩토리 함수를 정의한다.

   

function StudioUploadAdapterPlugin(editor) {
  editor.plugins.get("FileRepository").createUploadAdapter = (loader) => {
    return new StudioUploadAdapter(loader);
  };
}

❸ 에디터를 생성할 때 config.extraPlugins 옵션을 사용하여 편집기에서 CustomUploadAdapterPlugin을 활성화합니다.

   
function createCKEditor5(renderTo, options, callback) {
  renderTo = renderTo || $("#ckeditor");
  var $headers = headerWithAuth({});
  return ClassicEditor.create(renderTo.get(0), {
    extraPlugins: [StudioUploadAdapterPlugin],
    mediaEmbed: {
      previewsInData: true,
      extraProviders: [
        {
          name: "me",
          url: /^.*\/download/,
          html: match => {
            const id = match[ 1 ];
            return 'hello';
          }
        },
      ],
    },
    image: {
      // Configure the available styles.
      styles: ["alignLeft", "alignCenter", "alignRight"],      
      // You need to configure the image toolbar, too, so it shows the new style
      // buttons as well as the resize buttons.
      toolbar: [
        "imageStyle:alignLeft",
        "imageStyle:alignCenter",
        "imageStyle:alignRight",
        "|",
        "imageTextAlternative",
      ],
    },
  })
    .then((editor) => {
      window.ckeditor = editor;
      if (isFunction(callback)) {
        callback(editor);
      }
    })
    .catch((error) => {
      console.error("There was a problem initializing the editor.", error);
    });
}


서버 API

서버 API 는 Spring 를 기반으로 업로드된 이미지에 대한 정보를 JSON 형태로 응답하게 작성하면 된다. 

    
    // 자신의 환경에 맞게 구현이 필요. 
    @Secured({ "ROLE_ADMINISTRATOR", "ROLE_SYSTEM", "ROLE_DEVELOPER", "ROLE_USER"})
    @RequestMapping(value = "/images/0/upload", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public List uploadAndRetureLink(
    		@RequestParam(value = "objectType", defaultValue = "-1", required = false) Integer objectType,
    		@RequestParam(value = "objectId", defaultValue = "-1", required = false) Long objectId,
    		@RequestParam(value = "imageId", defaultValue = "0", required = false) Long imageId,
    		@RequestHeader MultiValueMap headers,
    		MultipartHttpServletRequest request) throws NotFoundException, IOException, UnAuthorizedException { 
		
		User user = SecurityHelper.getUser();
		if( user.isAnonymous() )
		    throw new UnAuthorizedException(); 
		
		if( log.isDebugEnabled() ) {
			headers.forEach((key, value) -> {
		        log.debug(String.format( "Header '%s' = %s", key, value.stream().collect(Collectors.joining("|"))));
		    });
		}
		
		List list = new ArrayList(); 		
		Iterator names = request.getFileNames();
		while (names.hasNext()) {
		    String fileName = names.next();
		    log.debug("multipart name : {}", fileName );
		    MultipartFile mpf = request.getFile(fileName);
		    Image image = upload( user , objectType, objectId, imageId, mpf); 		    
		    ImageLink link = imageService.getImageLink(image, true);
		    link.setFilename(image.getName());
		    list.add( link ) ; 
		}
		return list ; 
    } 
    
    


사용후기

CKEditor5 는 컴포넌트 기반으로 동작하며 모듈화가 잘되어 있다. 처음 사용해보았지만 매뉴얼도 잘되어 있어 큰 어려움 없이 설치가 가능하였다. 

모든 기능을 모듈을 통하여 구현하게 되는 장점이 가장 불편하기도 하다. 즉 임으로 HTML 테그를 추가하는 것이 불가하기 때문이다. 이미지 업로드 후 원하는 형식으로 이미지 태그를 사용하고 싶은데 이를 위해서는 별도의 플러그 인을 만들어 사용해야 하는 것 같다.

2020년 7월 26일

JWT ( JSON WEB TOKEN ) 인증환경에서 이미지 다운로드

서버 세션이 아닌 JWT 토큰을 기반으로 하는 웹 프로그램은 서버와의 통신이 필요하면 HTTP 헤더에 JWT 토큰을 포함하여  요청하고 서버는 클라이언트 요청 헤더의  JWT 토큰을 꺼내어 인증 정보를 꺼네어 사용하게 된다. 이미지의 경우는 어떻까 ? 

HTML 에서 img  태그는 JWT 와 같은 보안 토큰을 헤더에 포함하는 기능을 제공하지 않는다. 그러나 이미지 데이터를 스크립트에서 다운로드하고  data : image / FILETYPE; base64 URI 체계를 사용하여 이미지를 웹 페이지에 포함시키는 방법을 사용하면 간단하게 제한을 극복할 수 있다. ( vue-auth-image 모듈이나 관련 글들을 참고하는 것이 많은 도움이 되었다.) 



다수의 이미지를 다운로드 하는 경우 요청간 지연을 주기 위하여 아래와 같은 방법을 사용했다.
  var delay = 600 ;
  $("#grid figure img[is-secure]").each(function () {
    var $this = $(this);
    delay = delay + 50 ;
    $this.parent().addClass('is-loading');
    studio.ui.setSecureImage($this, delay);
  });
  
이미지를 가져오지 못하는 경우에는 재요청을 수행하도록 코드하였다.
  
  function setSecureImage(elemment, delay, callback) {
  var delay = delay || 1000;
  let url = elemment.attr("secure-image");
  setImageAsBase64(
    url,
    function (data) {
      elemment.attr("src", data);
      elemment.parent().removeClass("is-loading");
      if (isFunction(callback)) callback();
    },
    function () {
      let maxRetry = 3,
        retry = 0;
      if (defined(elemment.data("max-retry"))) {
        maxRetry = elemment.data("max-retry");
      }
      if (defined(elemment.data("retry"))) {
        retry = elemment.data("retry");
      }
      if (retry < maxRetry) {
        elemment.data("retry", retry + 1);
        sleep(delay);
        setSecureImage(elemment);
      } else {
        elemment.parent().removeClass("is-loading");
      }
    }
  );
  }

  async function setImageAsBase64(url, successHandler, errorHandler) {
  const headers = {};
  // JWT 토큰 값 설정
  Object.assign(headers, studio.services.accounts.authHeader()); 
  await axios
    .get(url, { headers: headers, responseType: "arraybuffer" })
    .then((response) => {
      var mineType = response.headers["content-type"].toLowerCase();
      var b64encoded = Buffer.from(response.data, "binary").toString("base64");
      var prefix = "data:" + mineType + ";base64,";
      if (isFunction(successHandler)) successHandler(prefix + b64encoded);
    })
    .then((error) => {
      if (isFunction(errorHandler)) errorHandler();
    });
  } 
  



아쉽지만 이미지를 다운로드하여 data 방식으로 설정하는데 있어 오류가 발생되어 다시 시도하도록 추가한 코드는 효용성이 없는 것 같다. 테스트 환경 이슈일 수도 있으나 다운로드에 있어 무작위로 오류가 발생하며 일단 오류가 발생되면 다시 시도한 경우 역시 오류가 발생한다. (→ 서버 이슈인지 코드 이슈인지는 확실하지 않다.)

참고자료

2020년 6월 8일

Spring 기반 웹 프로그램에서 JWT 인증 사용하기

JWT ( JSON WEB TOKEN ) 는 URL 형식으로 구현할 수 있는 전자서명 기술이다. JWT 는 주요한 속성 정보를 RFC7519 표준에 따라 JSON 테이터 구조로 표현한다.

JWT 기술을 활용하면 손쉽게 클라이언트(앱) 기반 프로그램에서 토큰 기반 인증 및 REST API  호출을 구현할 수 있다.

클라이언트와 서버간의 인증 시나리오를 정리하면 아래와 같다. 

❶ 사용자가 로그인을 하면, ❷ 서버는 사용자의 정보를 기반으로한 JWT 토큰을 발급한다.
그 후, ❸ 사용자가 서버에 요청을 할 때 마다 JWT를 포함하여 전달한다. ❹ 서버는 클라이언트에서 요청을 받을때 마다, 해당 토큰이 유효하고 인증됐는지 검증을 하고, 사용자가 요청한 작업에 권한이 있는지 확인하여 작업을 처리.



Spring 기반의 서버 사이드  JWT

다행스러운 것은 JWT 토큰 생성과 검증을 위한 라이브러리가  JWT 사이트에서 다양한 언어 버전으로 제공하고 있어 적절하게 이를 활용하여 손쉽게 JWT 를 적용할 수 있다.



글에서는  "https://github.com/jwtk/jjwt" 를 사용하였다. 


서버 프로그램 개발환경이 maven 으로 구현되어 있어 아래와 같이 라이브러리 의존성을 추가하였다. 

<!--  JWT  -->

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->

<dependency>

    <groupId>io.jsonwebtoken</groupId>

    <artifactId>jjwt</artifactId>

            <version>0.9.1</version>

</dependency>


서버 프로그램이 Spring 을 기반으로 하고 있어 Spring Security 설정에 JWT 처리를 위한 커스텀 필터를 구현하여 추가하였다. (커스텀 필터는 클라이언트에서 헤더에 JWT 토큰을 가지고 오는 경우 이를 검증하고 인증 정보를 보안 컨텍스트에 추가하는 역할을 수행한다.)

package architecture.community.security.spring.authentication.jwt;

import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;

public class JwtTokenProvider {

	private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);

	private static final String AUTHORITIES_KEY = "auth";

	/**
	 * JWT Token expire time.
	 */
	static final long EXPIRATIONTIME = 864_000_000; // 10 days

	/**
	 * Secret Key string
	 */
	static final String SECRET = "ThisIsASecret";

	/**
	 * JWT Token prefix
	 */
	static final String TOKEN_PREFIX = "Bearer";

	/**
	 * Header key for JWT Token
	 */
	static final String HEADER_STRING = "Authorization";

	public String createToken(Authentication authentication) {
		String authorities = authentication.getAuthorities().stream().map(authority -> authority.getAuthority())
				.collect(Collectors.joining(","));
		ZonedDateTime now = ZonedDateTime.now();
		ZonedDateTime expirationDateTime = now.plus(EXPIRATIONTIME, ChronoUnit.MILLIS);
		Date issueDate = Date.from(now.toInstant());
		Date expirationDate = Date.from(expirationDateTime.toInstant());
		return Jwts.builder().setSubject(authentication.getName()).claim(AUTHORITIES_KEY, authorities)
				.signWith(SignatureAlgorithm.HS512, SECRET).setIssuedAt(issueDate).setExpiration(expirationDate)
				.compact();
	}

	public Authentication getAuthentication(String token, UserDetailsService userDetailsService, boolean refresh) {
		Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
		Collection authorities = Arrays
				.asList(claims.get(AUTHORITIES_KEY).toString().split(",")).stream()
				.map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList());
		UserDetails details = userDetailsService.loadUserByUsername(claims.getSubject());
		return new UsernamePasswordAuthenticationToken(details, "", refresh ? details.getAuthorities() : authorities);
	}

	public Authentication getAuthentication(String token) {

		Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
		Collection authorities = Arrays
				.asList(claims.get(AUTHORITIES_KEY).toString().split(",")).stream()
				.map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList());

		User principal = new User(claims.getSubject(), "", authorities);

		return new UsernamePasswordAuthenticationToken(principal, "", authorities);

	}

	public boolean validateToken(String authToken) {
		try {
			Jwts.parser().setSigningKey(SECRET).parseClaimsJws(authToken);
			return true;
		} catch (SignatureException e) {
			logger.info("Invalid JWT signature: " + e.getMessage());
			logger.debug("Exception " + e.getMessage(), e);
		} catch (MalformedJwtException e) {
			logger.error("Invalid JWT token: {}", e.getMessage());
		} catch (ExpiredJwtException e) {
			logger.error("JWT token is expired: {}", e.getMessage());
		} catch (UnsupportedJwtException e) {
			logger.error("JWT token is unsupported: {}", e.getMessage());
		} catch (IllegalArgumentException e) {
			logger.error("JWT claims string is empty: {}", e.getMessage());
		}
		return false;
	}
}

(JwtTokenProvider 사용하여 Jwt 토큰을 검증하고 userDetailsService 을 이용하여 보안 컨텍스트에 인증 객체를 설정한다.)
package architecture.community.security.spring.authentication.jwt;

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;

import architecture.ee.util.StringUtils;
import io.jsonwebtoken.ExpiredJwtException;

public class JWTFilter extends GenericFilterBean {

	private Logger logger = LoggerFactory.getLogger(getClass());

	private final JwtTokenProvider jwtTokenProvider;

	@Autowired(required = false)
	@Qualifier("userDetailsService")
	private UserDetailsService userDetailsService;

	public JWTFilter(JwtTokenProvider jwtTokenProvider) {
		Assert.notNull(jwtTokenProvider, "JwtTokenProvider cannot be null");
		this.jwtTokenProvider = jwtTokenProvider;
	}

	@Override
	public void afterPropertiesSet() {
		Assert.notNull(jwtTokenProvider, "JwtTokenProvider cannot be null");
	}

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		String header = request.getHeader(JwtTokenProvider.HEADER_STRING);
		if (StringUtils.isNullOrEmpty(header)) {
			chain.doFilter(request, response);
			return;
		}

		try {
			String jwt = this.resolveToken(request);
			if (!StringUtils.isNullOrEmpty(jwt)) {
				logger.debug("jwt token : {}", jwt);
				if (this.jwtTokenProvider.validateToken(jwt)) {
					Authentication authentication;
					if (userDetailsService != null) {
						authentication = this.jwtTokenProvider.getAuthentication(jwt, userDetailsService, true);
					} else {
						authentication = this.jwtTokenProvider.getAuthentication(jwt);
					}
					SecurityContextHolder.getContext().setAuthentication(authentication);
				}
			}
		} catch (ExpiredJwtException eje) {
			logger.info("Security exception for user {} - {}", eje.getClaims().getSubject(), eje.getMessage());
			response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
			logger.debug("Exception " + eje.getMessage(), eje);
			return;
		}

		chain.doFilter(request, response);
		this.resetAuthenticationAfterRequest();
	}

	private void resetAuthenticationAfterRequest() {
		logger.debug("reset authentication as null.");
		SecurityContextHolder.getContext().setAuthentication(null);
	}

	private String resolveToken(HttpServletRequest request) {
		String bearerToken = request.getHeader(JwtTokenProvider.HEADER_STRING);
		if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(JwtTokenProvider.TOKEN_PREFIX)) {
			String jwt = bearerToken.substring(7, bearerToken.length());
			return jwt;
		}
		return null;
	}
}  
  
필터를 정의하고 사용자 정의 필터를 추가해준다.

<http auto-config="true" use-expressions="true" disable-url-rewriting="true">

  <cors configuration-source-ref="corsSource"/> 

  <headers>

    <frame-options policy="SAMEORIGIN" />

  </headers>

  <intercept-url pattern="/*" access="permitAll"/>

<intercept-url pattern="/error/*" access="permitAll"/>

<intercept-url pattern="/data/**" access="permitAll"/>

<intercept-url pattern="/display/**" access="permitAll"/>

<intercept-url pattern="/accounts/**" access="permitAll"/>

<intercept-url pattern="/secure/studio/**" access="hasRole('ROLE_ADMINISTRATOR') or hasRole('ROLE_SYSTEM') or hasRole('ROLE_DEVELOPER')" />

<intercept-url pattern="/secure/data/**" access="hasRole('ROLE_USER') or hasRole('ROLE_SYSTEM')" /> 

  <!-- Form Login Page Setting -->

  <form-login login-page="/accounts/login"

username-parameter="username" 

password-parameter="password"

login-processing-url="/accounts/auth/login_check"

authentication-success-handler-ref="authenticationSuccessHandler"

authentication-failure-handler-ref="authenticationFailureHandler" />

  <http-basic />

  <custom-filter before="BASIC_AUTH_FILTER" ref="jwtFilter" />


  ...


</http>


<beans:bean id="jwtTokenProvider" class="architecture.community.security.spring.authentication.jwt.JwtTokenProvider"></beans:bean>

<beans:bean id="jwtFilter" class="architecture.community.security.spring.authentication.jwt.JWTFilter">

  <beans:constructor-arg ref="jwtTokenProvider" />

</beans:bean>



③ CORS 지원
주의할 것은 보안상 이유에서 웹 브라우져는 스크립트에서 웹페이지가 로드된 원 도메인(IP)이 아닌 타 도메인(IP) 자원을 호출하는 것을 금지하고 있다. 

CORS(Cross-origin resource sharing) 는 대부분의 웹 브라우져들이 구현하고 있는 W3C 표준이며 이를 통하여 다른 도메인의 자원을 호출 할 수 있게 할 수 있다. Spring Security 는 필터 기반의 CORS 를 지원하고 있어 아래와 같이 손쉽게 처리가 가능하다. 

<http auto-config="true" use-expressions="true" disable-url-rewriting="true">

  <cors configuration-source-ref="corsSource"/> 

  <headers>

    <frame-options policy="SAMEORIGIN" />

  </headers>

  <intercept-url pattern="/*" access="permitAll"/>

<intercept-url pattern="/error/*" access="permitAll"/>

<intercept-url pattern="/data/**" access="permitAll"/>

<intercept-url pattern="/display/**" access="permitAll"/>

<intercept-url pattern="/accounts/**" access="permitAll"/>

<intercept-url pattern="/secure/studio/**" access="hasRole('ROLE_ADMINISTRATOR') or hasRole('ROLE_SYSTEM') or hasRole('ROLE_DEVELOPER')" />

<intercept-url pattern="/secure/data/**" access="hasRole('ROLE_USER') or hasRole('ROLE_SYSTEM')" /> 

  <!-- Form Login Page Setting -->

  <form-login login-page="/accounts/login"

username-parameter="username" 

password-parameter="password"

login-processing-url="/accounts/auth/login_check"

authentication-success-handler-ref="authenticationSuccessHandler"

authentication-failure-handler-ref="authenticationFailureHandler" />

  <http-basic />

  ...


</http>


<beans:bean id="corsSource" class="org.springframework.web.cors.UrlBasedCorsConfigurationSource">

  <beans:property name="corsConfigurations">

    <util:map>

      <beans:entry key="/**">

      <beans:bean class="org.springframework.web.cors.CorsConfiguration">

      <beans:property name="allowCredentials" value="true"/>

      <beans:property name="allowedHeaders">

      <beans:list>

        <beans:value>Authorization</beans:value>

        <beans:value>Content-Type</beans:value>

        <beans:value>responseType</beans:value>

        <beans:value>encoding</beans:value>

      </beans:list>

      </beans:property>

      <beans:property name="allowedMethods">

        <beans:list>

        <beans:value>POST</beans:value>

        <beans:value>GET</beans:value>

        <beans:value>PUT</beans:value>

        <beans:value>DELETE</beans:value>

        <beans:value>OPTIONS</beans:value>

        </beans:list>

      </beans:property>

      <beans:property name="allowedOrigins" value="*" />

      <beans:property name="exposedHeaders">

      <beans:list>

        <beans:value>Location</beans:value>

        <beans:value>Content-Disposition</beans:value>

      </beans:list>

      </beans:property>

      <beans:property name="maxAge" value="86400" /></beans:bean>

    </beans:entry>

    </util:map>

  </beans:property>

</beans:bean>


JWT 기반 인증 및 검증

아이디/비밀번호 기반의 인증에서 JWT 토튼을 사용하려면 인증이 성공하면 클라이언트에 토큰 값을 응답해주어야 한다. 아래는 스프링 컨트롤러에 간단하게 로그인 인증후 Jwt 토큰 값을 리턴하는 예이다.

LoginRequest.java

@RequestMapping(value = "/signin.json", method = { RequestMethod.POST})

public ResponseEntity<JwtResponse> authenticateUser(@RequestBody LoginRequest loginRequest) { 

Authentication authentication = authenticationManager.authenticate(

new UsernamePasswordAuthenticationToken(

loginRequest.getUsername(), 

loginRequest.getPassword()));

SecurityContextHolder.getContext().setAuthentication(authentication);

String jwt = jwtTokenProvider.createToken(authentication); 

CommuintyUserDetails details = SecurityHelper.getUserDetails(authentication);

return ResponseEntity.ok( 

            new JwtResponse(

                jwt

                details.getUser(), 

                getRoles(details.getAuthorities())));


웹 프로그램은 아래와 같이 인증후 획득한 토큰값을 저장하고 서버와 통신이 필요한 경우 헤더에 토큰 값을 포함하여 전달하게 된다. 서버 통신은 axois 를 사용하여 구현하였다. 참고로 코드 구현에 있어 UI 부분은 kendoui 를 사용하였다. ( kendoui 를 사용하려면 개발자라이선스가 필요함)

var renderTo = $('form[name=form-signin]');
var observable = new kendo.observable({
    username : null,
    password : null
});

var validator = renderTo.kendoValidator({
  errorTemplate: "

#=message#

" }).data("kendoValidator");; kendo.bind( renderTo , observable ); renderTo.submit(function(e) { e.preventDefault(); if( validator.validate() ){ kendo.ui.progress(renderTo, true); axios.post(studio.services.getApiUrl('/data/accounts/signin.json'), JSON.stringify(observable), { headers: { "Content-Type": "application/json" }} ).then(response => { const data = response.data; studio.services.accounts.loginSuccess(studio.services.accounts.state, data); window.location.replace("index.html"); }).catch(function (error) { // handle error studio.ui.handleAxiosError(error); observable.set('password', null); }) .then(function () { // always executed kendo.ui.progress(renderTo, false); }); } });

index.js
 
 const user = JSON.parse(localStorage.getItem("user"));
 const initialState = user
  ? { status: { loggedIn: true }, user }
  : { status: { loggedIn: false }, user: null };
  
 function authHeader() {
  // return authorization header with jwt token
  let _user = JSON.parse(localStorage.getItem("user"));
  if (_user && _user.jwtToken) {
    return { Authorization: "Bearer " + _user.jwtToken };
  } else {
    return {};
  }
  }

 function loginSuccess(state, data) {
  if (data.jwtToken) {
    // store user details and jwt token in local storage to keep user logged in between page refreshe
    localStorage.setItem("user", JSON.stringify(data));
  }
  state.loggedIn = true;
  state.user = data;
}

서버 호출이 필요한 경우 아래와 같은 방식으로 헤더에 jwt 포함하여 전송한다. 

sttings-locale.html 
const headers = {
  Accept: "application/json",
  "Content-Type": "application/json"
};
Object.assign(headers, studio.services.accounts.authHeader());

...

axios({
  url: studio.services.getApiUrl('/data/secure/mgmt/locale/save-or-update.json'),
  method: "post",
  data: JSON.stringify({ locale: $this.locale, timeZone: $this.timezone }),
  headers: headers
}).then(response => {
  let data = response.data;
  dialog.close();
}).catch(function (error) {
  studio.ui.handleAxiosError(error);
}).then(function () {
  // always executed
  kendo.ui.progress($('.k-dialog'), false);
});
                      

JWT 기반 인증 적용 후  경험한 문제들

  • spring security 에서 중복로그인 방지를 설정한 경우 중복 로그인 오류로 인하여 정상적인 응답을 받지 못하는 경우가 다수 발생했다. 
  • 인증된 사용자에게만 보여지는 이미지와 같은 바이너리 형식을 처리하는 경우에 어려움이 있었다. 

참고자료

Securing Spring Boot with JWTs
JWT (JSON Web Token) 이해와 활용
CORS support in Spring Framework