使用 Abp.Zero 搭建第三方登錄模組(三):網頁端開發

簡短回顧一下網頁端的流程,總的來說網頁端的職責有三:

  1. 生成一個隨機字元作為鑒權會話的臨時Token,
  2. 生成一個小程式碼, Token作為參數固化於小程式碼當中
  3. 監控整個鑒權過程狀態,一旦狀態變為AUTHORIZED(已授權)則獲取小程式登錄憑證code。調用ExternalAuthenticate完成登錄。

上一章,我們介紹了服務端的開發,這次我們需要調用GetACode,GetToken,分別獲取小程式碼,和獲取當前狀態

首先使用vue-cli創建一個web項目,命名為mp-auth

vue create mp-auth

新建ajaxRequest.ts,創建request對象,這一對象將利用axios庫發送帶有訪問憑證的Header的請求

這裡使用js-cookie庫獲取cookie中的訪問憑證,並添加到Header中 

import Cookies from "js-cookie";
import axios, {  CancelTokenSource } from 'axios'
//發送網路請求
const tokenKey = "main_token";
const getToken = () => Cookies.get(tokenKey);

export const request = async (url: string, methods, data: any, onProgress?: (e) => void, cancelToken?: CancelTokenSource) => {
    let token = null
    let timeout = 3000;
    if (cancelToken) {
        token = cancelToken.token
        timeout = 0;
    }

    const service = axios.create()
    service.interceptors.request.use(
        (config) => {
            const token = getToken();
            // Add X-Access-Token header to every request, you can add other custom headers here
            if (token) {
                config.headers['X-XSRF-TOKEN'] = token
                config.headers['Authorization'] = 'Bearer ' + token
            }
            return config
        },
        (error) => {
            Promise.reject(error)
        }
    )

    const re = await service.request({
        url: url,
        method: methods,
        data: data,
        cancelToken: token,
        timeout: timeout,
        onUploadProgress: function (progressEvent) { //原生獲取上傳進度的事件
            if (progressEvent.lengthComputable) {
                if (onProgress) {
                    onProgress(progressEvent);
                }
            }
        },
    })
    return re as any;
}

///獲得取消令牌
export const getCancelToken = () => {
    const source = axios.CancelToken.source();
    return source;
}

回到App.vue

我們按照網頁端這個三個職責的順序,分步驟完成程式碼

生成Token

首先建立兩個變數,存儲當前的Token和狀態枚舉值

export default {
  name: "App",
  data: () => {
    return {
      wechatMiniappLoginToken: null,
      wechatMiniappLoginStatus: "WAIT",
    };
  },

methods中建立getToken函數,這裡使用8位隨機數作為token值

  methods: {
    getToken() {
      if (this.wechatMiniappLoginToken == null) {
        var date = new Date();
        var token = `${(Math.random() * 100000000)
          .toFixed(0)
          .toString()
          .padEnd(8, "0")}`;
        this.wechatMiniappLoginToken = token;
      }
      return this.wechatMiniappLoginToken;
    }
   }

生成小程式碼

Html部分,插入一個圖片,將token傳入scene參數

<img :src="`${prefix}/MiniProgram/GetACode?scene=${getToken()}&page=${miniappPage}&mode=content`"/>

Prefix是你的服務地址前綴

prefix: "//localhost:44311/api/services/app"

page為小程式中鑒權頁面的路徑,需注意的是在小程式未發布時無法跳轉至頁面,報錯41030,若要使用掃碼來跳轉指定頁面,小程式需要先發布

miniappPage: "pages/login/index"

監控整個鑒權過程狀態

首先需要一個函數,根據當前的Token獲取當前鑒權狀態,並且不斷循環這一操作,這裡編寫start函數,並以每1秒鐘輪詢狀態,程式碼如下:

   start() {
      clearInterval(this.timerId);
      this.timerId = setInterval(async () => {
        if (!this.loading) {
          this.loading = true;

          await request(
            `${this.prefix}/MiniProgram/GetToken?token=${this.wechatMiniappLoginToken}`,
            "get",
            null
          )            
        }
      }, 1000);
    },

在頁面開始函數程式碼Created中調用這一函數

  created: function () {
    this.start();
  },

接下來處理輪詢結果,如果沒有拿到值,說明Token已過期,wechatMiniappLoginStatus狀態為”EXPIRED”

          await request(
            `${this.prefix}/MiniProgram/GetToken?token=${this.wechatMiniappLoginToken}`,
            "get",
            null
          )
            .then(async (re) => {
              if (re.data.result == null) {
                this.wechatMiniappLoginStatus = "EXPIRED";
                this.wechatMiniappLoginToken = null;
                this.loading = false;
              }

注意:

在後端項目的MiniProgramAppService.cs中,我們定義的

TokenCacheDuration為5分鐘,表明二維碼的有效時間為5分鐘。

public static TimeSpan TokenCacheDuration = TimeSpan.FromMinutes(5);

相應的Token為Expired時,將wechatMiniappLoginToken置空,這一屬性變動vue會通知img的src值變動而刷新小程式碼,同時獲取新的Token值賦值給wechatMiniappLoginToken,這也是刷新小程式碼的邏輯

this.wechatMiniappLoginToken = null;

這樣能以簡單方式,實現二維碼刷新功能。

介面中新建一個刷新小程式碼的按鈕:

      <el-button
        v-if="wechatMiniappLoginToken != null"
        type="primary"
        size="medium"
        @click="wechatMiniappLoginToken = null"
        >刷新
      </el-button>

編寫一個externalLogin方法,在用於獲取Code後,調用後端第三方登錄介面,獲取訪問憑證存儲於Cookie中

async externalLogin(userInfo: {
      authProvider: string;
      providerKey: string;
      providerAccessCode: string;
    }) {
      let authProvider = userInfo.authProvider;
      let providerKey = userInfo.providerKey;
      let providerAccessCode = userInfo.providerAccessCode;

      await request(
        `//localhost:44311/api/TokenAuth/ExternalAuthenticate`,
        "post",
        {
          authProvider,
          providerKey,
          providerAccessCode,
        }
      ).then(async (res) => {
        var data = res.data.result;
        setToken(data.accessToken);
      });
    },

 定義setToken函數,使用js-cookie庫將訪問憑證寫入瀏覽器cookie中

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

 

在此之前我們需寫一個參數傳遞對象,為了保留一定的擴展能力,data中我們定義loginExternalForms,已經實現的微信小程式登錄,則對應的authProvider值為「WeChatAuthProvider」,providerAccessCode則為生成的Token值

      loginExternalForms: {
        WeChat: {
          authProvider: "WeChatAuthProvider",
          providerKey: "default",
          providerAccessCode: "",
        },
      },

接下來包裝externalLogin方法,在調用完成前後做一些操作,比如登錄成功後,將調afterLoginSuccess方法

為了保留一定的擴展能力,handleExternalLogin函數中我們保留參數authProvider,已實現的微信小程式登錄handleWxLogin函數調用時傳遞參數”WeChat”

    async handleExternalLogin(authProvider) {
      // (this.$refs.baseForm as any).validate(async (valid) => {
      //   if (valid == null) {
      var currentForms = this.loginExternalForms[authProvider];

      this.loading = true;
      return await this.ExternalLogin(currentForms).then(async (re) => {
        return await request(
          `${this.prefix}/User/GetCurrentUser`,
          "get",
          null
        ).then(async (re) => {
          var result = re.data.result as any;
          return await this.afterLoginSuccess(result);
        });
      });
    },

    async handleWxLogin(providerAccessCode) {
      this.loginExternalForms.WeChat.providerAccessCode = providerAccessCode;
      return await this.handleExternalLogin("WeChat");
    },

afterLoginSuccess函數用於登錄成功後的邏輯,停止計時器,並跳轉頁面,本實例僅做彈窗提示

    successMessage(value = "執行成功") {
      this.$notify({
        title: "成功",
        message: value,
        type: "success",
      });
    },    

    async afterLoginSuccess(userinfo) {
      clearInterval(this.timerId);
      this.successMessage("登錄成功");
      this.userInfo = userinfo;
    },

繼續編寫start函數

如果拿到的token至不為空,則傳遞值給wechatMiniappLoginStatus,當wechatMiniappLoginStatus狀態為”AUTHORIZED”時調用handleWxLogin函數:

              if (re.data.result == null) {
                this.wechatMiniappLoginStatus = "EXPIRED";
                this.wechatMiniappLoginToken = null;
                this.loading = false;
              } else {
                var result = re.data.result;
                this.wechatMiniappLoginStatus = result.status;
                if (
                  this.wechatMiniappLoginStatus == "AUTHORIZED" &&
                  result.providerAccessCode != null
                ) {
                  await this.handleWxLogin(result.providerAccessCode)
                    .then(() => {
                      this.wechatMiniappLoginToken = null;
                      this.loading = false;
                    })
                    .catch((e) => {
                      this.wechatMiniappLoginToken = null;
                      this.loading = false;
                      clearInterval(this.timerId);
                    });
                } else {
                  this.loading = false;
                }
              }

接下來簡單編寫一個介面,

介面將清晰的反映wechatMiniappLoginStatus各個狀態時對應的UI交互:

WAIT(等待掃碼):


ACCESSED(已掃碼):


 ACCESSED(已掃碼):


 

完整的Html程式碼如下:

<template>
  <div id="app">
    <!-- <img alt="Vue logo" src="./assets/logo.png" />
    <HelloWorld msg="Welcome to Your Vue.js App" /> -->
    <div style="height: 450px">
      <div v-if="wechatMiniappLoginStatus == 'ACCESSED'">
        <el-result
          icon="info"
          title="已掃碼"
          subTitle="請在小程式上根據提示進行操作"
        >
        </el-result>
      </div>

      <div v-else-if="wechatMiniappLoginStatus == 'AUTHORIZED'">
        <el-result
          icon="success"
          title="已授權"
          :subTitle="loading ? '請稍候..' : '正在使用微信帳號登錄系統'"
        >
        </el-result>
      </div>
      <div v-else class="center">
        <img
          :src="`${prefix}/MiniProgram/GetACode?scene=${getToken()}&page=${miniappPage}&mode=content`"
        />
      </div>
    </div>
    <div class="center">
      <el-button
        v-if="wechatMiniappLoginToken != null"
        type="primary"
        size="medium"
        @click="wechatMiniappLoginToken = null"
        >刷新</el-button
      >
    </div>
    <div class="center">
      <span>{{ userInfo }}</span>
    </div>
  </div>
</template>

至此我們已完成網頁端的開發工作