Apps Script用Sheet生成動態網頁(10): 處理使用者登入token及登出功能

在前一篇Apps Script用Sheet生成動態網頁(9): 會員登入頁面,中最後將token以?Query參數的形式給予首頁。 所以在首頁部份就要處理登入token,並且還要能讓使用者進行登出。

1. Apps Script的部份(後端/伺服端)

authLogin()提供前端網頁可以驗證登入的token是否可用,並回傳登入使用者的基本資訊。內部的流程如下6點:

  1. 檢查token基本格式, JWT是由3個部份(1)header, (2)payload, (3)signature所組成,之間用.符號做分隔。
  2. 針對payload部份解碼,確認token的內容可以正常解碼。
  3. 確認payload發行token的host是當前伺服器。
  4. 確認token發行token是否已經逾時。
  5. 檢查簽章部份驗證token是否為正常。
  6. 回傳登入使用者的基本資訊。

function authLogin(token) {
if (!token || typeof token != 'string')
throw new Error('token arg');
let [header, payload, signature] = token.split('.');
if (!header || !payload || !signature)
throw new Error('invalid jwt');
let json = {};
try {
let decoded = Utilities.base64DecodeWebSafe(payload);
json = JSON.parse(Utilities.newBlob(decoded).getDataAsString());
} catch (error) {
Logger.log(error);
throw new Error('payload');
}
if (json.host !== ScriptApp.getService().getUrl())
throw new Error('invalid host:' + json.host);
let now_time = Date.now() / 1000;
if (now_time >= json.exp)
throw new Error(`expired:${json.exp}, now:${now_time}`);
const base64Encode = (text, json = true) => {
const data = json ? JSON.stringify(text) : text;
return Utilities.base64EncodeWebSafe(data).replace(/=+$/, '');
};
const computedSignature = base64Encode(
Utilities.computeHmacSha256Signature(
`${header}.${payload}`,
ScriptApp.getScriptId(), // 請改成你喜歡的
),
false);
if (signature !== computedSignature)
throw new Error(`invalid signature: ${signature}, ${computedSignature}`);
return json.user;
}


2. HTML部份(前端/使用者端)

首頁當初使用Apps Script的template功能直接寫在code裡頭,不過到這個階段我們要將它獨立出來,並且對於Apps Script內的連結也都使用這篇的方法做了調整。

修改doGet()的內容,設定新的首頁檔案template檔案名稱為index.html

const TemplateFiles = {
default: 'index.html'
}

function doGet(req) {
...
else {
output = HtmlService.createTemplateFromFile(TemplateFiles.default).evaluate();
}
return output;
}


從程式碼中提取出來的index.html如下

<!DOCTYPE html>
<html>
<head><base target="_top" href="<?= getServerUrl() ?>"></head>
<body>
<h1>用Sheet內容生成網頁</h1>
<? let name_list = getNameListFromSheet(); ?>
<? let is_manager = validateUploadPermission(true); ?>
<ul>
<?
for (let i = 0 ; i < name_list.length ; ++i ) {
if (null == name_list[i][0]) continue;
let name = name_list[i][0];
let url = '?show=html&name=' + name;
?>
<li>
<a href="<?= url ?>">前往<?= name ?></a>
<? if (is_manager) { ?><button onclick="onDeleteClicked(event)" name="<?= name ?>">刪除</button><? } ?>
</li>
<?
}
?>
<li><a href="?show=filesInDrive">上傳到Drive資料夾的檔案</a></li>
</ul>
<script src="?getJavascript=app"></script>
</body>
</html>


app.js.html 部份新增dropToken()是讓使用者可以登出。而加上的DOMContentLoaded事件處理,則是用來接收Query中的token,並呼叫authLogin()確認token的正確性。

function dropToken(event) {
if (event) event.preventDefault()
google.script.url.getLocation(function (location) {
var params = {}
for (var key in location.parameters) {
if (key !== 'token') params[key] = location.parameters[key]
}
google.script.history.replace(null, params, location.hash)
window.top.location = document.baseURI
})
}
document.addEventListener('DOMContentLoaded', function () {
try {
google.script.url.getLocation(function (location) {
google.script.run.withSuccessHandler(hasLogined)
.withFailureHandler(notLogin)
.authLogin(location.parameter.token);
})
} catch (error) {
notLogin(error);
}
function hasLogined(login_user) {
console.log(login_user);
appendChildToBody(`${login_user.name}歡迎您 <button onclick="dropToken(event)">登出</button>`);
}
function notLogin(error) {
console.error(error.message, error);
appendChildToBody(`您尚未登入,<a href="?show=html&name=%e7%99%bb%e5%85%a5%e6%9c%83%e5%93%a1">點此</a>登入`);
}
function appendChildToBody(html) {
var PANEL_ID = "login_pannel";
var comp = document.getElementById(PANEL_ID) || document.createElement('div');
comp.id = PANEL_ID;
comp.innerHTML = html;
var body = document.getElementsByTagName('body')[0];
body.appendChild(comp);
}
});

這裡使用google.script.url.getLocation來取的Query的參數,依照官方文件的說明query會在parameter或是parameters。

google.script.url.getLocation(function (location) {
    google.script.run.withSuccessHandler(hasLogined)
        .withFailureHandler(notLogin)
        .authLogin(location.parameter.token);
   })
}


3. 展示

4. 後語

登入和登出功能是完成了,但是你如果以登入會員,再點擊登入會員,你將會發現怎麼還能登入呢?

是的,你突破他了,因為http並沒有紀錄下登入狀態,一般網站的登入狀態是由server端發送cookie,client端收到cookie後會在使用者點擊連結時,將cookie送出,讓server端維持登入狀態。但是,AppScript這裡沒辦法從server提供cookie。


留言