使用 Abp.Zero 搭建第三方登录模块(三):网页端开发

简短回顾一下网页端的流程,总的来说网页端的职责有三:

  1. 生成一个随机字符作为鉴权会话的临时Token,
  2. 生成一个小程序码, Token作为参数固化于小程序码当中
  3. 监控整个鉴权过程状态,一旦状态变为AUTHORIZED(已授权)则获取小程序登录凭证code。调用ExternalAuthenticate完成登录。
  4. 上一章,我们介绍了服务端的开发,这次我们需要调用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;
    }

    <span class="hljs-keyword">const service = axios.<span class="hljs-title function_">create()
    service.<span class="hljs-property">interceptors.<span class="hljs-property">request.<span class="hljs-title function_">use(
        <span class="hljs-function">(<span class="hljs-params">config) =&gt; {
            <span class="hljs-keyword">const token = <span class="hljs-title function_">getToken();
            <span class="hljs-comment">// Add X-Access-Token header to every request, you can add other custom headers here
            <span class="hljs-keyword">if (token) {
                config.<span class="hljs-property">headers[<span class="hljs-string">'X-XSRF-TOKEN'] = token
                config.<span class="hljs-property">headers[<span class="hljs-string">'Authorization'] = <span class="hljs-string">'Bearer ' + token
            }
            <span class="hljs-keyword">return config
        },
        <span class="hljs-function">(<span class="hljs-params">error) =&gt; {
            <span class="hljs-title class_">Promise.<span class="hljs-title function_">reject(error)
        }
    )
    
    <span class="hljs-keyword">const re = <span class="hljs-keyword">await service.<span class="hljs-title function_">request({
        <span class="hljs-attr">url: url,
        <span class="hljs-attr">method: methods,
        <span class="hljs-attr">data: data,
        <span class="hljs-attr">cancelToken: token,
        <span class="hljs-attr">timeout: timeout,
        <span class="hljs-attr">onUploadProgress: <span class="hljs-keyword">function (<span class="hljs-params">progressEvent) { <span class="hljs-comment">//原生获取上传进度的事件
            <span class="hljs-keyword">if (progressEvent.<span class="hljs-property">lengthComputable) {
                <span class="hljs-keyword">if (onProgress) {
                    <span class="hljs-title function_">onProgress(progressEvent);
                }
            }
        },
    })
    <span class="hljs-keyword">return re <span class="hljs-keyword">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: "https://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;
    
    
          <span class="hljs-keyword">await <span class="hljs-title function_">request(
            <span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-variable language_">this.prefix}/MiniProgram/GetToken?token=<span class="hljs-subst">${<span class="hljs-variable language_">this.wechatMiniappLoginToken}`,
            <span class="hljs-string">"get",
            <span class="hljs-literal">null
          )            
        }
      }, <span class="hljs-number">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(
            `https://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>
    

    至此我们已完成网页端的开发工作

    使用 Abp.Zero 搭建第三方登录模块(三):网页端开发

    https://blog.matoapp.net/posts/571d661/

    作者

    林晓lx

    发布于

    2022-07-19

    更新于

    2024-09-11

    许可协议

    评论