用 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 绑定认证成功~就算用其他的浏览器开启,我们也能让使用者手动输入认证码/信箱进行认证。

明天就是最後一天了~虽然还有很多能改善的东西,但总归是告一段落了。


<<:  如何衡量万事万物 (8) 观察少数

>>:  [Part 6 ] Vue.js 的精随-元件生命周期 (续)

Day 3 - Playing with CSS

前言 JS 30 是由加拿大的全端工程师 Wes Bos 免费提供的 JavaScript 简单应用...

Day 17 - Spring Boot 例外处理

经过上一篇Day 16 - Spring Boot 资料验证的功能实作後,我们的业务逻辑层需要处理的...

DAY7 MongoDB 资料更新(Update)

DAY7 MongoDB 资料更新(Update) 更新(update) 资料更新(Update)如...

Day 30 - 永丰银行付款外挂发布、铁人赛总结

今天是铁人赛最後一天,也完成了连续 30 天晚上没躺在椅子上睡着的挑战 ^^" 除了假日以...

【PHP Telegram Bot】Day26 - 入群欢迎机器人(2):设定欢迎讯息

如果欢迎讯息写死在程序里,临时想换还要把程序打开来改,改完还要测试,不如就直接让它能在群组里设定吧...