2023年5月28日 星期日

建立 Java Dynamic Web Application JavaEE 的 Maven 專案

在 Eclipse 新增 Java Dynamic Web Application,
對 Project 按右鍵,選 
Configure --> Convert to Maven Project
(會有 webContent 資料夾)


直接用 Maven 指令產生專案 
(我要加雙引號包住參數,不然會有
The goal you specified requires a project to execute but there is no POM in this directory
的錯誤訊息):
mvn archetype:generate "-DarchetypeGroupId=org.apache.maven.archetypes" "-DarchetypeArtifactId=maven-archetype-webapp" "-DarchetypeVersion=1.4" "-DgroupId=你專案要取的groupId" "-DartifactId=你專案要取的artifactId" "-Dversion=0.1" "-DinteractiveMode=false"


在 Eclipse 新增 Maven project,
Archetype 選 maven-archetype-webapp
(不會有 webContent 資料夾)

要 build 成 war 檔時,下載 maven 後,
到專案目錄下 (有 pom.xml 的位置),
執行命令列模式 (command line) 的
mvn clean install。 
如果 consle印出 "Build Success",
應可在專案裡的 "target" 資料夾找到 build 出來的程式,
應有完整已 compile 的 web 專案 (包括 lib),
即簡單佈署用的 war 檔。

如果想在 Eclipse 裡面 build ,可以使用 plugin,
在 pom.xml 加上

<plugin>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.4</version>
 </plugin>

要 build 時,在 Eclipse 對專案按右鍵,選
Run Configuratoins --> 選 Maven,
Goals 欄位填 "clean install" ,再按 Run 。


參考:

2023年5月18日 星期四

使用 OpenSSL 製做 Client Certificate 做客戶端 TLS 認證 - 配合 Nginx 和 Java 做示範

在這裡我要來記錄一下使用 OpenSSL 製做 Client Certificate 做 客戶端 TLS 認證
(可參考 Mutual TLS (mTLS 或雙向認證) ) 認證的一些步驟事項。

首先第一件事是要安裝 OpenSSL 這個 tool,
使用以下指令安裝:
安裝 OpenSSL:

apt install openssl

在這裡我們為了方便演示及測試,
我們把自己當成憑證頒發機構 (Certificate authority,或稱 CA),
首先先產生 CA 的 private key,
取名為 my-ca-key.pem (或 my-ca.key,檔名可自取,副檔名一般會用 .pem 或 .key 來代表是一個 private key):

openssl genrsa -out my-ca-key.pem 4096

接下來產生自用的 Certificate Signing Request (CSR),在這邊命名為 my-ca.csr,
執行指令後被要求填入一些資訊,
要注意的是 Common Name 這個欄位不可以和等下 CA Certificate 要簽名的 Client CSR 一樣,
就是在 Certificate 的簽署鍵中各 Certificate 的 Common Name 不能一樣:

openssl req -new -key my-ca-key.pem -out my-ca.csr

再來我們執行以下指令用 CA 的 private key 對 CA CSR 來進行簽名,產生 CA 用的憑證 CA Certificate,檔名為 my-ca.crt,期限設為 365 天:

openssl x509 -req -days 365 -in my-ca.csr -signkey my-ca-key.pem -CAcreateserial -out my-ca.crt

這樣 CA 的東西都準備好了,我們現在應該會有三個檔案:

  1. my-ca-key.pem:CA 的 private key
  2. my-ca.csr:CA 的 CSR
  3. my-ca.crt:CA 的 certificate 憑證

CA 都準備好了以後,
接下來就是要來產生 Client 端的 Client private key、Client CSR、Client certificate,
基本上做法跟 CA 差不多,
差在是在要做 Client Certificate 時,Client CSR 要用 CA private key 和 CA Certificate 來簽名產生,不用給 Client private key。

首先是產生 Client private key,命名為 client-key.pem:

openssl genrsa -out client-key.pem 4096

再來是產生 Client CSR,命名為 client.csr,記得要填資訊欄位時,Common Name 不可以和 CA CSR 的一樣:

openssl req -new -key client-key.pem -out client.csr

最後我們使用 CA private key 和 CA Certificate 對  Client CSR 進行簽名產生 Client Certificate,
命名為 client.crt,期限設定 365 天:

openssl x509 -req -days 365 -in client.csr -CA my-ca.crt -CAkey my-ca-key.pem -CAcreateserial -out client.crt

在這邊我們可以用以下指令來檢查 Client Certificate 是不是由 CA Certificate 簽名的:

openssl verify -CAfile my-ca.crt client.crt

如果顯示 OK 的話,就代表正確。

基本上這樣 CA 和 Client 端的東西都準備好了,
不過有一些環境可能還會需要特別的 KeyStore 檔,KeyStore 裡會存放 private key 和 certificate 等資訊,例如 Java 就可能會需要 PKCS12 格式的 .p12 KeyStore 檔,
因為等下會用 Java 做 Client Certificate TLS 示範,
所以我們在這用以下指令將 Client private key 和 Client Certificate 打包成一個 .p12 的 KeyStore 檔案,這裡命名為 client-keystore.p12,
注意這裡會要求為 KeyStore 設定一個密碼,
等下 Java 程式會用到:

openssl pkcs12 -export -inkey client-key.pem -in client.crt -out client-keystore.p12

到這裡 Client 端的部份我們應該會有以下四個檔案:

  1. client-key.pem:Client private key
  2. client.csr:Client CSR
  3. client.crt:由 CA Certificate 和 CA private 簽名的 Client Certificate
  4. client-keystore.p12:裝了 Client private key 和 Client Certificate 的 KeyStore

先來示範在 Server 的 Nginx 設定要求 Client Server 要提供 Client Certifcate,
在 Nginx 的設定檔中,server 區塊的設定可以加上
ssl_client_certificate 和
ssl_verify_client
的 Client Certificate 設定:

server {
   ......
   ssl_client_certificate "CA 憑證路徑,例如上述產生的 my-ca.crt";
   ssl_verify_client on; #強制開啟 Client Certificate 要求
   ......
}

或是 ssl_verify_client 也可以不要設定 on,改設定
optional,
也就是不強制要求 Client Certificate,
但還是會檢查 Client 端有無帶上 Client Certificate,
 ,然後用 $ssl_client_verify 的值是不是 SUCCESS 來判斷有沒有認證成功,
範例如下:

server {
   ......
   ssl_client_certificate "CA 憑證路徑,例如上述產生的 my-ca.crt";
   ssl_verify_client optional; #不強制要求 Client Certificate
   ......
   #只有 /xx/xx/xx1 的 url 請求才檢查 Client Certificate
   location /xx/xx/xx1 { 
      ......
      if ($ssl_client_verify != SUCCESS) { #用 $ssl_client_verify 判斷 Client 端有無帶上 Client Certificate
         return 401;
      }
      ......
   }

   # /xx/xx/xx2 的 url 請求不檢查 Client Certificate
   location /xx/xx/xx2 {
      ......
   }
}

在 Client 端的部份,我們使用 Java 帶上 Client Certificate 來對 Server 端發出 Https 請求,
如果請求成功,應該會收到 Server 端的 200 HTTP response status:

Maven Dependency 配置:

 <!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
 <dependency>
     <groupId>org.apache.httpcomponents</groupId>
     <artifactId>httpclient</artifactId>
     <version>4.5.13</version>
 </dependency>

Java 程式碼:

package test;

import java.io.FileInputStream;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import javax.net.ssl.SSLContext;

import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;

public class ClientCertificateSSLTest {

	public static void main(String[] args) throws ClientProtocolException, IOException, NoSuchAlgorithmException, CertificateException, KeyManagementException, UnrecoverableKeyException, KeyStoreException {
		String keyStorePassphrase = "12345"; // Client KeyStore 的密碼
		String keyStorePath = "D:\\xxx\\client-keystore.p12"; // Client KeyStore 的路徑

		KeyStore keyStore = KeyStore.getInstance("PKCS12");
		keyStore.load(new FileInputStream(keyStorePath), keyStorePassphrase.toCharArray());
		
		SSLContext sslContext = SSLContexts.custom()
		        .loadKeyMaterial(keyStore, keyStorePassphrase.toCharArray())
                //如果 keystore 中存有多個憑證,可自行用 Alias 選擇特定憑證
//		        .loadKeyMaterial(keyStore, keyPassphrase.toCharArray(), (aliases, socket) -> {return "chosenAlias";})
		        .build();

//		CloseableHttpClient httpClient = HttpClients.createDefault(); // 如果不帶上 Client Certificate,Server 端就會拒絕連線
		CloseableHttpClient httpClient = HttpClients.custom().setSSLContext(sslContext).build();
		
//		HttpPost httpPost = new HttpPost("https://xxx.xxx.xxx/xx/xx/xx2"); // 這條 url 可以不用帶上 Client Certificate
		HttpPost httpPost = new HttpPost("https://xxx.xxx.xxx/xx/xx/xx1"); // 這條 url 需要帶上 Client Certificate
		HttpResponse response = httpClient.execute(httpPost);
		System.out.println(response.getStatusLine()); // 成功連線應該要回 200 HTTP response status
	}

}


參考資料:

  1. OpenSSL create client certificate & server certificate with example

2023年5月8日 星期一

設計長寬等比例隨視窗大小縮放的 DOM 元素(使用 aspect-ratio 或 padding)

當要設計一個長寬等比例隨視窗大小縮放的 DOM 元素時,
可以使用最新的 CSS 屬性:

aspect-ratio

來達成,
例如現在想要放入一個 Youtube 的 iframe,

<iframe src="https://www.youtube.com/embed/Cu8NnGwYZp0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen=""></iframe>

寬度要是外層元素的 80%,寬高比要是 16:9,
可以這樣設定:

HTML:

<iframe class="aspect-ratio"  src="https://www.youtube.com/embed/Cu8NnGwYZp0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen=""></iframe>

CSS:

.aspect-ratio {
  aspect-ratio: 16 / 9;
  width: 80%;
}


不過像在以前還沒有 aspect-ratio 的時代,
有一個蠻常用的、利用了 padding 的特性來實現的方法,
特別在這邊記錄一下。

首先先上 HTML 和 CSS:

HTML:

<div class="wrapper">
  <div class="box">
      <iframe class="box-in" src="https://www.youtube.com/embed/Cu8NnGwYZp0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen=""></iframe>
  </div>
</div>

CSS:

.wrapper {
  width: 80%;
}

.box {
  width: 100%;
  padding-top: 56.25%;
  height: 0;
  position: relative;
}

.box-in {
  position: absolute;
  width: 100%;
  height: 100%;
  left: 0;
  top: 0;
}

接著把上述兩種方法放上在 JsFiddle 的成品來做為比較:

說明:
使用 padding 可以達成等比例 DOM 的原理是,
當元素的 height 是 0 ,並且沒有設定 border 和 margin 等時,
此時示素的高度就由它的 padding-top 或 padding-bottom 決定,
而 padding-top 此時會由它的父元素之寬來做為參考計算,
在上述的例子中,父示素就是 class="wrapper",width 是 80%,在這裡也就是 <body> 寬的 80%,
而 class="box" 設定了 padding-top=56.25%,也就是 9/ 16,
這樣它的高度就會是父元素的寬乘上 9 / 16,然後它的寬會是父元素的寬。

在 class="box" 裡我們還要再設定一個 class="box-in" 元素,
因為 class="box" 有設定 padding-top 的關係,所以位置會被往下推,
要解決這個,我們可以先將 class="box" 設為 position=relative 做為子元素位置 left, top 的參考點,
然後在 class="box-in" 上設定
position=absolute
left = 0
top = 0
來將 class="box-in" 往上放至正確的位置,
接著在 class="box-in" 中就可以放上想要放的 Youtube iframe 了。