2

Vue3项目中微信授权登录的优雅实现

 3 years ago
source link: https://segmentfault.com/a/1190000040649617
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Vue3项目中微信授权登录的优雅实现

微信授权登录是做微信公众号开发一直绕不开的话题,而且整个授权登录流程的实现,是需要前后端配合一起完成的。在过去前后端还未分离的年代,也许我们前端并不需要太过关心授权的具体实现。然而现在都2021年了,前后端分离的架构大行其道,如何在前后端分离的情况下实现微信授权登录就成了今天要探讨的重点问题。

首先,我们还是需要先梳理下微信授权整个流程是怎样的,这里我就直接将官方文档搬来:

如果用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。
...
关于网页授权的两种scope的区别说明
1、以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)
2、以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。
...
具体而言,网页授权流程分为四步:
1、引导用户进入授权页面同意授权,获取code
2、通过code换取网页授权access_token(与基础支持中的access_token不同)
3、如果需要,开发者可以刷新网页授权access_token,避免过期
4、通过网页授权access_token和openid获取用户基本信息(支持UnionID机制)

这里附上微信公众号开发之微信授权的官方文档

以上是笔者提炼出来的比较关键的几点信息,当然还有更多的说明,希望新手读者们还是先认真看完官方文档。

这里我再补充说明下,以上流程的4个步骤中,除了第一步以外,另外三步都是需要在服务器端去完成的。前端要做的核心其实是怎么进行用户登录状态的检查判断和登录状态的维护。

大家都知道,Vue是前后端分离技术方案下的产物,它是一个纯前端应用(服务端渲染除外)。通常我们是需要在用户打开页面,执行到页面的js脚本时,我们再异步去请求服务端数据,再进行相关逻辑的处理和判断。我们要实现微信授权登录的前提是,需要先判断用户是否需要登录(cookie或者token)。当用户未登录时,才需要走授权登录流程,当授权登录成功后,我们也需要在前端记录好登录状态,以方便在页面切换时,不用再次触发授权登录。再通过分析可知,前端其实能做的就是获取微信服务器给我们的code,再将code给我们的后端,让后端完成后续步骤拿到用户信息后生成用户。那么整个过程我再梳理如下:

  1. (前端)检查用户是否登录;
  2. (前端)如果未登录,引导用户进入授权页面同意授权,获取code
  3. (前端)将获取到的code提交给后端
  4. (后端)通过code换取用户凭证openid
  5. (后端)通过openid检查用户是否存在,是否需要注册新用户,并获取用户id
  6. (后端)返回用户信息;
  7. (前端)记录用户登录状态,跳回登录前页面;

这个过程,我画了个图,如下:

image.png

根据以上思路,现在开始编码环节。笔者采用的是Vue3,Vue2的开发者还请根据情况做适当调整。
为了方便唤起用户授权登录逻辑,笔者打算将授权登录封住为一个login页面,这样做的好处是我们在任何判断到需要登录的地方直接通过Vue Router的push方法跳转到登录页面即可。
通常情况下,我们的应用并不是所有页面都需要登录后才能访问,只有在访问特定页面时候,才需要用户登录,那么我们就需要标识哪些页面需要进行登录鉴权。这里我们可以利用Vue Router的meta属性来进行标识,官方文档对meta解释如下:

有时,你可能希望将任意信息附加到路由上,如过渡名称、谁可以访问路由等。这些事情可以通过接收属性对象的meta属性来实现,并且它可以在路由地址和导航守卫上都被访问到。

刚好Vue Router官方就有示例,如下:

const routes = [
  {
    path: '/posts',
    component: PostsLayout,
    children: [
      {
        path: 'new',
        component: PostsNew,
        // 需要登录后才能访问的页面
        meta: { requiresAuth: true }
      },
      {
        path: ':id',
        component: PostsDetail,
        // 任何人都可访问的页面
        meta: { requiresAuth: false }
      }
    ]
  }
]

接下来我们就可以在Vue Router的全局守卫beforeEach中获取到这个元信息从而做登录跳转了

router.beforeEach((to, from) => {
  // 而不是去检查每条路由记录
  // to.matched.some(record => record.meta.requiresAuth)
  if (to.meta.requiresAuth && !userStore.isLogin) {
    // 此路由需要授权,请检查是否已登录
    // 如果没有,则重定向到登录页面
    return {
      path: '/login',
      // 保存我们所在的位置,以便以后再来
      query: { redirect: to.fullPath },
    }
  }
})

需要补充说明的是,userStore.isLogin的实现。这里和我们实际采用的登录态维护方案有关,如果是采用token方式的话,那就是检查token是否已经存在。笔者采用了vuex作为来保存token,然后借助插件来将Store中的数据持久化到localStorage。

接下来我们来看具体的实现:
login.vue: 登录组件

<template>
  <div class="login"></div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

import { jump2Auth, getUserInfo } from '@/hooks/useWechatAuth'
import { userStore } from '@/store/modules/user'
import { redirectTo, getRouteQuery } from '@/hooks/usePage'

export default defineComponent({
  name: 'Login',
  setup() {
    let code = getRouteQuery().code as string
    // 3.如果有code,则已经授权
    if (code) {
      getUserInfo(code as string).then((res: any) => {
        // 记录token
        userStore.saveToken(res.access_token)
        const redirect = userStore.userState.landPageRoute || '/'
        // 跳转到授权前访问的页面
        redirectTo(redirect)
      })
    } else {
      // 1.记录上一个页面的地址
      const { redirect } = getRouteQuery()
      if (redirect) {
        userStore.setLandPage(redirect as string)
      }
      // 2.跳转授权
      const callbackUrl = window.location.origin + window.location.pathname
      jump2Auth(callbackUrl)
    }
  },
})
</script>

可以看到,login页面其实并没有什么内容,跳转到该页面后,我们会直接重定向到微信授权的页面,授权回调回来也会回到该页面,此时我们再通过获取路由参数的方式获取code参数。
@/hooks/usePage.ts: 该文件主要是封装了router相关的常用方法

import router from '@/router'
import { cloneDeep } from 'lodash'
import { toRaw } from 'vue'

/**
 * 重定向
 * @param path 路径
 */
export function redirectTo(path: string) {
  const { replace } = router
  replace({
    path,
  })
}

/**
 * 获取路由上query参数
 */
export function getRouteQuery() {
  const { currentRoute } = router
  const { query } = currentRoute.value
  return cloneDeep(query)
}

@/hooks/useWechatAuth.ts:该文件封装了微信授权与后端交互的请求

import { useAxios } from '@/hooks/useAxios'

/**
 * 获取微信授权的跳转地址
 * @param callbackUrl 授权后回调链接
 * @returns
 */
export function jump2Auth(callbackUrl: string) {
  useAxios({
    url: '/api/wechat/auth',
    params: {
      redirect_url: callbackUrl,
    },
  }).then((authUrl: any) => {
    if (process.env.NODE_ENV === 'development') {
      window.location.href = callbackUrl + '?code=test'
    } else {
      window.location.href = authUrl
    }
  })
}

/**
 * 提交code进行登录
 * @param code
 * @returns
 */
export async function getUserInfo(code: string) {
  const userInfo = await useAxios({
    method: 'POST',
    url: '/api/wechat/auth',
    params: {
      code,
    },
  })
  return userInfo
}

@/store/modules/user.ts: 全局状态存储,主要是记录token和登录前访问页面

import { Module, VuexModule, Mutation, getModule, Action } from 'vuex-module-decorators'
import store from '@/store'
import { initialUnencryptedStorage } from '../globals'

interface UserState {
  token: string
  landPageRoute: string
}

const NAME = 'user'
// name: 模块名字
// namespaced 表示开启命名空间
// dynamic设置为true时,表示创建动态模块,运行时将模块注册到存储中
// preserveState 如果数据有持久化,该变量为true时可以从storage中拿取初始值
@Module({
  namespaced: true,
  name: NAME,
  dynamic: true,
  store,
  preserveState: Boolean(initialUnencryptedStorage[NAME]),
})
export class User extends VuexModule {
  userState: UserState = {
    token: '',
    /** 登录前访问页面 */
    landPageRoute: '',
  }

  get isLogin(): boolean {
    return !!this.userState.token
  }

 
  @Mutation
  saveToken(token: string): void {
    this.userState.token = token
  }

  @Mutation
  setLandPage(route: string): void {
    this.userState.landPageRoute = route
  }
}

export const userStore = getModule<User>(User)

笔者借助vuex-persistedstate插件将store中数据存储到了localStorage,这样做的好处是用户关闭页面后,再次访问,即不用重新触发微信授权流程,大大优化了用户体验。

不得不说,Vue3在代码抽象和复用这块上,写起来着实舒服很多,希望大家也也尝试着按照官方实践那样,多将逻辑代码解耦抽离,生成一个个的hook,这样代码就显得优雅多啦。该方案经过笔者尝试论证,不论是代码整洁优雅程度,还是业务需求的实现上,都几乎完美(请容我装一波b)。当然,这里可能存在我没发现的bug或者痛点,毕竟从来没有十全十美的架构嘛,这里也欢迎看官大佬们和我交流探讨,提供更好的方案和想法。

最近工作稍微闲了些,笔者打算用Vue3+ts+vant从0开始完成一个公众号应用并将开发过程分享出来,也算是激励自己持续学习的一个方式吧。那么,笔者做的是怎样的一个公众号应用呢?是的,考虑到看官老爷们写代码太忙,没空找对象,所以笔者打算做一个分配对象的公众号应用(是不是很刺激啊 )。别人都考虑你技术够不够好,只有我关心你有没有对象哈哈哈。欢迎扫下边二维码体验,功能还在不断完善中,如果有想法和建议,欢迎找我探讨哦。
qrcode_for_gh_cfb38d16f4f4_344.jpeg
同时,为方便大家更好的探讨微信公众号开发相关技术,笔者也建了一个微信群,欢迎加入和大家一起学习成长。添加笔者微信imwty2020,即可进群


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK