검색

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

2024년 10월 26일

코딩 - 자바스크립트 CryptoJS 와 JAVA 간의 하이브리드 암호화 (RSA+AES)

클라이언트와 서버간의 안전한 통신을 위하여 간단하게 암호화를 만들어보았다. 작업 환경은 아래와 같다. 

◼︎ 환경

  • HW : MacBook Pro (14-inch, 2021)
  • CPU : Apple M1 Pro
  • MENORY : 16GB
  • DISK : 512 GB SSD
  • OS : macOS 15.0.1 (24A348)
  • TOOLS : Visual Studio Code, Java 11 
  • Programming Language : Java, HTML, JavaScript 


개인적으로 데이터 암호화는 AES (Advanced Encryption Standard) 알고리즘을 사용한다. AES는 대칭키 알고리즘 중 하나로, 데이터 암호화와 복호화에 동일한 키를 사용한다. 
  • AES 키: 대칭키 암호화에서 데이터를 암호화하고 복호화하는데 사용되는 비밀 키. AES 키는 128, 192, 또는 256비트로 설정될 수 있다.
  • IV (Initialization Vector): AES-CBC 모드에서 사용되는 추가 보안 요소. IV는 암호화할 때마다 무작위로 생성되고, 암호화된 데이터와 함께 전송됩다.
  • Base64 인코딩: AES 키와 IV는 바이너리 데이터이므로, 이를 텍스트 형식으로 저장하고 전송하기 위해 Base64로 인코딩한다.
서버/클라이언트 환경을 고려하면 대칭키 방식의 암호화/복호화는 서버와 클라이언트 모두가 AES 키 와 IV 값을 미리 알고 있어야 한다.  키 관리의 위험성을 고려하면 암호화 키를 서버와 클라이언트가 공유 하는 대칭키 방식 보다는 비대칭키를 사용하는 것이 더 키 관리에 안전하다고 할 수 있다. 

대칭키와 비대칭키 암호화의 장점과 단점은 아래와 같다.

대칭키 암호화 (Symmetric Encryption)
① 특징:
  • 암호화와 복호화에 같은 키를 사용.
  • AES(Advanced Encryption Standard)와 같은 알고리즘이 대칭키 암호화에 해당.
② 장점:
  • 빠른 속도: 대칭키 암호화는 비대칭키보다 훨씬 빠름. 특히 대량의 데이터를 암호화할 때 성능이 우수함.
  • 간단한 구현: 동일한 키로 암호화와 복호화를 수행하기 때문에 상대적으로 간단하게 구현할 수 있음.
③ 단점:
  • 키 관리 문제: 대칭키를 안전하게 공유하는 것이 문제. 만약 네트워크를 통해 키를 전송해야 한다면, 키가 중간에서 가로채기당할 위험이 있음.
  • 보안성 문제: 키가 노출되면 데이터를 보호할 방법이 없음. 키 교환이 안전하지 않다면, 대칭키 암호화는 취약할 수 있음.

비대칭키 암호화 (Asymmetric Encryption)
① 특징:
  • 암호화에는 공개키를, 복호화에는 개인키를 사용.
  • RSA(Rivest-Shamir-Adleman)와 같은 알고리즘이 비대칭키 암호화에 해당.
② 장점:
  • 보안성: 비대칭키 암호화는 공개키를 사용해 암호화하므로 키 교환이 안전. 공개키는 누구나 사용할 수 있지만, 개인키는 소유자만이 알고 있기 때문에 보안성이 뛰어남.
  • 키 관리 용이: 공개키는 네트워크를 통해 쉽게 공유할 수 있고, 개인키는 비밀로 유지되므로 키 교환이 안전하게 구현됨.
③ 단점:
  • 속도 문제: 비대칭키 암호화는 대칭키 암호화에 비해 훨씬 느림. 특히 대량의 데이터를 암호화하는 데 비효율적임.
  • 데이터 크기 제한: RSA와 같은 비대칭키 알고리즘은 암호화할 수 있는 데이터의 크기가 키 길이에 의해 제한됨.
이들 암호화 방식의 특성을 고려하면 서버/클라이언트 환경에서는  TLS/SSL 프로토콜(HTTPS) 등에서 널리 사용되는 비대칭키와 대칭키를 결합한 하이브리드 암호화 방식이 가장 안전하고 효율적이라고 할 수 있다. 

참고로 아래는 TLS/SSL 프로토콜(HTTPS) 을 쉽게 설명하는 이미지 이다. (출처https://has3ong.github.io/computer%20science/ssl-tls/ )


클라이언트/서버간의 안전한 데이터 암호화를 위한 하이브리드 방식은 아래와 같은 절차를 따른다.
  1. 서버는 클라이언트(웹) 에게 RSA 공개키를 배포
  2. 클라이언트는 랜덤 생성된 AES 키를 사용하여 데이터를 암호화
  3. RSA 공개키로 AES 키를 암호화 하고 암호화된 데이터와 IV 값을 함께 서버에 전달
  4. 서버는 RSA 개인키로 암호화된 AES 키를 복호화하고 함께 전달된 IV 값을 사용하여 데이터를 복호화



하이브리드 방식의 암호화 방식을 적용하면 적은 노력으로 손쉽게 클라이언트와 서버간의 암호화 통신을 구현할 수 있다.  

하이브리드 방식 암호화 구현에 있어 클라이언트에서 ❶ AES 암호화는 CryptoJS (JavaScript library of crypto standards) 을 사용하였고 ❷ RSA 암호화는 웹 암호화 API 의 일부로 제공되는 crypto.subtle 을 사용하였다. 주의 할 점은 crypto.subtle API는모든 브라우저에서 지원하지 않으며, 특히 일부 구형 브라우저나 특정 모바일 브라우저에서는 지원되지 않을 수 있다. 서버의 경우 기본 자바의 암호화 기능을 사용하였다.

Can I Use - Web Cryptography API 사이트를 통하여 호환성을 확인해 볼 수 있다. 이 사이트에 따르면 대부분의 웹 브라우저에서 지원된다. 

하이브리드 방식 암호화 구현하기

서버는 AES 암호화를 위한 대칭키를 암호화기 위하여 먼저 RSA 비대칭키  생성한다


비밀키는 안전하게 보관하고 공개키는 클라이언트에 전달한다. 클라이언트는 AES 키를 생성하여 데이터를 암호화하고 AES 키는 서버가 공개한 RAS 공개키로 암호화를 한다.

Encrypt Data 버튼을 클릭하여 데이터를 암호화 한다.

이제 공개키로 암호화된 AES 키 값, AES 로 암호화된 데이터와 IV 값을 서버에 전달한다.
  • 공개키로 암호화된 AES 키 : BASE64로 인코딩 
  • IV : BASE64로 인코딩 
  • AES 로 암호화된 데이터 : BASE64로 인코딩

서버는 비밀키로 암호화된 대칭키 값을 복호화하고 키를 사용하여 암호화된 데이터를 복호화 한다.

참고자료 

  • ChatGPT 4o : 코드는 ChatGPT 4o 을 사용하여 작성.

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년 1월 15일

자바스크립트에서 미국표준시로 날짜 보여주기


웹 프로그램에서 사용자에게 보여지는 시간을 PST 표준시로 보여주고자 한다. 이를 구현하기 위하여 아래와 같은 방법을 사용하였다.

❶ 서버는 시간 정보를 협정 세계시(UTC, Coordinated Universal Time) 로 ISO-8601 (Date elements and interchange formats - Information interchange - Representation of dates and times) 국제 표준 날짜 표기 기준을 사용하여 클라이언트에 데이터를 전달하고 ❷ 클라이언트는 UTC 데이터를 사용하여 원하는 시간대의 형태로 변경하여 보여준다.



Spring 기반의 서버 프로그램은 데이터를 JSON 형식으로 전달하며 객체의 Date 타입 필드값에 대하여 어노테이션을 사용하여 ISO-8601 형식으로 변환되도록 하였다.

UTC 기준 시간 변환을 위하여  "yyyy-MM-dd'T'hh:mm:ss.SSS+00:00"  패턴 값을 사용하였다.


class OpenRsc implements Serializable{

 int rsc_id;
 String title;
 String description;    
 int viewCount; 
 String linkId;
 String creationUser;
 @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'hh:mm:ss.SSS+00:00" )
 Date creationDate;
 String modifiedUser;
 @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'hh:mm:ss.SSS+00:00" )
 Date modifiedDate;  
  
 public OpenRsc(int rsc_id, String title, String description, int viewCount, String linkId,
       String creationUser, Date creationDate, String modifiedUser, Date modifiedDate ) {
  super();
  this.rsc_id = rsc_id;
  this.title = title;
  this.description = description;
  this.viewCount = viewCount;
  this.linkId = linkId;
  this.creationUser = creationUser;
  this.creationDate = creationDate;
  this.modifiedUser = modifiedUser;
  this.modifiedDate = modifiedDate;
 } 
}

서버 시간이 한국 표준시를 기준으로 2020년 1월 15일 17시 54분 인 경우 ISO-8606 형식으로 변환을 하면  2020-01-15T17:52:00.000+00:00  값이 된다.

자바스크립트에서 서버에서 응답한 UTC 기준 시간 값을 인자로 Date 객체를 생성하면 클라이언트 타임존이 한국표준시인 경우 Thu Jan 16 2020 02:52:00 GMT+0900 (한국 표준시) Date 값이 생성된다.

new Date ('2020-01-15T17:52:00.000+00:00')

자바스크립트에서 다른 시간대로 보여주려면 Moment 스크립트를 사용하면 된다. 먼저 사용을 위하여 아래와 같이 자바 스크립트를 HTML 에 추가한 다음 원하는 타입존 값을 지정하여 변환하면 된다.







참고 자료

2018년 5월 23일

스프링 기반의 Server-Sent Events 기술을 이용한 웹 기반 알림 구현


HTML5 Server-Sent Events (이하 SSE )를 이용하면 비교적 손쉽게 필요할때마다 클라이언트에 데이터를 전송하는 서버 PUSH 구현이 가능하다.

그럼 SSE 는 무엇인가?

전통적인 웹 기술로 폴링(POLLING) 불리우는 것이 있다.
이 기술은 응용프로그램이 요청을 통하여 서버에서 데이터를 반복적으로 가져오는 것으로 AJAX 에서 널이 사용하고 있다.  단점이라면 클라이언트는 요청을 하고 서버의 응답을 기다리는 것으로 경우에따라서는 의미없는 데이터를 받는 경우가 많기 때문에 HTTP 오버헤드를 유발하게 된다. (서버가 전달할 메시지가 있는지 없는 지를 알수 없기 때문에 주기적으로 호출해야 함.)

보다 개선된 방법으로 긴 폴링(Hanging GET/COMET) 기술이 있다. 긴 폴링은 서버는 새로운 데이터가 없는 경우 클라이언트로 부터의 요청을 보류한다.  새로운 데이터가 있는 경우 응답을 하고 연결을 종료하게 된다. 가장 손쉬운 예가 iframe에 스크립트를 추가하여 서버의 응답이 있는 경우 콘텐츠가 갱신된도록 구현하는 것이 될 것 같다.

SSE 는 이러한 전통적인 웹 기술들과 다르게 처음부터 효율적으로 메시지를 클라이언트에 보낼 수 있도록 설계되었다. SSE를 사용하여 통신 할 때 서버는 초기 요청을 하지 않고도 필요할 때마다 데이터를 클라이언트에게 푸시 할 수 있다. 즉, 서버에서 클라이언트로 업데이트된 데이터를 스트리밍 할 수 있다는 의미이다. 비교적 많이 알려진 WebSocket 기술과 비교하면 양방향이 아닌 단방향 통신을 지원하며 별도의 서버구현 또는 프로토콜 없이 HTTP 기술을 통하여 구현할 수 있다는 점이다. (이점이 아주 매력적이다.)


SSE 기술을 이용한 스프링 환경에서 웹 푸시 구현


간단한 이슈관리 웹 프로그램이 있다. 이 웹 프로그램에서 새로운 이슈 또는 변경이 발생하는 경우 클라이언트에게 알림을 보여주는 것을 구현해보자. 여기에 브라우져가 PWA 를 지원하는 경우 이를 이용한 푸시 알람도 같이 구현해보자. 

아쉬은 것은 아직까지 익스플로러는 SSE 기술을 지원하지 않아 사용할 수 없다는 점이다. 다행스러운 것은 아주 방법이 없는 것은 아니여서 polyfill 스크립트를 사용하여 IE10, IE11에서 동작하도록 했다.


SSE 기술을 지원하는 브라우져
BrowserSupportedNotes
Internet ExplorerNo
Mozilla FirefoxYesStarting with Firefox 6.0
Google ChromeYesStarting with Chrome 6
OperaYesStarting with Opera 11
SafariYesStarting with Safari 5.0

개발 환경 


  • Springframework 4.3.2 
  • Tomcat 8.0
  • Java 8.3.2 


개발방법

이슈 생성/변경시 푸시 알람을 구현하기 위하여 서버 프로그램은 

  1. 새로운 이슈가 등록되거나 변경되면 
  2. Springframework 의 비동기 이벤트 기술을 사용하여 이벤트를 발생시킨다.
  3. Springframework 에서 지원하는 SSE기능을 사용하여 발생된 이벤트를 클라이언트에 전송한다.
그림 1. PUSH 알림 동작 원리


스프링 컨트롤러에서 SSE 사용하기 

스프링은 SseEmitter 불리우는 클래스를 사용하여 HTTP 응답을 text/event-stream 타입으로 리턴한다.
이를 위해서 아래와 같이 web.xml 파일에 sync-support 를 반듯이 추가해주어야 한다.
    
        community-servlet
         org.springframework.web.servlet.DispatcherServlet
        
            contextConfigLocation
            
                /WEB-INF/servlet-config/community-servlet-context.xml
            
        
        1
        true
    

이슈를 저장하는 서비스는 아래와 같은 형식으로 코딩한다. 저장이 완료되면 Spring 에서 제공하는 EventPublisher 를 사용하여 이벤트를 발생시키면 된다. 

package architecture.community.projects;
public class DefaultProjectService extends EventSupport implements ProjectService {
    private Logger logger = LoggerFactory.getLogger(getClass().getName());
    
    @Autowired(required = false) 
    private ApplicationEventPublisher eventPublisher;

    public DefaultProjectService(){}    

    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
    public void saveOrUpdateIssue(Issue issue) {
        boolean isNew = true;
        IssueStateChangeEvent.State state ;

        if( issue.getIssueId() > 0 ) {
            isNew = false;
            state = IssueStateChangeEvent.State.UPDATED;
        }else {
            state = IssueStateChangeEvent.State.CREATED;
        }

        if( issue.getIssueId() > 0 && projectIssueCache.get(issue.getIssueId()) != null  ) {
            projectIssueCache.remove(issue.getIssueId());
        } 

        clearProjectStats(issue.getObjectId());
        logger.debug("save or update user {}" , issue );
        projectDao.saveOrUpdateIssue(issue);

        if( eventPublisher != null)
            eventPublisher.fireEvent(new IssueStateChangeEvent( issue, SecurityHelper.getUser(), state ));        
    }
}

컨트롤러 코드에서는 아래와 같은 방식으로 이벤트를 리스닝하다 이벤트가 발생되면 아래와 같은 방법으로 코딩을 한다.

package architecture.community.web.spring.controller.data.v1;


import java.util.ArrayList;

import java.util.List;

import java.util.concurrent.CopyOnWriteArrayList;


import org.springframework.context.event.EventListener;

import org.springframework.security.access.annotation.Secured;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;


import architecture.community.projects.event.IssueStateChangeEvent;


@Controller("sse-data-controller")
@RequestMapping("/data/api/v1/notifications")

public class SSEController {


    private final CopyOnWriteArrayList emitters = new CopyOnWriteArrayList<>();


    public SSEController() {

    }


    @Secured({ "ROLE_USER" })

    @GetMapping("/issue.json")

    public SseEmitter handle() { 

        final SseEmitter emitter = new SseEmitter();

        this.emitters.add(emitter); 

        emitter.onCompletion( new Runnable() {

            public void run() {

                emitters.remove(emitter); 

            }

        }); 

        emitter.onTimeout( new Runnable() {

            public void run() {

                emitters.remove(emitter); 

            }
        });
        return emitter;
    }

    @EventListener
    public void onIssueStateChangeEvent(IssueStateChangeEvent event) {
        List deadEmitters = new ArrayList<>();
        for(SseEmitter emitter : emitters ) { 
            try {
                emitter.send(event);
            } catch (Exception e) {
                deadEmitters.add(emitter);
            }     
        }
        this.emitters.removeAll(deadEmitters);
    } 
}

클라이언트 즉 웹 페이지에서 PUSH 알림 구현하기

먼저  IE 는 SSE 를 지원하지 않기 때문에 EventSource.js 파일을 다운받아 사용한다.
사용하는 방법은 기존 EventSource 와 동일하다. 참고로 데스크탑 알림은 반듯이 웹 사이트가 https 인경우에만 동작하기 때문에 이경우는 다른 방법을 알림을 제공하도록 kendoui 를 사용하여 구현하였다.

    function createNotification(observable){ 

        console.log('show notification... : ' + observable.get('notificationEnabled') );
        console.log("create notification..");
        if(window.Notification){
            if( Notification.permission == 'denied' ){
                observable.set('notificationEnabled', false);
            }else{
                observable.set('notificationEnabled', true);
            }    
        }            

        var template = kendo.template($("#notification-template").html());
        const eventSource = new EventSource('/data/api/v1/notifications/issue.json'); 

        eventSource.onmessage = function(e) { 
            console.log('msg: ' + e.data);
            var obj = JSON.parse(e.data);
            var title = "";
            if( obj.state == 'CREATED' ){
                title = "신규 이슈 알림";
            }else {
                title = "이슈 변경 알림";
            } 

            if(observable.get('notificationEnabled')){
                 var notification = new Notification(title, {
                        body: template(obj),
                        icon: iconDataURI
                });         
            }else{
                title = title + " : " + new Date().toLocaleTimeString() ;
                community.ui.notification({ 
                    autoHideAfter:0, 
                    allowHideAfter: 0,
                    width : 500,
                    templates : [{
                        type : "alert",
                        template : '
#if(title){#
#= title #
#}#
#= message #
' }] }).show({ title:title, 'message': template(obj), time: new Date().toLocaleTimeString() },"alert"); } return; } }

일반적인 웹 페이지에서 알림이 발생된 경우 예시 화면은 아래와 같다.



참고 사이트

Server-Sent Events with Spring
Use Server-Sent Event in Spring 4.2
Spring Events
프로그레시브 웹앱(PWA) 푸시 알람 A to Z
W3C Server-Sent Events
What is a Polyfill
Stream Updates with Server-Sent Events
Is there a Microsoft equivalent for HTML5 Server-Sent Events?

2017년 11월 22일

YUI Compressor 을 이용한 웹 프로그램 성능 개선

웹브라우저를 통하여 실행되는 웹 프로그램은 가능한 가볍게 만드는 것이 중요하다.
가벼운 웹 프로그램을 만드는 가장 간단한 작업은 코드 축소, HTTP 압축등이 있다.

코드 축소와 관련하여 자바 스크립트 코드를 축소하는데 가장 널리 사용되는 도구에는 여러가지가 있지만 개인적으로는 YUI Compressor 를 선호한다.

참고로 CompressorRater 웹 사이트에서는 여러 도구들에 대한 성능 테스트할 수 있는 웹 기반의 도구를 제공하고 있다.

Eclipse Plugin 설치

Eclipse 에서 설치는 아주 간단하다. 다음의 버튼을 드레그하여 실행중인 Eclipse 에 끌어다 놓으면 설치가 진행된다. ( 플러그인 설치 사이트 )

Drag to your running Eclipse* workspace. *Requires Eclipse Marketplace Client

Eclipse 에서 사용하기 

코드 축소 대상을 선택하고 오른쪽 마우스 버튼을 클릭후 External Tools > Compress CSS 선택한다.
주의할 것은 동일 이름의 파일이 이미 존재하는 경우는 정상적으로 코드 축소 파일이 생성되지 않는다.


Eclipse Maven 에서 사용하기 

프로젝트를 Maven 을 사용하여 빌드하고 있다면 pom.xm 파일에 코드 압축 관련 설정을 통하여 빌드시 자동으로 처리되도록 할 수 있다.

먼저 YUI Compressor Maven plugin 플러그인을 사용하도록 pom.xml 파일을 아래와 같이 수정해주어야한다.

 

   net.alchim31.maven
    yuicompressor-maven-plugin
    1.5.1
    
        
            
                compress
            
        
    



다음으로 프로젝트의 스크립트 및 CSS 소스가 WebContent 폴더에 존재하고 아래와 같은 구조로 되어있다고 가정하자.


WebContent
├── css
    ├── community.ui
    └── bootstrap
├── js
└── fonts


또한 WebContent/css/community.ui 폴더의 특정 css 들만을 소스 압축하려고 한다고 하면 그리고 압축파일은 min.css 형식을 원한다면 다음과 같은 설정이 필요하다.

 

    
        net.alchim31.maven
        yuicompressor-maven-plugin
        1.5.1
        
          
            
              compress
            
          
           
        
            utf-8
            true
            
            .min   
            
            
            **/css/community.ui/community.ui.core.css
            **/css/community.ui/community.ui.glogals.css
            **/css/community.ui/community.ui.style.css
            
            ${basedir}/WebContent
            ${basedir}/WebContent 
                       
    



이제 Maven 빌드시 goals 을 package 또는 yuicompressor:compress 지정하여 파일 압축을 처리할 수 있다.

결론

일반적으로는 execludes 속성을 사용하여 기술하지만 대부분의 오픈소스들은 이미 소스 압축된 파일들을 제공하고 있고 직접 만들어 사용하는 css 또는 javascript 들만 압축을 하면 되기 때문에 위와 같이 include 옵션을 사용했다.



2016년 12월 6일

자바스크립트 CryptoJS 와 JAVA 간의 AES 암호화

CryptoJS 를 이용한 암호화


CryptoJS 를 사용하면 웹에서도 손쉽게 AES 암호화가 가능하다.

자바스크립트에서 AES 암호화 예 1

1
2
3
4
5
6
7
8
9
10
11
12
<script    src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js"></script>
<script    src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/sha256.js"></script>
<script>
    
    var message = "Led Zeppelin- Stairway to Heaven"
    var passphrase = "1234";
    var encrypt = CryptoJS.AES.encrypt(message, passphrase);
    var decrypted = CryptoJS.AES.decrypt(encrypt, passphrase );
 
    // 암호화 이전의 문자열은 toString 함수를 사용하여 추출할 수 있다.
    var text = decrypted.toString(CryptoJS.enc.Utf8);
</script>
cs

텍스트 형식의 비밀번호를 사용하고 싶지 않는 경우는 SHA-256 같은 단방향 해쉬 알고리즘을 사용 (암호화된 메시지) 다이제스트를 생성하여 사용할 수 도 있다. 대부분의 웹 프로그램들은 보통 텍스트 형식의 비밀번호를 추론하기 어렵게 단방향으로 암호화하여 비밀번호를 보호하고 있다. (원본 메시지를 알면 암호화된 메시지를 구하기는 쉽지만 암호화된 메시지로는 원본 메시지를 구할 수 없어야 하며 이를 '단방향성'이라고 한다.)

자바스크립트에서 AES 암호화 예 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script    src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js"></script>
<script    src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/sha256.js"></script>
<script>
    
    var message = "Led Zeppelin- Stairway to Heaven"
 
    // 생성된 해쉬 값을 사용하면 평문 사용을 피할 수 있다.
    var hashedPassword = CryptoJS.SHA256("1234").toString() ; 
 
    var encrypt = CryptoJS.AES.encrypt(message, hashedPassword);
    var decrypted = CryptoJS.AES.decrypt(encrypt, hashedPassword );
 
    // 암호화 이전의 문자열은 toString 함수를 사용하여 추출할 수 있다.
    var text = decrypted.toString(CryptoJS.enc.Utf8);
</script>
cs


자바에서 원본 메시지를 구하기 위해서는 CryptoJS 내부적으로 키생성을 위하여 내부적으로 사용하는 비표준 OpenSSL KDF 를 사용함을 알고 있어야 한다. (사실 이미 관련 자바 코드가 공개되어 있어 몰라도 상관없다.)

주의할 것은 자바에서는 미국 이외의 국가에는 암호화 관련 제약(AES에서 128bit(16byte)를 초과하는 길이의 key 사용불가)이 있으며 별도의 확장 패키지(JCE Unlimited Strength Jurisdiction Policy)를 설치를 통하여 해지가 가능하다. 참고로 SHA-256를 사용하지 않고 PBKDF2 키를 사용하면 확장 패키지 설치 없이 암호화 및 복호화가 가능하다.

Java SE Downloads



자바에서 복호화


다음은 자바에서 암호화된 메시지를 복호화하는 과정이다.

복호화 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
 
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
 
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
 
public class CryptoTest {
    
    public static void main(String[] args) throws UnsupportedEncodingException, GeneralSecurityException, DecoderException {        
        String ciphertext = "U2FsdGVkX18+s94R2PY8hZ+HLrPiucqI33KNQHmkAvES41hGlh1HLgfTwGbwc9dkSid4RW5xj5yBn9hj0y7y0A==";
        String password = "1234";
        System.out.println ( decrypt(ciphertext, password) );
       }
    
    public static String decrypt(String ciphertext, String passphrase) {
        try {
            final int keySize = 256;
            final int ivSize = 128;
 
            // 텍스트를 BASE64 형식으로 디코드 한다.
            byte[] ctBytes = Base64.decodeBase64(ciphertext.getBytes("UTF-8"));
 
            // 솔트를 구한다. (생략된 8비트는 Salted__ 시작되는 문자열이다.) 
            byte[] saltBytes = Arrays.copyOfRange(ctBytes, 816);
            System.out.println( Hex.encodeHexString(saltBytes) );
            
            // 암호화된 테스트를 구한다.( 솔트값 이후가 암호화된 텍스트 값이다.)
            byte[] ciphertextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.length);
                       
            // 비밀번호와 솔트에서 키와 IV값을 가져온다.
            byte[] key = new byte[keySize / 8];
            byte[] iv = new byte[ivSize / 8];
            EvpKDF(passphrase.getBytes("UTF-8"), keySize, ivSize, saltBytes, key, iv);
            
            // 복호화 
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
            byte[] recoveredPlaintextBytes = cipher.doFinal(ciphertextBytes);
 
            return new String(recoveredPlaintextBytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
 
        return null;
    }
    
    private static byte[] EvpKDF(byte[] password, int keySize, int ivSize, byte[] salt, byte[] resultKey, byte[] resultIv) throws NoSuchAlgorithmException {
        return EvpKDF(password, keySize, ivSize, salt, 1"MD5", resultKey, resultIv);
    }
 
    private static byte[] EvpKDF(byte[] password, int keySize, int ivSize, byte[] salt, int iterations, String hashAlgorithm, byte[] resultKey, byte[] resultIv) throws NoSuchAlgorithmException {
        keySize = keySize / 32;
        ivSize = ivSize / 32;
        int targetKeySize = keySize + ivSize;
        byte[] derivedBytes = new byte[targetKeySize * 4];
        int numberOfDerivedWords = 0;
        byte[] block = null;
        MessageDigest hasher = MessageDigest.getInstance(hashAlgorithm);
        while (numberOfDerivedWords < targetKeySize) {
            if (block != null) {
                hasher.update(block);
            }
            hasher.update(password);            
            // Salting 
            block = hasher.digest(salt);
            hasher.reset();
            // Iterations : 키 스트레칭(key stretching)  
            for (int i = 1; i < iterations; i++) {
                block = hasher.digest(block);
                hasher.reset();
            }
            System.arraycopy(block, 0, derivedBytes, numberOfDerivedWords * 4, Math.min(block.length, (targetKeySize - numberOfDerivedWords) * 4));
            numberOfDerivedWords += block.length / 4;
        }
        System.arraycopy(derivedBytes, 0, resultKey, 0, keySize * 4);
        System.arraycopy(derivedBytes, keySize * 4, resultIv, 0, ivSize * 4);
        return derivedBytes; // key + iv
    }    
}
cs


PBKDF2 방식의 AES 암호화와 복호화


참고로 PBKDF2는 NIST(National Institute of Standards and Technology, 미국표준기술연구소)에 의해서 승인된 알고리즘이고, 미국 정부 시스템에서도 사용자 패스워드의 암호화된 다이제스트를 생성할 때 사용된다고 한다.

PBKDF2 키를 이용한 자바스크립트에서 AES 암호화 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script    src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js"></script>
<script    src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/pbkdf2.js"></script>
<script>
 
    var iv = CryptoJS.lib.WordArray.random(128/8).toString(CryptoJS.enc.Hex);
    var salt = CryptoJS.lib.WordArray.random(128/8).toString(CryptoJS.enc.Hex);
    var plainText = "Led Zeppelin- Stairway to Heaven";
    var keySize = 128;
    var iterationCount = 10000;
    var passPhrase = "1234";
    
    // PBKDF2 키 생성 
    var key128Bits100Iterations = 
            CryptoJS.PBKDF2(passPhrase, CryptoJS.enc.Hex.parse(salt),
            { keySize: keySize/32, iterations: iterationCount });
        
    var encrypted = CryptoJS.AES.encrypt(
            plainText,
            key128Bits100Iterations,
            { iv: CryptoJS.enc.Hex.parse(iv) });
 
    
});    
</script>
cs

128 비트 키를 사용하기 때문에 별도의 확장 패키지(JCE Unlimited Strength Jurisdiction Policy)를 설치필요하지 않는다.

PBKDF2 키를 이용한 자바에서 AES 복호화 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.security.spec.KeySpec;
 
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
 
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
 
public class Crypto2Test {
 
    public static void main(String args[]) throws Exception {
        String ciphertext = "rV2MW3IoCfcCHMa6cZuCcDJpOnOhHDS4R8cOXEL+Z4kqYiBqYbG+zVJNfNUZrKDI"
        String passPhrase = "1234";
        String salt = "18b00b2fc5f0e0ee40447bba4dabc952"
        String iv = "4378110db6392f93e95d5159dabdee9b";
        String decrypted = decrypt(salt, iv, passPhrase, ciphertext, 10000128);
        System.out.println(decrypted);
    }
    
    public static String decrypt(String salt, String iv, String passphrase, String ciphertext, int iterationCount, int keySize) throws Exception {        
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        KeySpec spec = new PBEKeySpec(passphrase.toCharArray(), Hex.decodeHex(salt.toCharArray()), iterationCount, keySize);
        SecretKey key = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");        
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(Hex.decodeHex(iv.toCharArray())));        
        byte[] decrypted = cipher.doFinal(Base64.decodeBase64(ciphertext));        
        return new String(decrypted, "UTF-8");
    }
}
cs


참고

스크립트와 자바 간의 AES 암호화는 비밀 키를 서로 알고있어야 한다는 키 관리 이슈를 가지고 있다. 
키관리를 위하여 HTTPS 와 유사하게 RSA 알고리즘을 적용하면 더욱 안전한 암호화를 구현할 수 있다.