小程序最佳实践-登录授权篇

最近在学习使用taro+dva编写小程序,依然是springboot的后台。
以下是学习过程中对于登录授权最佳实践的一些理解和实现。
也许对于其他架构来讲不是最佳,仅供参考


一、程序官方文档中关于登录授权的接口及组件

1.wx.login:

调用接口获取登录凭证(code)。持凭证通过应用侧后台接口换取用户登录态信息,包括用户的唯一标识(openid)及本次登录的会话密钥(session_key)等。
其中session_key用于后续用户数据的加解密

2.wx.checkSession

检查登录态是否过期。
通过 wx.login 接口获得的用户登录态拥有一定的时效性(时长不一定)。用户越久未使用小程序,用户登录态越有可能失效。反之如果用户一直在使用小程序,则用户登录态一直保持有效。具体时效逻辑由微信维护,对开发者透明。开发者只需要调用 wx.checkSession 接口检测当前用户登录态是否有效。

3.按钮发起授权请求

<button open-type="getUserInfo"/>
触发请求后会弹出授权窗口,同意后可获得用户信息(关键数据加密(openid)),关键数据通过wx.login获取的session_key解密。

4.wx.getUserInfo

获取用户信息。
不会弹出授权窗口,调用成功的前提是用户通过【3.按钮发起授权请求】已同意授权。


二、应用侧实现思路

网上各种实现方案中,有些是把登录和授权并做一步,即按钮授权回调中调用wx.login获取code,然后把授权回调数据及code同时传到后台进行登录。
这样实现有以下两个问题:

  1. 授权回调数据加密key可能与wx.login返回新ode获取的session_key不一致,导致解密失败,报错illegal。
  2. 授权前无法获取自定义登录态,授权与登录合并一步使得判断变得复杂。

在我们的应用中,将登录和获取用户信息分为两步,基于以下考虑:

  1. 小程序登录是无需用户感知的,小程序应用应该充分利用小程序的自动登录接口,静默建立用户体系,降低用户使用部分功能的门槛,同时还可以在一定程度上保护接口数据不被外接随意访问。
  2. 对于一些关键性操作,可以向用户申请授权。这也是小程序将登录和授权分离的初衷。

三、应用侧实现方案(基于taro+dva+springboot)

1. 小程序打开时自动登录

此处有个问题,因为小程序初始化及页面初始化是同步进行的。若页面初始化时,小程序初始化中登录请求仍未完成。会导致未携带token,鉴权失败。
所以在实现登录时使用Promise,登录成功后调用接口。
但又带来另一个问题,如果接口较多,则会调用多次登录,所以在登录方法引入队列,同时只允许一个登录进程运行,一个运行成功后就可以直接执行登录后续的接口。

app.js

componentWillMount() {
    auth.login();
}

auth.login()

 - 首先判断是否已登录,即:存在自定义登录态(token)、且wx.checkSession()有效。
 - 如果已登录,返回true
 - 如果未登录,调用登录方法auth.loginProcess()

auth.loginProcess()

 - 使用队列实现登录
 - 如果正在登录中,将resolve存入队列。
 - 调用微信登录wx.login
 - 调用服务端登录,通过code换取openid、session_key、unionid等,同时根据openid查询或建立用户体系,生成jwttoken(token中带着授权态,老用户如果已授权,后续需要授权访问的地方可省去授权步骤)返回前端
 - Taro.setStorageSync保存token
 - 消耗队列
2. http请求携带token

request.js

代码仅供参考

/**
 * 封装微信的的request
 */
function request(url, data = {}, method = "GET") {
  const authorization = Taro.getStorageSync(Cons.WX_MINIAPP_HEADER_AUTH) || {};

  return new Promise(function(resolve, reject) {
    Taro.request({
      url: url,
      data: data,
      method: method,
      header: {
        'Content-Type': 'application/json',
        'Pragma': 'no-cache',
        [Cons.WX_MINIAPP_HEADER_AUTH]: authorization || '' // 此处为token
      },
      success: function(res) {
        if (res && (res.statusCode === 200 || res.statusCode === 304)) {
          resolve(res.data);
          return;
        }

        const { statusCode } = res;
        if (statusCode === 401) {
          let sessionStatus = res.header['SESSION-STATUS'];
          // 未登录
          if (sessionStatus && sessionStatus === 'WX_MINIAPP_NEED_LOGIN') {
            auth.logout();
            // 切换到登录页面
            Taro.navigateTo({
              url: '/pages/auth/login'
            });
            return;
          }
        } else {
          Tips.toast(codeMessage[statusCode]);
        }
        reject(codeMessage[statusCode]);
      },
      fail: function(err) {
        reject(err)
      }
    })
  });
}
3. 后台拦截器,判断登录态
  • 不需要拦截的方法加注解过滤掉。
  • 需要拦截的方法判断header中token是否存在且解密后判断是否合法有效。
  • 因为小程序是自动登录,这种错误可视为异常情况,拦截后页面跳转到登录页,手动登录。
    微信截图_20200308175718.png
4. 授权拦截

一开始是想通过后台拦截器判断接口是否需要授权,实现授权拦截,但基于以下考虑,我将授权拦截前置到小程序侧。

  1. 这样会消耗网络资源,请求已发起并拦截,只能授权后再次请求,有碍体验。
  2. 因为是自动登录,整个应用运行期间都携带登录态,授权其实属于小程序端“锦上添花”的功能,所以放到后台去拦截实在是浪费资源
  3. 不好兼容多端

授权拦截前置,就需要在每个需要授权使用的功能处编写授权方法,因而会设计大量重复代码,不利于快速开发。
于是抽离一个鉴权组件,使用该组件包裹需要授权才能使用的按钮。

AuthView.js

代码仅供参考

class Index extends Component {

  state = {
    showLoginPanel: false,
  };

  /**
   * 登录
   */
  click() {
    // 检查是否已授权,如果已授权,直接执行回调方法
    auth.checkAuthed().then(this.props.onAgree).catch(() => {
      // 显示登录框
      this.setState({
        showLoginPanel: true,
      });
    });
  }

  /**
   * 授权登录
   * @param e
   */
  async bindGetUserInfo(e) {
    if (!!e.detail && !!e.detail.iv) {
      // 保存用户基本信息,并标记为“已授权”
      auth.authByWeixin(e.detail).then(() => {
          this.setState({
            showLoginPanel: false,
          });
          // 执行回调
          this.props.onAgree();
      }).catch((err) => {
          this.props.onDeny();
          Tips.toast('授权失败' + err);
          console.error(err);
      });
    } else {
      this.props.onDeny();
      Tips.toast('授权失败');
    }
  }

  cancel() {
    this.setState({
      showLoginPanel: false,
    });
  }

  render() {
    return (
      <Block>
        <View onClick={this.click}>{this.props.children}</View>
        {
          this.state.showLoginPanel && <View className='login-panel'>
            <View className='login-main'>
              <View className='login-main-subtitle'>请先授权再进行操作</View>
              <View className='footer'>
                <View className='footer-button cancel' onClick={this.cancel.bind(this)}>暂不登录</View>
                <Button className='btn-reset' openType='getUserInfo' onGetUserInfo={this.bindGetUserInfo}>
                  <View className='footer-button confirm'>授权登录</View>
                </Button>
              </View>
            </View>
          </View>
        }
      </Block>
    );
  }
}

demo.js

用AuthView包裹需要授权才能用的按钮,按钮不加onClick事件,将事件放到AuthView上,同意授权后即可执行操作。这样不会打断用户正在执行的操作,不用授权后再次点击按钮。

<AuthView onAgree={this.getQuestionSmartList}>
    <AtButton type='primary'>开始考试</AtButton>
</AuthView>

至此已完成小程序端对于授权及登录的封装。

dva登录授权login用户小程序taro

我来吐槽

*