微信小程序开发-登陆态维护

技术分享 Finley Fu 2019-12-01 3评论 358
小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系。

        在小程序开发过程中,如遇到需要用户登陆的场景,我们可以调用小程序的API获取数据,从而生成自己需要的登陆状态。

   登陆流程  

  • 登陆过程

        1. 调用 wx.login() 获取临时登录凭证code ,并回传到开发者服务器。

        2. 调用 auth.code2Session 接口,换取用户唯一标识 OpenID 和 会话密钥 session_key。

        3. 开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。

  • 注意

        1. 会话密钥 session_key 是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥。
        2. 临时登录凭证 code 只能使用一次

  • 时序图


   示例代码  

  • 小程序端代码
//login方法获取code,需要授权
wx.login({
  success: function (res) {
    //获取用户信息
    wx.getUserInfo({
      success: function (uinfo) {
        var params = {
          js_code: res.code,                  //调用小程序api所需code
          rawData: uinfo.rawData,             //不包括敏感信息的原始数据字符串,用于计算签名
          signature: uinfo.signature,         //使用 sha1( rawData + sessionkey ) 得到字符串,用于校验用户信息
          encryptedData: uinfo.encryptedData, //包括敏感数据在内的完整用户信息的加密数据
          iv: uinfo.iv,                       //加密算法的初始向量
        }
        //封装的调用后台api方法
        app.HttpService.getLogin(params)
          .then(res => {
            if (res.data.errcode == 0){
              //获取到session放入本地存储
              wx.setStorage({
                key: "session",
                data: res.data.session,
                success (res) {
                }
              })
            }else{
              //登陆失败
              wx.showToast({
                title: "登陆失败",
                icon: 'none',
                duration: 2000
              });
            }
          })
      }
    })
  },
  fail: function (res) {
    wx.showToast({
      title: "登陆失败",
      icon: 'none',
      duration: 2000
    });
  }
})
  • 后台代码(ruby)
def get_login
    js_code = params[:js_code]
    #调用api获取登陆信息
    client = RestClient::Request.execute(method: :get, url: "https://api.weixin.qq.com/sns/jscode2session?appid=#{WEI_APPID}&secret=#{WEI_SECRET}&js_code=#{js_code}&grant_type=authorization_code", timeout: 10)
    result = JSON.parse client.body
    if result["errcode"].to_i == 0
        #该用户openid,唯一
        openid = result["openid"]
        #临时登陆session_key
        session_key = result["session_key"]
        if !openid.blank? && !session_key.blank?
            #不包括敏感信息的原始数据字符串,用于计算签名
            raw_data = params[:rawData]   
            #使用 sha1( rawData + sessionkey ) 得到字符串,用于校验用户信息
            signature = params[:signature]
            #包括敏感数据在内的完整用户信息的加密数据
            de_encrypted_data = Base64.decode64(params[:encryptedData])
            #加密算法的初始向量
            de_iv = Base64.decode64(params[:iv]) 
            #验证签名
            if signature == Digest::SHA1.hexdigest(raw_data+session_key)
                aes_key = Base64.decode64(session_key)
                #开始解密
                decipher = OpenSSL::Cipher::AES.new(128, :CBC)
                decipher.decrypt
                decipher.key = aes_key
                decipher.iv = de_iv
                decipher.padding = 1
                rand_msg = decipher.update(de_encrypted_data) + decipher.final
                #解密后数据
                data = JSON.parse(rand_msg).deep_symbolize_keys
                Rails.logger.info data
                hash = {
                    wx_openid: openid,
                    name: data[:nickName],
                    head: data[:avatarUrl],
                    gender: data[:gender],
                }
                #创建或查找用户
                user = User.find_or_create_weixin(hash)
                #生成session,此为维护用户登录态最终session,此处使用的uuid
                session = UUIDTools::UUID.random_create.to_s
                #可以在redis中存储用户登陆信息,并根据自己需要设置过期时间
                RedisOperation.set_wx_session(session, openid, session_key)
            end
        end
    end
    if !user.blank?
        #给小程序端的信息
        userinfo = {}
        render json: {:errcode => 0, :session => session, :userinfo => userinfo}
    else
        render json: {:errcode => 500, :errmessage => "登陆失败"}
    end
end
   踩坑  

小程序官方文档可以用open-type='getUserInfo'直接发起授权并获得code和userinfo,于是尝试了一下

<button open-type='getUserInfo' bindgetuserinfo="bindgetuserinfo"></button>

点击后回调bindgetuserinfo方法

bindgetuserinfo: function (e) {
    var that = this
    //注意这里是先获取到了getUserInfo,再wx.login
    uinfo = e.detail.userInfo
    if (uinfo) {
        wx.login({
            success: function (res) {
                var params = {
                    js_code: res.code,
                    rawData: uinfo.rawData,
                    signature: uinfo.signature,
                    encryptedData: uinfo.encryptedData,
                    iv: uinfo.iv,
                }
                app.HttpService.getLogin(params)
                    .then(res => { if (res.data.errcode == 0){
                            wx.setStorage({
                                key: "session",
                                data: res.data.session,
                                success (res) {
                                    cb()
                                }
                            })
                        }else{
                            that.globalData.userInfo = null
                            wx.showToast({
                                title: "登陆失败",
                                icon: 'none',
                                duration: 2000
                            });
                        }
                    })
            },
            fail: function (res) {
                wx.showToast({
                    title: "登陆失败",
                    icon: 'none',
                    duration: 2000
                });
            }
        })
    }
}
        但是当后台解密数据时发现,有时候解密不成功,隐藏猜测可能是在点击按钮获取userInfo时,小程序官方会自动先生成一个code和session_key,而当我们再调用wx.login以及jscode2session时获取到的很有可能是刷新之后的session_key,那么再用这个session_key去解密肯定是不成功的。

        所以产生了我上面的写法,点击按钮后,e.detail.userInfo弃用,直接走正常流程调用wx.login然后再wx.getUserInfo,这样就每次都能成功解密数据。