iT邦幫忙

2021 iThome 鐵人賽

DAY 29
0
自我挑戰組

從無到有打造驗證碼共享的 Line 機器人系列 第 29

用 Line LIFF APP 實現信箱驗證綁定功能(5) - 前後端認證功能

前幾天完成了關於發送認證信的各種細節,但認證碼發出去後,使用者還是需要回到系統認證才能綁定。先前有提過讓使用者手動輸入認證實在不友善,今天就來著手改善這步驟吧~

如何自動化執行身份認證

常見的認證方法有以下兩種:

  1. 表單輸入認證碼
  2. 將認證碼填入認證連結的 query string,並將連結發送給使用者

以驗證碼小幫手來說,第二種方法可以讓使用者快速認證,但同時實作第一種方法則可以增加更多彈性

Google App Script 認證 API 開發

建立一個新的 Google App Script 專案 User Validation,並且引入 ReadMail & ReplyMessage 專案做為資料庫使用,新增 app.gs 內容如下:

function doPost(e) {
  var message = 'fail';
  const input = JSON.parse(e.postData.contents);
  if (input.code && (input.token || input.email)) {
    const userId = input.token || getUserIdByEmail(input.email);
    if ((userId.length !== 0) && ReplyMessage.tagVerificationCode(input.code, userId)) {
      message = 'success';
      ReplyMessage.changeRichMenuAfterBindSuccess(userId);
    }
  }
  return ContentService.createTextOutput(JSON.stringify({message})).setMimeType(ContentService.MimeType.JSON);
}

function getUserIdByEmail(email) {
  const sheet = ReadMail.connectToSheet('users');
  const searchResult = ReadMail.searchColumnValue(sheet, 'email', email);
  if (searchResult !== -1) {
    const targetRowIndex = searchResult+2;
    const targetRowRange = sheet.getRange(targetRowIndex, 1, 1, 1);
    return targetRowRange.getValue();
  }
  return "";
}

因為大部分都是之前就寫好的功能重複使用,所以很快就能完成~接著別忘了儲存並部署為網頁應用程式。

前端串接 API

用 vue router 新增一個 validate page

流程構思

  1. (liff.isLoggedIn() && liff.isInClient()) 則檢查是否帶有認證碼 query string
    • 有:call User Validation 進行認證
    • 沒有:顯示認證碼輸入框,送出後 call User Validation 進行認證
  2. 若不符合 1,則顯示認證碼&信箱輸入框,送出後 call User Validation 進行認證

新增 validateCodePost in api.js

export const validateCodePost = (code, token, email) => {
    const targetUrl = "YOUR_USER_VALIDATION_URL";
    let data = JSON.stringify({code, token, email});
    return axios.post(targetUrl, data, {
        headers: { 'content-type': 'application/x-www-form-urlencoded' }
    }).then(response => {
        if (response) {
            return response;
        } else {
            return Promise.reject();
        }
    }).catch(error => {
        console.log('error', error);
    });
};

新增 ValidateForm.vue

<script setup>
import {onMounted, ref} from 'vue'
import {validateCodePost} from '/src/service/api'
import * as yup from 'yup';
import { useRoute } from 'vue-router'

const route = useRoute()

const validationCode = ref("");
const userToken = ref("");
const inputEmail = ref("");
const result = ref("");
const showCode = ref(false);
const showMail = ref(false);
const onSubmit = ref(false);

const closeLiff = () => {
  liff.closeWindow();
}

const mailSchema = yup.string().email().required();
const submit = async () => {
  result.value = "";
  onSubmit.value = true;

  if (validationCode.value.length === 0) {
    onSubmit.value = false;
    alert('請輸入認證碼');
    return;
  }

  let res = null;
  if (userToken.value.length === 0) {
    const isMailValid = await mailSchema.isValid(inputEmail.value);
    if (isMailValid) {
      res = await validateCodePost(validationCode.value, null, inputEmail.value)
    } else {
      alert('請輸入有效的信箱地址');
    }
  } else {
    res = await validateCodePost(validationCode.value, userToken.value, null)
  }
  result.value = (res && res.data.message) || '';
  onSubmit.value = false;
}

onMounted(() => {
  validationCode.value = route.query.code || '';
  showCode.value = validationCode.value.length <= 0;

  liff.init({
    liffId: 'YOUR_LIFF_ID'
  }).then(() => {
    if (liff.isLoggedIn() && liff.isInClient()) {
      const user = liff.getDecodedIDToken();
      userToken.value = user && user.sub;
      if (!showCode.value && userToken.value.length > 0) {
        submit();
      } else {
        showMail.value = userToken.value.length <= 0
      }
    } else {
      showMail.value = true;
    }
  }).catch((err) => {
    console.log(err);
    showCode.value = true;
    showMail.value = true;
  });
});
</script>

<template>
  <p v-if="showCode" v-show="!onSubmit">認證碼:<input type="email" v-model="validationCode" placeholder="請輸入認證碼"></p>
  <p v-if="showMail" v-show="!onSubmit">收到認證碼的信箱:<input type="email" v-model="inputEmail" placeholder="請輸入收到認證碼的信箱"></p>
  <button v-if="showCode || showMail" v-show="!onSubmit" type="button" class="btn" @click="submit()" :disabled="onSubmit">送出</button>

  <p v-show="onSubmit">認證中,請稍候...</p>

  <template v-if="result.length > 0">
    <p>{{(result === 'success') ? '認證成功' : '認證失敗'}}</p>
    <button type="button" class="btn" @click="closeLiff">關閉</button>
  </template>
</template>

部署到 Github Pages

npm run build

然後將靜態檔案部署到 Github Pages
因為需要多一個 /validate 的 uri,比較笨但快的做法是在 dist 資料夾底下新建一個 validate 資料夾,並把 dist 資料夾底下的 index.html 拷貝一份到 validate 資料夾底下

認證信加上 Line LIFF 認證連結

修改 Send Mail 專案

將發給使用者認證信的內容修改如下:

var content = `${input.name} 您好,您的身份認證碼為: ${verificationCode},時效為10分鐘。也可點擊此連結進行認證:{YOUR_LIFF_APP_URL}/validate?code=${verificationCode}`;

這樣一來使用者用手機點擊連結就可以直接開啟 Line LIFF APP 綁定認證成功~就算用其他的瀏覽器開啟,我們也能讓使用者手動輸入認證碼/信箱進行認證。

明天就是最後一天了~雖然還有很多能改善的東西,但總歸是告一段落了。


上一篇
防止使用者頻繁送出 Request & 倒數計時重新發送認證碼
下一篇
驗證碼小幫手完整測試流程 & 完賽心得
系列文
從無到有打造驗證碼共享的 Line 機器人30

尚未有邦友留言

立即登入留言