Code前端首页关于Code前端联系我们

小程序登录流程 - 连接小程序和服务器代码

terry 2年前 (2023-09-23) 阅读数 90 #移动小程序

用户登录是最完整的应用程序的必要过程

一个简单的用户系统至少要注意这几个方面

  • 安全(加密)
  • 持久性登录状态(类似cookie)
  • 登录过期处理
  • 确保用户唯一性,避免多个账户
  • 授权
  • 绑定用户名、头像等真实手机信息 B❝ 密码保护方式)

很多业务需求可以抽象为Restful接口来配合CRUD操作

但是登录流程比较复杂,各个平台都有自己的流程。事实上,它已经成为项目中耗时的部分,就像小程序一样。登录流程小程序登录流程-附小程序和服务端代码

对于一个从零开始的项目来说,正确的登录流程是一个好的开始,好的开始是成功的一半

本文以微信小程序为平台,全面定制用户登录流程,我们来一起啃这块难啃的骨头

术语解释

首先简单解释一下登录过程时序图中出现的概念

  • 代码临时凭证,有效期五年分钟,获取会话密钥
  • Session_key会session_key通过wx.login(),服务器将获得'openid唯一用户名'openid'从未更改过,服务器将获得,服务器将获得通过二维码(公众号、小程序、网站、手机应用)获取微信开放平台同一账号下用户的
  • unionId。永远不会改变
  • appId小程序唯一标识
  • appSecret可换取session_key的code和appId的小程序app Secretnew rawData不含敏感信息的原始数据串,用于计算签名
  • encryptedData 包含敏感信息的用户信息已加密
  • 用户签名未被伪造 ” iv 初始加密算法向量

哪些信息是敏感信息?手机号、openId、unionId,可见这些值可以唯一定位用户,而无法找到用户的昵称、头像等不属于敏感信息

与登录相关的功能小程序

  • wx.login
  • wx.getUserInfo
  • wx.checkSession

Promise

小程序

我们发现小程序的异步接口是成功和失败回调。写起来很容易回调地狱

所以我们可以简单地实现一个异步wx函数,先将其转换为promise工具函数

const promisify = original => {
  return function(opt) {
    return new Promise((resolve, reject) => {
      opt = Object.assign({
        success: resolve,
        fail: reject
      }, opt)
      original(opt)
    })
  }
}
复制代码

这样我们就可以这样调用函数

promisify(wx.getStorage)({key: 'key'}).then(value => {
  // success
}).catch(reason => {
  // fail
})
复制代码

服务器端实现

本demo的服务器端实现是基于express.js的

注意,为了demo的简单,服务器使用js变量来存储用户数据,这意味着如果重启服务器,用户数据将会被已删除

如果需要永久存储用户数据,可以自行实现数据库相关逻辑

// 存储所有用户信息
const users = {
  // openId 作为索引
  openId: {
    // 数据结构如下
    openId: '', // 理论上不应该返回给前端
    sessionKey: '',
    nickName: '',
    avatarUrl: '',
    unionId: '',
    phoneNumber: ''
  }
}

app
  .use(bodyParser.json())
  .use(session({
    secret: 'alittlegirl',
    resave: false,
    saveUninitialized: true
  }))
复制代码

小程序登录

首先我们实现基本的oauth授权登录

oau代码交换流程openId和sessionKey

前端小程序登录

写在app.js中

login () {
  console.log('登录')
  return util.promisify(wx.login)().then(({code}) => {
    console.log(`code: ${code}`)
    return http.post('/oauth/login', {
      code,
      type: 'wxapp'
    })
  })
}
复制代码

服务端实现Oauth授权

服务端实现上面的‶/小程序登录流程-附小程序和服务端代码

这个接口

app
  .post('/oauth/login', (req, res) => {
    var params = req.body
    var {code, type} = params
    if (type === 'wxapp') {
      // code 换取 openId 和 sessionKey 的主要逻辑
      axios.get('https://api.weixin.qq.com/sns/jscode2session', {
        params: {
          appid: config.appId,
          secret: config.appSecret,
          js_code: code,
          grant_type: 'authorization_code'
        }
      }).then(({data}) => {
        var openId = data.openid
        var user = users[openId]
        if (!user) {
          user = {
            openId,
            sessionKey: data.session_key
          }
          users[openId] = user
          console.log('新用户', user)
        } else {
          console.log('老用户', user)
        }
        req.session.openId = user.openId
        req.user = user
      }).then(() => {
        res.send({
          code: 0
        })
      })
    } else {
      throw new Error('未知的授权类型')
    }
  })
复制代码

获取用户信息

登录系统中有一个重要的功能:获取用户信息,我们称之为 getUserInfo

如果登录用户调用 getUserInfo,例如,则返回用户信息昵称、头像等。如果没有登录,则返回“用户未登录”

也就是说,这个接口还有一个功能 判断用户是否登录...

小程序用户信息一般存储在app.globalData.userInfo(模板是这样的)

在服务器端,我们添加前端中间件,我们通过session获取对应的用户信息,并把请求V对象中

app
  .use((req, res, next) => {
    req.user = users[req.session.openId]
    next()
  })
复制代码

然后实现接口/user/info,用于返回用户信息

app
  .get('/user/info', (req, res) => {
    if (req.user) {
      return res.send({
        code: 0,
        data: req.user
      })
    }
    throw new Error('用户未登录')
  })
复制代码

Applet调用用户信息接口专门为小程序发送而设计的库requests

小程序 代码通过 http.gethttp.post 等 API 发送请求,背后使用 requests 库。

@chunpu/http 是专门为小程序设计的http。 request库,可以输入小程序的请求,比如axios,支持拦截器等强大功能,甚至比axios还方便

初始化方法如下

import http from '@chunpu/http'

http.init({
  baseURL: 'http://localhost:9999', // 定义 baseURL, 用于本地测试
  wx // 标记是微信小程序用
})
复制代码

具体使用请参考github文档。 com/chunpu/http…

自定义登录状态持久化

浏览器有cookie,但小程序没有cookie,那么如何模仿网页的登录状态?

这里使用的是自定义小程序持久化接口,即setStorage和getStorage

为了方便各方共享接口或者直接复用Web接口,我们引入一个简单的我们自己读取cookie和保存cookie的逻辑

首先,我们必须根据http响应返回的标头,文件是种子cookie。这里我们使用@chunpu/http中的响应拦截器,与使用axios是一样的。当然,我们在输入请求时也需要带上所有cookie。这就是请求拦截器被使用的地方。

http.interceptors.request.use(config => {
  // 给请求带上 cookie
  return util.promisify(wx.getStorage)({
    key: 'cookie'
  }).catch(() => {}).then(res => {
    if (res && res.data) {
      Object.assign(config.headers, {
        Cookie: http.qs.stringify(res.data, ';', '=')
      })
    }
    return config
  })
})
复制代码

登录状态有效期

我们知道浏览器登录状态cookie已经过期。比如一天、七天或者一个月

有的朋友可能会问。如果我们直接使用存储,小程序的登录状态有效期该怎么办?

对了!小程序帮助我们实现了这次会议。有效性时间评估wx.checkSession

比cookies更智能。官方文档是这样描述的

通过wx.login接口获取用户的登录状态具有一定的时效性。用户不使用小程序的时间越长,用户登录状态失败的可能性就越大。另一方面,如果用户使用了小程序,用户的登录状态将一直保持有效

也就是说,小程序也会帮助我们自动重置我们的登录状态,这只是一款人工智能饼干,你喜欢吗?

如何具体控制前端?代码写在app.js中

onLaunch: function () {
  util.promisify(wx.checkSession)().then(() => {
    console.log('session 生效')
    return this.getUserInfo()
  }).then(userInfo => {
    console.log('登录成功', userInfo)
  }).catch(err => {
    console.log('自动登录失败, 重新登录', err)
    return this.login()
  }).catch(err => {
    console.log('手动登录失败', err)
  })
}
复制代码

需要注意的是,这里的session不仅仅是前端的登录状态,还有后端的session_key有效期。如果前端登录状态无效,那么终端也无效的话,就需要更新session_key

理论上小程序也可以自定义登录过期时间策略,但这种情况下,我们需要考虑开发者自身的过期时间和小程序接口服务过期的时间。最好是统一的。简单

确保每个页面都能获取用户信息

如果您选择 在新的小程序项目中创建通用的快速启动模板

,我们得到一个可以直接运行的模板

open代码Na一看,大部分代码都是处理userInfo的....小程序登录流程-附小程序和服务端代码

注释说

因为getUserInfo是网络请求,在Page.onLoad之后可能会返回

,所以这里加了一个回调以避免这。这种情况

但是这样的模板并不科学。它只考虑主页需要用户信息的情况。如果扫码进入的页面也需要用户信息怎么办?还有直接跳转到未付费页面活动的页面等等...

如果每个页面都这样评估用户信息是否已经加载,代码就显得太冗余了

此时我们想jQuery准备好的函数$(function),如果文档准备好了,就可以直接运行函数中的代码了。如果文档还没准备好,就等文档准备好了再运行代码

这是一个想法!我们把小程序应用视为一个网站文档

我们的目标是获取页面上的userInfo没有错误的页面

Page({
  data: {
    userInfo: null
  },
  onLoad: function () {
    app.ready(() => {
      this.setData({
        userInfo: app.globalData.userInfo
      })
    })
  }
})
复制代码

这里实现这个功能我们使用min-ready

代码实现还是写在app.js中

import Ready from 'min-ready'

const ready = Ready()

App({
  getUserInfo () {
    // 获取用户信息作为全局方法
    return http.get('/user/info').then(response => {
      let data = response.data
      if (data && typeof data === 'object') {
        this.globalData.userInfo = data
        // 获取 userInfo 成功的时机就是 app ready 的时机
        ready.open()
        return data
      }
      return Promise.reject(response)
    })
  },
  ready (func) {
    // 把函数放入队列中
    ready.queue(func)
  }
})
复制代码

连接用户信息和手机号码

仅仅获取到用户的openId。 OpenId只能标记用户,甚至不能标记用户的昵称和头像。

如何获取这个用户信息并存储到后端数据库中?

我们在服务器端实现这两个接口,链接用户信息 绑定用户手机号码

app
  .post('/user/bindinfo', (req, res) => {
    var user = req.user
    if (user) {
      var {encryptedData, iv} = req.body
      var pc = new WXBizDataCrypt(config.appId, user.sessionKey)
      var data = pc.decryptData(encryptedData, iv)
      Object.assign(user, data)
      return res.send({
        code: 0
      })
    }
    throw new Error('用户未登录')
  })

  .post('/user/bindphone', (req, res) => {
    var user = req.user
    if (user) {
      var {encryptedData, iv} = req.body
      var pc = new WXBizDataCrypt(config.appId, user.sessionKey)
      var data = pc.decryptData(encryptedData, iv)
      Object.assign(user, data)
      return res.send({
        code: 0
      })
    }
    throw new Error('用户未登录')
  })
复制代码

wxml小程序个人中心实现如下

<view wx:if="userInfo" class="userinfo">
  <button
    wx:if="{{!userInfo.nickName}}"
    type="primary"
    open-type="getUserInfo"
    bindgetuserinfo="bindUserInfo"> 获取头像昵称 </button>
  <block wx:else>
    <image class="userinfo-avatar"  mode="cover"></image>
    <text class="userinfo-nickname">{{userInfo.nickName}}</text>
  </block>

  <button
    wx:if="{{!userInfo.phoneNumber}}"
    type="primary"
    style="margin-top: 20px;"
    open-type="getPhoneNumber"
    bindgetphonenumber="bindPhoneNumber"> 绑定手机号 </button>
  <text wx:else>{{userInfo.phoneNumber}}</text>
</view>
复制代码

小程序中的函数bindUserInfo和bindPhoneNumber。根据最新的微信策略,这两个操作都需要用户点击按钮才可以运行

bindUserInfo (e) {
  var detail = e.detail
  if (detail.iv) {
    http.post('/user/bindinfo', {
      encryptedData: detail.encryptedData,
      iv: detail.iv,
      signature: detail.signature
    }).then(() => {
      return app.getUserInfo().then(userInfo => {
        this.setData({
          userInfo: userInfo
        })
      })
    })
  }
},
bindPhoneNumber (e) {
  var detail = e.detail
  if (detail.iv) {
    http.post('/user/bindphone', {
      encryptedData: detail.encryptedData,
      iv: detail.iv
    }).then(() => {
      return app.getUserInfo().then(userInfo => {
        this.setData({
          userInfo: userInfo
        })
      })
    })
  }
}
复制代码

代码

本文提到的代码可以在我的github上找到

小程序代码在wxapp-login -demo

服务端代码Node.js在wxapp-login -server

作者:七物周刊
链接:https://juejin.im/post/5bda95a22223f来源:掘金
版权归作者所有。商业转载请联系作者获得许可。商业请注明来源。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门