用Abp實現兩步驗證(Two-Factor Authentication,2FA)登入(二):Vue網頁端開發

2023-04-12 15:00:39

@


前端程式碼的框架採用vue.js + elementUI 這套較為簡單的方式實現,以及typescript語法更方便閱讀。

首先新增全域性物件:

loginForm: 登入表單物件
twoFactorData: 兩步驗證資料,
showTwoFactorSuccess: 是否顯示兩步驗證成功提示

loginForm: {
    //登入物件
    username: "",
    password: "",
    twoFactorAuthenticationToken: "",
    twoFactorAuthenticationProvider: "Phone",
},
twoFactorData: null,
showTwoFactorSuccess: false,

傳送驗證碼

編寫傳送驗證碼函數sendVerificationCode,傳送驗證碼後,啟動定時器,60秒後可以再次傳送驗證碼。

   async sendVerificationCode() {
      this.smsSendCd = 60;
      this.timer = setInterval(() => {
        this.smsSendCd--;
        if (this.smsSendCd <= 0) {
          clearInterval(this.timer);
        }
      }, 1000);
      await request(
        `${this.host}api/TokenAuth/SendTwoFactorAuthenticateCaptcha`,
        "post",
        {
          provider: "Phone",
          userId: this.twoFactorData.userId,
        }
      )
        .catch((re) => {
          var res = re.response.data;
          this.errorMessage(res.error.message);
        })
        .then((re) => {
          var res = re.data.result;
          this.showTwoFactorSuccess = true;
          this.showTwoFactorSuccess = false;
          this.successMessage("傳送驗證碼成功");
        });
    },

request 是利用axios庫傳送帶有存取憑證Header請求功能的封裝 ,ajaxRequest.ts請參考博文使用 Abp.Zero 搭建第三方登入模組(三):網頁端開發

這裡使用js-cookie庫獲取cookie中的存取憑證,並新增到Header中

import { request } from "@/ajaxRequire";
import Cookies from "js-cookie";

const tokenKey = "main_token";
const setToken = (token: string) => Cookies.set(tokenKey, token);
const cleanToken = () => Cookies.remove(tokenKey);
const getToken = () => Cookies.get(tokenKey);

登入

編寫登入函數handleLogin:

 async handleLogin() {
      this.loading = true;

      var userNameOrEmailAddress = this.loginForm.username;
      var password = this.loginForm.password;

      var twoFactorAuthenticationToken =
        this.loginForm.twoFactorAuthenticationToken;
      var twoFactorAuthenticationProvider =
        this.loginForm.twoFactorAuthenticationProvider;

      userNameOrEmailAddress = userNameOrEmailAddress.trim();
      await request(`${this.host}api/TokenAuth/Authenticate`, "post", {
        userNameOrEmailAddress,
        password,
        twoFactorAuthenticationToken,
        twoFactorAuthenticationProvider,
      })
        .catch((re) => {
          var res = re.response.data;
          this.errorMessage(res.error.message);
        })
        .then(async (res) => {
          var data = res.data.result;
          if (data.requiresTwoFactorAuthenticate) {
            this.twoFactorData = data;
          } else {
            setToken(data.accessToken);
            setRememberClientToken(data.rememberClientToken);
            await this.getCurrentUser();
          }
        })
        .finally(() => {
          setTimeout(() => {
            this.loading = false;
          }, 1.5 * 1000);
        });
    },

請注意,當需要進行兩步驗證時,requiresTwoFactorAuthenticate會返回true,同時返回
twoFactorAuthenticationProviders。

退出登入

登出, 將Token以及使用者資訊置空

<el-button
    :loading="loading"
    type="danger"
    style="width: 100%"
    @click.native.prevent="logout">
    退出登入
</el-button>
logout() {
    setToken(null);
    this.token = null;
    this.userInfo = null;
},

介面控制元件

在登入表單的HTML中,新增兩步驗證控制元件:
顯示規則為,當需要兩步驗證時(即twoFactorData不為空),顯示兩步驗證控制元件,否則顯示登入控制元件。

根據twoFactorAuthenticationProviders。我們採用了兩種方式,一種是簡訊驗證碼,一種是郵箱驗證碼,這裡我們採用了elementUI的tab元件,來實現兩種方式的切換。

     <el-form
        ref="loginForm"
        :model="loginForm"
        class="login-form"
        autocomplete="on"
        label-position="left"
      >
        <template v-if="twoFactorData == null">
            ...
        </template>
        <template v-else>
          <p>您的賬號開啟了兩步驗證,請選擇一種認證方式以繼續登入</p>
          <el-tabs
            v-model="loginForm.twoFactorAuthenticationProvider"
            tab-position="top"
          >
            <el-tab-pane
              :lazy="true"
              label="SMS簡訊驗證"
              name="Phone"
              :disabled="
                twoFactorData.twoFactorAuthenticationProviders.indexOf(
                  'Email'
                ) == -1
              "
            >
              <el-row>
                <el-col
                  :span="24"
                  style="
                     {
                      margin-bottom: 10px;
                    }
                  "
                >
                  <el-alert
                    v-if="showTwoFactorSuccess"
                    title="驗證碼已傳送至使用者的手機號,請查收"
                    type="info"
                  >
                  </el-alert>
                </el-col>
                <el-col :span="24">
                  <el-form-item
                    class="item"
                    prop="twoFactorAuthenticationToken"
                  >
                    <el-input
                      v-model="loginForm.twoFactorAuthenticationToken"
                      :placeholder="'傳送驗證碼後鍵入驗證碼'"
                      tabindex="2"
                      autocomplete="on"
                      @blur="capsTooltip = false"
                    >
                      <el-button
                        slot="append"
                        :disabled="smsSendCd > 0"
                        @click="sendVerificationCode"
                        >{{
                          smsSendCd == 0 ? "傳送驗證碼" : smsSendCd + "後重試"
                        }}</el-button
                      >
                    </el-input>
                  </el-form-item>
                </el-col>
              </el-row>
            </el-tab-pane>

            <el-tab-pane
              :lazy="true"
              label="郵箱驗證"
              name="Email"
              :disabled="
                twoFactorData.twoFactorAuthenticationProviders.indexOf(
                  'Email'
                ) == -1
              "
            >
              <el-row>
                <el-col :span="24">
                  <el-alert
                    v-if="showTwoFactorSuccess"
                    title="驗證碼已傳送至登入使用者對應的郵箱,請查收"
                    type="info"
                  >
                  </el-alert>
                </el-col>
                <el-col :span="24">
                ...
                </el-col>
              </el-row>
            </el-tab-pane>
          </el-tabs>
        </template>

        <el-row type="flex" class="row-bg" justify="center" :gutter="10">
          <el-col :span="10" v-if="twoFactorData != null">
            <el-button
              :loading="loading"
              style="width: 100%"
              @click.native.prevent="twoFactorData = null"
            >
              返回
            </el-button>
          </el-col>
          <el-col :span="10">
            <el-button
              :loading="loading"
              type="primary"
              style="width: 100%"
              @click.native.prevent="handleLogin"
            >
              {{ twoFactorData == null ? "登入" : "繼續" }}
            </el-button>
          </el-col>
        </el-row>
      </el-form>

獲取使用者資訊功能

登入成功後我們要拿到當前使用者的資訊,存入userInfo物件,並在頁面上簡單展示

<span>{{ userInfo }}</span>

建立一個獲取當前使用者的函數

async getCurrentUser() {
    await request(
    `${this.host}${this.prefix}/User/GetCurrentUser`,
    "get",
    null
    )
    .catch((re) => {
        var res = re.response.data;
        this.errorMessage(res.error.message);
    })
    .then(async (re) => {
        var result = re.data.result as any;
        this.userInfo = result;
        this.token = getToken();
        clearInterval(this.timer);

        this.smsSendCd = 0;
        this.currentVerifyingType = null;

        this.successMessage("登入成功");
    });
}

最終效果

專案地址

Github:matoapp-samples