Using mod_harbour.v2.1 to implement a RESTful API call
- Since mod_harbour.v2.1 does not support URLs like https://xxx.xxx.xxx/api.prg/user/1, you need to add this functionality manually.
- Modify mod_harbour.v2.1/source/ap_func_c.c and add the AP_GETURI() function:
b. Because JWT is required, download the file from: https://github.com/matteobaccan/HarbourJwt — you only need the jwt.prg file.
//----------------------------------------------------------------// // mh_GetUri() cannot retrieve URLs like: http?://xxx.xxx/api.prg/user/1 // So we add this function to get the full URI HB_FUNC(AP_GETURI) { hb_retc(GetRequestRec()->uri); }
c. Modify ./mod_harbour.v2.1/libmhapache.hbp and add: \your_path\jwt.prg
d. Modify ./mod_harbour.v2.1/source/externs.hbx and add: extern AP_GETURI
e. Recompile to generate libmhapache.dll and mod_harbour.v2.so - Modify the Apache httpd.conf configuration:
You need to set two environment variables:SetEnv JWT_SECRET "your_sha256" SetEnv JWT_REFRESH_SECRET "your_sha256"
JWT_SECRET: Access Token, used to validate access requests
JWT_REFRESH_SECRET: Refresh Token, used to obtain a new Access Token when the old one expires
If your website manages multiple RESTful APIs at the same time, you can separate them using <Directory> blocks, for example:
<Directory "{DOCUMENTPATH}/api">
SetEnv JWT_SECRET "...."
SetEnv JWT_REFRESH_SECRET "...."
</Directory>
<Directory "{DOCUMENTPATH}/zzz">
SetEnv JWT_SECRET "...."
SetEnv JWT_REFRESH_SECRET "...."
</Directory>To generate the SHA256 values, use the following command and paste the result into JWT_SECRET and JWT_REFRESH_SECRET:
openssl rand -base64 32Next is the [Frontend] test webpage content:
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF--8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>mod_harbour RESTful API 測試</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.6; padding: 20px; max-width: 800px; margin: auto; }
h1, h2 { border-bottom: 2px solid #eee; padding-bottom: 10px; }
.container { display: flex; gap: 20px; }
.controls, .output { flex: 1; min-width: 0; /* 解決 flex 項目被內容撐開的問題 */ }
button { display: block; width: 100%; padding: 10px; margin-bottom: 10px; border: 1px solid #ccc; background-color: #f0f0f0; cursor: pointer; }
button:hover { background-color: #e0e0e0; }
input { width: calc(100% - 22px); padding: 10px; margin-bottom: 10px; }
pre { background-color: #2d2d2d; color: #f1f1f1; padding: 15px; border-radius: 5px; white-space: pre; overflow-x: auto; }
label { font-weight: bold; margin-bottom: 5px; display: block; }
</style>
</head>
<body>
<h1>mod_harbour RESTful API 測試</h1>
<div class="container">
<div class="controls">
<h2>控制面板</h2>
<button onclick="getAllUsers()">GET /users (取得所有使用者)</button>
<hr>
<label for="userId">使用者 ID:</label>
<input type="text" id="userId" placeholder="輸入 ID (例如: 1)" value="1">
<button onclick="getUserById()">GET /users/{id} (取得單一使用者)</button>
<button onclick="deleteUser()">DELETE /users/{id} (刪除使用者)</button>
<hr>
<label for="userName">姓名:</label>
<input type="text" id="userName" placeholder="輸入姓名">
<label for="userEmail">Email:</label>
<input type="text" id="userEmail" placeholder="輸入 Email">
<button onclick="createUser()">POST /users (新增使用者)</button>
<button onclick="updateUser()">PUT /users/{id} (更新使用者)</button>
<hr>
<h2>身分驗證 (JWT)</h2>
<label for="loginUser">使用者名稱:</label>
<input type="text" id="loginUser" value="admin">
<label for="loginPass">密碼:</label>
<input type="password" id="loginPass" value="password">
<button onclick="login()">POST /login (登入並取得 Token)</button>
<button onclick="getProfile()">GET /profile (測試保護路由)</button>
<button onclick="logout()">登出 (清除 Token)</button>
</div>
<div class="output">
<h2>輸出結果</h2>
<pre id="response">點擊按鈕以發送請求...</pre>
</div>
</div>
<script>
// API 的基礎 URL,指向 .prg 原始碼檔案
const API_BASE_URL = '/RESTful/api.prg';
// 頁面載入時,嘗試從 localStorage 讀取 token
let jwtToken = localStorage.getItem('jwtToken');
// 如果一開始就有 token,可以提示使用者
if (jwtToken) {
console.log('偵測到已儲存的 Token,目前為登入狀態。');
}
const responseElement = document.getElementById('response');
// 新增:嘗試刷新 Access Token 的函式
async function tryRefreshToken() {
try {
// 注意:這裡不需要手動帶入 headers,因為瀏覽器會自動帶上 HttpOnly cookie
const response = await fetch(API_BASE_URL + '/refresh_token', { method: 'POST' });
if (!response.ok) {
return false;
}
const data = await response.json();
if (data.token) {
jwtToken = data.token;
localStorage.setItem('jwtToken', jwtToken);
return true;
}
return false;
} catch (error) {
console.error('Error during token refresh:', error);
return false;
}
}
async function apiCall(method, path, body = null) {
const url = API_BASE_URL + path;
const options = {
method: method,
headers: {}
};
options.headers['Content-Type'] = 'application/json';
if (jwtToken) {
options.headers['Authorization'] = `Bearer ${jwtToken}`;
};
if (body) {
options.body = JSON.stringify(body);
}
try {
responseElement.textContent = `正在發送 ${method} 請求至 ${url} ...`;
let response = await fetch(url, options);
// 新增:如果收到 401 (Unauthorized),嘗試刷新 token 並重試
if (response.status === 401 && path !== '/login' && path !== '/refresh_token') {
console.log('Access Token 可能已過期,正在嘗試刷新...');
const refreshSuccess = await tryRefreshToken();
if (refreshSuccess) {
console.log('Token 刷新成功,正在重試原始請求...');
// 使用新的 token 更新 Authorization 標頭
options.headers['Authorization'] = `Bearer ${jwtToken}`;
// 重試請求
response = await fetch(url, options);
} else {
console.log('Token 刷新失敗,請重新登入。');
alert('您的連線階段已過期,請重新登入。');
await logout(); // 清理前端和後端的登入狀態
return;
}
}
let responseText = `狀態: ${response.status} ${response.statusText}\n\n`;
// 處理沒有內容的回應 (例如 204 No Content)
if (response.status === 204) {
responseText += "操作成功,無返回內容。";
} else {
// 如果是登入成功,儲存新的 access token
if (path === '/login' && response.ok) {
const data = await response.json();
localStorage.setItem('jwtToken', data.token); // 將 Token 儲存到 localStorage
jwtToken = data.token;
alert('登入成功!Access Token 已儲存。');
responseText += JSON.stringify(data, null, 2);
responseElement.textContent = responseText;
return; // 結束函式
}
const data = await response.json();
responseText += JSON.stringify(data, null, 2);
}
responseElement.textContent = responseText;
} catch (error) {
responseElement.textContent = `發生錯誤:\n${error}`;
}
}
function getAllUsers() {
apiCall('GET', '/users');
}
function getUserById() {
const id = document.getElementById('userId').value;
if (!id) {
alert('請輸入使用者 ID');
return;
}
apiCall('GET', `/users/${id}`);
}
function createUser() {
const name = document.getElementById('userName').value;
const email = document.getElementById('userEmail').value;
if (!name || !email) {
alert('請輸入姓名和 Email');
return;
}
const body = { name, email };
apiCall('POST', '/users', body);
}
function updateUser() {
const id = document.getElementById('userId').value;
const name = document.getElementById('userName').value;
const email = document.getElementById('userEmail').value;
if (!id) {
alert('請輸入要更新的使用者 ID');
return;
}
if (!name && !email) {
alert('請至少輸入姓名或 Email 來進行更新');
return;
}
const body = {};
if (name) body.name = name;
if (email) body.email = email;
apiCall('PUT', `/users/${id}`, body);
}
function deleteUser() {
const id = document.getElementById('userId').value;
if (!id) {
alert('請輸入要刪除的使用者 ID');
return;
}
if (confirm(`確定要刪除 ID 為 ${id} 的使用者嗎?`)) {
apiCall('DELETE', `/users/${id}`);
}
}
function login() {
const username = document.getElementById('loginUser').value;
const password = document.getElementById('loginPass').value;
if (!username || !password) {
alert('請輸入使用者名稱和密碼');
return;
}
const body = { username, password };
jwtToken = null; // 登入前先清除記憶體中的舊 token
localStorage.removeItem('jwtToken'); // 同時清除 localStorage 中的舊 token
apiCall('POST', '/login', body);
}
function getProfile() {
if (!jwtToken) {
alert('請先登入以取得 Token!');
return;
}
apiCall('GET', '/profile');
}
async function logout() {
// 先呼叫後端 API 來清除 HttpOnly cookie
await apiCall('POST', '/logout');
// 然後清理前端的 token
jwtToken = null;
localStorage.removeItem('jwtToken');
alert('已登出成功。');
}
</script>
</body>
</html>Next is the [Backend] main program:
<?prg
/*
* api.prg (範例程式)
* 這是一個使用 mod_harbour v2.1 開發的簡單 RESTful API 範例 (物件導向版本)。
*
* 這個版本將原來的程序化程式碼重構為一個名為 UserApi 的類別,
* 使得程式碼結構更清晰、更易於維護和擴展。
*
* 主要功能:
* - 管理 "users" 資源,提供 CRUD 操作。
* - 使用 JWT (JSON Web Token) 進行身分驗證。
* - 實現 Refresh Token 機制以延長使用者登入狀態。
* - 採用物件導向設計,將所有相關邏輯封裝在 UserApi 類別中。
*/
#include "hbclass.ch"
// --- 主程序 ---
PROCEDURE Main()
// 取得當前請求的 HTTP 方法 (GET, POST, PUT, DELETE)。
LOCAL cMethod := AP_Method()
// 使用我們在 C 原始碼中新增的 AP_GetUri() 函式
LOCAL hHeaders := AP_HeadersIn()
LOCAL cURI := AP_GetUri() // 請求的 URL 路徑,例如 /RESTful/api.prg/users/1
LOCAL cBody := AP_GetBody() // 預先讀取 body,避免在多個方法中重複讀取
LOCAL oApi
// 建立 API 物件並傳入請求資訊
oApi := UserApi():New( cMethod, cURI, hHeaders, cBody )
// 執行請求處理
oApi:HandleRequest()
RETURN
// --- API 類別定義 ---
CLASS UserApi
// --- 資料成員 (Properties) ---
// 模擬資料庫
DATA aUsers INIT {}
// 檔案快取時間戳
DATA tsLastModTime INIT NIL
// JWT 密鑰
DATA cJwtSecret INIT ""
DATA cJwtRefreshSecret INIT ""
// 請求相關資料
DATA cMethod INIT ""
DATA cURI INIT ""
DATA hHeaders INIT NIL
DATA cBody INIT ""
// --- 方法 (Methods) ---
METHOD New( cMethod, cURI, hHeaders, cBody )
METHOD HandleRequest()
// API 端點方法
METHOD GetAllUsers()
METHOD GetUser( nID )
METHOD CreateUser()
METHOD UpdateUser( nID )
METHOD DeleteUser( nID )
METHOD Login()
METHOD GetProfile()
METHOD RefreshToken()
METHOD Logout()
// 輔助方法
METHOD VerifyToken() PROTECTED
METHOD SendError( nStatusCode, cMessage ) PROTECTED
METHOD FindUser( nID ) PROTECTED
METHOD FindUserIndex( nID ) PROTECTED
METHOD GetNextID() PROTECTED
METHOD LoadData() PROTECTED
METHOD SaveData() PROTECTED
METHOD InitData() PROTECTED
METHOD GetCookieValue( cCookieHeader, cCookieName ) PROTECTED
ENDCLASS
// --- 類別實作 ---
....
....
?>Important!! Please reconfigure your own Access Token and Refresh Token lifecycle!!
You can view full code from:https://www4.zzz.com.tw/phpBB3/viewtopic.php?f=2&t=370
WeChat ID: ssbbstw