文档章节

从小程序的安全说起

Mr_Qi
 Mr_Qi
发布于 08/08 13:29
字数 2715
阅读 3429
收藏 56

背景

第一个问题 小程序中可以使用session么?

答案可能出乎大部分人意外

不可以!因为微信本身不是web方案,因此表现出来不会携带cookie 我们知道cookie和session的关系 Cookie,Session和Token的故事

分析

那么我们如何来判断用户是哪个用户呢???

小程序的官方实践

现状

首先我们来看看我们是怎么做的?

getWxUserInfo (options) {
    const url = `/weapp/code2session`;
    wx.login({
      success: res => {
        if (res.code) {
          //发起网络请求,用code换sessionkey
          requestUtil.wxRequestGet({
            url,
            data: {
              code: res.code,
              appid: config.appId,
            },
            success: result => {
              this.saveUserInfo(options, result.data.session_key);
            },
            fail: result => {
              wx.showToast({title: '小程序登录失败,请稍后再试。', icon: 'none'});
            },
          });
        } else {
          wx.showToast({title: '小程序登录失败,请稍后再试。', icon: 'none'});
        }
      },
      fail(res) {
        wx.showToast({title: '小程序登录失败,请稍后再试。', icon: 'none'});
      }
    });
  },
  saveUserInfo: (options, sessionKey) => {
    options = options || {};
    wx.getUserInfo({
      lang: "zh_CN",
      withCredentials: true,
      success: res => {
        requestUtil.wxRequestPostForm({
          url: '/weapp/login',
          data: {
            appid: config.appId,
            userInfo: JSON.stringify(res.userInfo),
            encryptedData: res.encryptedData,
            iv: res.iv,
            sessionKey,
          },
          success: result => {
            app.globalData.userInfo = result.data.user;
            app.globalData.sessionKey = result.data.sessionKey;
            if (options.success) {
              options.success(result);
            }
          },
          fail: result => {
            wx.showToast({title: '获取用户信息,请稍后再试。', icon: 'none'});
          },
        });
      },
      fail: res => {
        if (res.errMsg === 'getUserInfo:fail auth deny') {
          wx.showModal({
            title: '提示',
            content: '小程序需要获取您的公开信息,请在设置中打开。',
            confirmText:'去设置',
            success: res => {
              if (res.confirm) {
                wx.openSetting({
                  success: (res) => {
                  },
                });
              } else {
                wx.showToast({title:'获得授权失败, 无法获取用户信息', icon: 'none'});
              }
            }
          })
        }
      },
    })
  },
};
  1. wx.login接口返回code字段 该字段用来换取sessionkey
  2. 后端将sessionkey传递到了前端
  3. 调用wx.getUserInfo方法获取用户openid【传递sessionKey】
  4. 后端根据sessionKey对加密字段解密并存储

     

 

上述步骤其实很明显了 如果我是坏人 我不会把你的sessionkey传给你 我可以自定义一个sessionKey【用该sessionKey和我想要的数据伪造出一个假的用户】

这样就会产生一个带有openid的伪造的用户了

通俗的说sessionkey相当于钥匙 微信客户端回传加密信息 我们拼接该钥匙进行解密  我们目前的做法是把钥匙给了客户然后客户每次过来携带者钥匙和加密数据给我来解密【掩耳盗铃】

 

问题

再回到开始的问题 我们说小程序不支持cookie 那么自然也不支持session 那么我们要如何记住客户的状态呢???

回到cookie和session的起点【究竟啥时cookie 啥是session】 Cookie,Session和Token的故事

我们明白实质上cookie就是一个header 那么如果我们在我们的请求中将对应的header设置到请求上去 那么是不是就变相的可以使用session了呢???

答案是肯定的 因此聪明的小伙伴封装了一把

const getHeader = (options)=> {
  if (app.globalData.sessionKey) {
    options.cookie = `${config.sessionKey}=${app.globalData.sessionKey}`;
    return options;
  }
  return options;
};

那么问题来了 session就有有效期 这个session多长时间超时呢???【这个session是真的session么 session过期了应该怎么办呢?用户有办法取消session授权么?】

来看一下我们如何check用户

if (!app.globalData.userInfo) {
     user.getWxUserInfo({
       success: ()=> {
         this.fetchData();
       },
     });
   } else {
     user.getUserInfo({
       success: res => {
         wx.stopPullDownRefresh();
         this.setData({
           userInfo: res.data.user,
           showSelHistory: !!app.globalData.userInfo,
           remainCountText: `剩余 ${res.data.user.remainCount || 0 } 次/天`,
         });
         app.globalData.userInfo = res.data.user;
         this.setPhoneNo(this.data.userInfo.cellPhone);
       },
       fail: res => {
         wx.stopPullDownRefresh();
         wx.showToast({title: res.data.msg, icon: 'none'});
       },
     });
   }

当globalData中存在数据则直接将该数据设置到微信的page中===》如果客户退出微信重新登陆了怎么办???

在看后端如何获取sessionKey

@RequestMapping(value="/weapp/code2session")
@ResponseBody
public CarzoneJson weAppCode2Session(HttpServletRequest request,
                                String appid,
                                String code) throws Exception {
    String jsonStr = HttpUtil.get("", String.format(WE_APP_CODE_2_SESSION_URL, appid, WE_APP_SECRET, code));
    JSONObject jsonObject = new JSONObject(jsonStr);
    String sessionKey = "";
    try {
        sessionKey = jsonObject.getString("session_key");
    } catch (Exception e) {
        log.error("小程序登录失败" + jsonStr);
        return JsonMessage.getError();
    }
 
    CarzoneJson result = JsonMessage.getSuccess().set("session_key", sessionKey);
    return result;
}

这边需要注意一下和外部接口调用最好可以估计一下信息回放 这边json数据没有任何地方记录

请求参数

appid 小程序唯一标识
secret 小程序的 app secret
js_code 登录时获取的 code
grant_type 填写为 authorization_code

参数

必填

说明

在不满足 UnionID 下发条件的情况下,返回参数

openid 用户唯一标识
session_key 会话密钥

参数

说明

在满足 UnionID 下发条件的情况下,返回参数

openid 用户唯一标识
session_key 会话密钥
unionid 用户在开放平台的唯一标识符

参数

说明

很明显这边的openId是真实的openid 也是真正需要的openid【后续授权必须先经过这一步】

但是我们通过解析字段如何做的呢?

 @RequestMapping(value="/weapp/login")
    @ResponseBody
    public CarzoneJson weAppLogin(HttpServletRequest request, String appid,
                                  String userInfo, String encryptedData,
                                  String sessionKey, String iv) throws Exception{
        //保存或更新用户信息
        if (StringUtils.isNotEmpty(userInfo) && StringUtils.isNotEmpty(encryptedData)
                && StringUtils.isNotEmpty(sessionKey) && StringUtils.isNotEmpty(iv)) {
            JSONObject userInfoObj = new JSONObject(userInfo);
            com.alibaba.fastjson.JSONObject encryptedJson = WechatUtils.getUserInfo(sessionKey, encryptedData, iv);
            String openId = encryptedJson.getString("openId");
            String unionId = encryptedJson.getString("unionId");
            GetUserInfoResponse userInfoModel = new GetUserInfoResponse();
            userInfoModel.setNickname(userInfoObj.getString("nickName"));
            userInfoModel.setCity(userInfoObj.getString("city"));
            userInfoModel.setProvince(userInfoObj.getString("province"));
            userInfoModel.setCountry(userInfoObj.getString("country"));
            userInfoModel.setHeadimgurl(userInfoObj.getString("avatarUrl"));
            userInfoModel.setLanguage(userInfoObj.getString("language"));
            userInfoModel.setSex(userInfoObj.getInt("gender"));
            userInfoModel.setOpenid(openId);
            userInfoModel.setUnionid(unionId);
//         代表需要授权的f6用户,1代表需要授权的零公里用户,2代表需要授权的孚美用户,3代表不需要授权的其他用户
            userService.addOrUpdateWxUser(userInfoModel, appid, CommonConstants.GB_WX_USER_LEVEL_10, 0);
            CarzoneJson result = this.login(request, appid, openId, unionId);
            result.set("sessionKey", request.getSession().getId());
            return result;
        } else {
            throw new InvalidParameterException();
        }
    }

这边OpenId如果不是上述的openid说明是伪造的噢~【由于sessionkey的流失那么可以出现客户伪造】===》如果我们前面记录下了日志我们可以根据openid查找不在名单内的用户了~【最简单放到redis中可以check是否存在】

实践

按照微信的文档所描述 一段时间类用code换取的sessionKey实际上是不变的【可能达到30天 但是随着用户操作频率来说可能发生变化===》理解成我们的session 最长存在30天但是长时间不访问session将自动过期 也就是说获取wx用户的api取不到数据了】

因此这边存在问题 如何让微信授权过后的用户无缝访问我们的受保护api呢???

按照官方推荐方案使用自己平台的session【本质是还是验证我是谁】

每次请求过来都会带上用户的sessionid 这样我们根据sessionid解析出对应的session信息【由于session信息未暴露出去 自然提供了更好的安全性】

那么值钱提的问题 如何保证session的过期【比如用户切换了微信号】

微信提供了如下接口

会话密钥 session_key 有效性

开发者如果遇到因为 session_key 不正确而校验签名失败或解密失败,请关注下面几个与 session_key 有关的注意事项。

  1. wx.login() 调用时,用户的 session_key 会被更新而致使旧 session_key 失效。开发者应该在明确需要重新登录时才调用 wx.login(),及时通过登录凭证校验接口更新服务器存储的 session_key。

  2. 微信不会把 session_key 的有效期告知开发者。我们会根据用户使用小程序的行为对 session_key 进行续期。用户越频繁使用小程序,session_key 有效期越长。

  3. 开发者在 session_key 失效时,可以通过重新执行登录流程获取有效的 session_key。使用接口 wx.checkSession() 可以校验 session_key 是否有效,从而避免小程序反复执行登录流程。

  4. 当开发者在实现自定义登录态时,可以考虑以 session_key 有效期作为自身登录态有效期,也可以实现自定义的时效性策略。

wx.checkSession(OBJECT)

校验用户当前 session_key 是否有效。

OBJECT 参数说明:

success Function 接口调用成功的回调函数,session_key 未过期
fail Function 接口调用失败的回调函数,session_key 已过期
complete Function 接口调用结束的回调函数(调用成功、失败都会执行)

参数名

类型

必填

说明

示例代码:

wx.checkSession({
  success: function( ){
    //session_key 未过期,并且在本生命周期一直有效
  },
  fail: function( ){
    // session_key 已经失效,需要重新执行登录流程
    wx.login() //重新登录
    ....
  }
})

在我们的系统中由于globalData一直存在用户将不会每次触发login 因此建议在小程序初始打开时调用checkSession方法

再思考

为何我们需要用户手机号???

  1. 更好的安全性 用户必须受到验证码
  2. 更好的全平台统一打通【手机号可以唯一标致客户】
  3. 更好的融资【带客户手机号的更好卖吧 ^_^】
  4. 其他思考

那么针对来说

  1. 手机短信平台很常见 大概大家不知道人心险恶===》由于验证码被人刷的概率太多了
  2. 这个是必要的
  3. 这个也是必要的
  4. 但是由于引入手机号输入势必造成客户手动输入手机号~同时要防止恶意注册等等

    微信其实也提供了安全的手机号获取接口 【很不巧 我们也有权限 ^_^】

    getPhoneNumber(OBJECT)

    说明

    获取微信用户绑定的手机号,需先调用 login 接口。

    因为需要用户主动触发才能发起获取手机号接口,所以该功能不由 API 来调用,需用 <button> 组件的点击来触发。

    注意:目前该接口针对非个人开发者,且完成了认证的小程序开放。需谨慎使用,若用户举报较多或被发现在不必要场景下使用,微信有权永久回收该小程序的该接口权限。

    使用方法

    需要将 <button> 组件 open-type 的值设置为 getPhoneNumber,当用户点击并同意之后,可以通过 bindgetphonenumber 事件回调获取到微信服务器返回的加密数据, 然后在第三方服务端结合 session_key 以及 app_id 进行解密获取手机号。

    注意

    在回调中调用 wx.login 登录,可能会刷新登录态。此时服务器使用 code 换取的 sessionKey 不是加密时使用的 sessionKey,导致解密失败。建议开发者提前进行 login;或者在回调中先使用 checkSession 进行登录态检查,避免 login 刷新登录态。

    例子

    <button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber"> </button>
    
    Page({ 
        getPhoneNumber: function(e) { 
            console.log(e.detail.errMsg) 
            console.log(e.detail.iv) 
            console.log(e.detail.encryptedData) 
        } 
    })
    

    返回参数说明

    encryptedData String 包括敏感数据在内的完整用户信息的加密数据,详细见加密数据解密算法
    iv String 加密算法的初始向量,详细见加密数据解密算法

    参数

    类型

    说明

    encryptedData 解密后为以下 json 结构,详见加密数据解密算法

    {
        "phoneNumber": "13580006666",  
        "purePhoneNumber": "13580006666", 
        "countryCode": "86",
        "watermark":
        {
            "appid":"APPID",
            "timestamp":TIMESTAMP
        }
    }
    
    phoneNumber String 用户绑定的手机号(国外手机号会有区号)
    purePhoneNumber String 没有区号的手机号
    countryCode String 区号

    参数

    类型

    说明

    比如某牛

© 著作权归作者所有

共有 人打赏支持
Mr_Qi
粉丝 266
博文 328
码字总数 343520
作品 0
南京
程序员
加载中

评论(18)

深蓝苹果
深蓝苹果
别玩标题党
DragonFK
DragonFK
只能说你见识太少了
江湖小火_roy
江湖小火_roy
可以用wafer,比如node有wafer-session-node这样相当于有了session;其实你错了,这样的框架是加强的安全的;当然方便性是下降很多了。。。而且很多人的老框架要重写了。。。
kuafoo
kuafoo
session 和cookie 有什么关系? android 没有session,ios 没有session 难道都不安全了,就不能自己实现个session
竹隐江南
竹隐江南
我就想知道,这博文怎么上首页的?按照你这么说,JWT的方式的美国国防局都不安全了。
七月_
七月_

引用来自“七月_”的评论

session都没有的小程序,如果谈方便?
小程序:就没想让你方便,哈哈。
如何
七月_
七月_
session都没有的小程序,如果谈方便?
小程序:就没想让你方便,哈哈。
c
cweijan
storage,oauth了解一下?你这文章我看都不看一眼
原始数据
原始数据
哈哈,佩服你的勇气啊,兄弟。写了这么多,但是我一句没看。
w2907
w2907
说这种话,发出这个文章足以说明您的技术不行,可以看看auth2.0
《从零开始学Swift》学习笔记(Day 15)——请注意数字类型之间的转换

原创文章,欢迎转载。转载请注明:关东升的博客 在C、Objective-C和Java等其他语言中,整型之间有两种转换方法: 从小范围数到大范围数转换是自动的; 从大范围数到小范围数需要强制类型转换...

智捷课堂
2015/09/21
44
0
【云周刊】第170期:技术人生-看90后如何逆袭,从实习生成长为阿里云分布式NoSQL领域专家

本期头条 【技术人生】看90后如何逆袭,从实习生成长为阿里云分布式NoSQL领域专家 从5年前第一次进入阿里云实习到如今,我一直都在表格存储TableStore团队,参与分布式NoSQL的研发等工作。另...

场景研读
05/11
0
0
Swift数字类型之间的转换

Swift数字类型之间的转换Swift是一种安全的语言,对于类型的检查非常严格,不同类型之间不能随便转换。 一、整型之间的转换 在C和Objective-C等其他语言中,整型之间有两种转换方法: 从小范...

智捷课堂
2014/09/05
0
0
腾讯云+社区沙龙·小程序敏捷开发实战(北京)开启报名

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 作者:由云加社区技术沙龙发表在云+社区 2018伊始,最火爆的不是春晚和红包,而是从天而降的“跳一跳”。这款来自微信的轻量级小...

腾讯云+社区
03/22
0
0
DeepMind 资深研究员黄士杰发表临别感言,宣布正式离开 AlphaGo 项目

雷锋网 AI 科技评论按,北京时间 12 月 11 日晚间,Google 宣布推出围棋教学工具 AlphaGo Teach,而在 12 月 12 日晚间,仅仅一天之后,又迎来另一条引爆媒体圈的消息:DeepMind 资深研究员的...

思颖
2017/12/13
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

windbg调试C源码级驱动

联机方式不多说了。我博客里有,英文的。 windbg联机文档 https://docs.microsoft.com/zh-cn/windows-hardware/drivers/debugger/debug-universal-drivers---step-by-step-lab--echo-kernel......

simpower
26分钟前
0
0
redis快照和AOF简介

数据持久化到硬盘:一是快照(snapshotting),二是只追加文件(append-only file AOF) 快照 核心原理:redis某个时间内存内的所有数据写入硬盘 场景:redis快照内存里面的数据 1. 用户发送bgsav...

拐美人
26分钟前
0
0
这个七夕,送你一份程序员教科书级别的告白指南

给广大爱码士们的高能预警: 今天,就是七夕了…… (单身非作战人群请速速退场!) 时常有技术GG向个推君抱怨 经过网民多年的教育 以及技术人持之以恒的自黑 冲锋衣狂热分子·格子衫骨灰级粉...

个推
31分钟前
0
0
python爬虫日志(15)cookie详解

转载:原文地址 早期Web开发面临的最大问题之一是如何管理状态。服务器端没有办法知道两个请求是否来自于同一个浏览器。那时的办法是在请求的页面中插入一个token,并且在下一次请求中将这个...

茫羽行
32分钟前
0
0
qlv视频格式转换器

  腾讯视频中的视频影视资源有很多,小编经常在里面下载视频观看,应该也有很多朋友和小编一样吧,最近热播的电视剧也不少,如《香蜜沉沉烬如霜》、《夜天子》还有已经完结的《扶摇》,这么...

萤火的萤火
36分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部