小程序档案馆 - 登录授权
智能小程序运行在百度 App 之类的宿主上时,相比普通的 H5 应用有一项很大的优 势,就是可以调用宿主应用提供的各种 API 从而完成各种在 H5 实现比较困难的功 能,其中使用宿主的账号体系完成登录授权就是一项很重要的功能。
智能小程序在宿主应用中进行登录的方案是OAuth 2.0协议的变种,使用这种方案 进行登录的优势主要有以下几点:
某些小程序需要有一套用户体系来识别用户,进而提供对应的服务,但是往往 在用户注册这个环节上由于用户觉得麻烦而放弃使用,导致获取到的用户又流 失掉了。使用 OAuth 系的登录方式,智能小程序可以直接使用用户在百度 App 等宿主上的账号,用户登录了宿主的账号,就可以直接使用宿主账号在小 程序里登录。
普通用户要记住自己在不同智能小程序中的用户名、邮箱、密码是一个比较困 难的事情,许多用户会选择在大多数应用中使用相同的用户名、密码等信息, 而这又容易导致某个应用的用户数据泄露后入侵者可以拿这些数据去尝试登录 其它智能小程序,从而入侵其它原本安全的应用,这种即是常见的“撞库”攻击。 使用 OAuth 系的登录方式,一般应用将用户登录的安全校验托管给提供OAuth 服务的应用,可以避免被“撞库”的风险,提高应用的安全性。
使用宿主账号体系进行登录
使用宿主的账号体系进行登录的全流程可见小程序开发文档中的登录章节,这里对使用此章节的内容进行登录做更详细的介绍。
获取 OpenID 和 SessionKey
在小程序的关联登录机制中,有两个非常重要的概念:
OpenID 是宿主为每个用户在每个小程序中生成的用户标识,一个相同的 OpenID 可以始终对应同一个宿主账号的用户。
SessionKey 是宿主为每个用户在每个小程序中生成的数据密钥,数据密钥有 两个重要的作用:首先是通过宿主提供的接口获取到的数据会使用 SessionKey 加密,可以保证开发者服务端拿到的数据是真实有效的,其次是 SessionKey 会有更新机制,保证用户登录信息的安全。后面会在登录标识章节再详细讨论。
小程序获取用户的 OpenID 和 SessionKey 的流程图如下:
图中接口的详细参数可以参加开发者文档中的登录章节。
需要特别提到的一点是:swan.Login()不需要用户授权,也就是说在用户已登录 宿主账号的情况下整个流程是完全静默完成的,但是当用户在宿主应用上并未登录 的时候,调用此接口会弹出宿主应用的登录窗口,用户需要完成登录后再继续进行。 如果需要在用户未登录的时候做其它逻辑处理而不是要求用户登录,可以先通过 swan.IsLoginSync()判断用户的登录状态。
登录标识
当开发者获取到用户的 OpenID 和 SessionKey 之后,开发者应该生成一个唯一的 登录标识返回给小程序,并写到小程序的本地存储中,在后续的请求开发者的服务 端接口时通过参数带上这个唯一标识,供后端通过这个登录标识找到对应的 OpenID 和 SessionKey。
一种比较好的登录标识的设计范例是:Hash(OpenID + SessionKey + Salt),其中 Salt 是开发者自己定义的一个随机字符串,Hash 可以考虑使用 SHA1 或者 MD5 等碰 撞概率非常小的哈希函数。
一种非常不安全的方式是:直接使用 OpenID 或 SessionKey 的明文作为登录标识。 使用 OpenID 作为登录标识的风险在于:同一个账号的 OpenID 永远不会变,因此 若攻击者获取到某个用户的 OpenID,则可在参数中一直使用此 OpenID 来伪装成此 用户访问开发者的服务;使用 SessionKey 明文作为登录标识的风险在于,攻击者 不光可以直接使用此 SessionKey 解密用户数据,还可以使用它加密出假的用户数 据,然后通过开发者提供的服务端提口篡改用户的数据。
总结来说,登录标识需要满足:唯一性、有时效性,并不能带上敏感信息。
使用 SessionKey 处理用户数据
当获取到 SessionKey 和 OpenID 后,开发者就可以获取到有效的用户数据了。获 取用户数据主要有 swan.getUserInfo()和 swan.getPhoneNumber()等接口。下面
以 swan.getUserInfo()为例,讲解如何使用 SessionKey 安全有效地处理用户信息。
swan.getUserInfo()接口返回的内容可以分为明文和密文两部分,明文部分是为 了可以更快地显示在小程序的页面上,主要包括用户的头像、昵称等信息;密文部 分是 data 和 iv 两个字段。
以消息推送的场景为例,开发者可能需要在消息内容里以“亲爱的 XXX”为开头,此 时会用到用户的 OpenID 和真实的用户名。如果开发者提供一个接口,在通过 swan.getUserInfo()获取到用户信息后直接将用户信息以明文的方式发到开发者 自己的服务端某个接口中保存起来,就会出现一个安全问题:攻击者可以伪造用户 的名称等各种信息来访问开发者的服务器写入假的用户数据。
安全的做法是:开发者在前端获取到 swan.getUserInfo()后,将上一节中所说的 用户登录标识从 LocalStorage 中取出来,加上 data 和 iv,将这三个数据发到自己 的服务器,先通过登录标识将 SessionKey 取出来,然后用 SessionKey 和 iv 一起 使用 AES-192-CBC 算法对 data 进行解密(解密详细的过程及代码示例可参见用户数据的签名验证和加解密),通过判断解密是否成功,以及解密出的 OpenID 是否 与登录标识所关联的 OpenID 一致来判断数据的真实性,再保存到自己的数据库中。
特别说明一点,调用 swan.getUserInfo()这类接口会弹出授权提示框让用户进行 授权,若用户拒绝授权,再次调用此接口不会重复弹出授权提示框。开发者此时可 引导用户重新授权,并调用 swan.openSetting()让用户重新授权。
SessionKey 有效性判断
由上两节可知,开发者每次获取用户信息的时候,需要同时判断本地是否有用户标 识,以及 SessionKey 是否有效:若没有本地标识,则无法找到对应的 SessionKey; 而若 SessionKey 已经无效,说明开发者数据库中的用户 SessionKey 无法对宿主 所提供的 data 和 iv 进行解密。
判断 SessionKey 是否有效可以通过调用 swan.checkSession()来判断。如果为 false,则走上述获取 SessionKey 的流程重新让用户使用宿主账号在小程序内完成 登录;若为 true,则说明不用重新换取 SessionKey,可以直接获取用户信息。
需要注意的一点是,在提供多点登录的宿主应用(例如百度 App)中,有可能同一 个账号会在多台不同的设备上同时登录,此时有可能会出现此账号在多台设备上先 后都换取过 SessionKey,但是在此过程中 SessionKey 发生了变化,则变化前已换 取过 SessionKey 的设备上,本地存储中的登录标识已经失效(找不到对应的 SessionKey),而调用 swan.checkSession()仍然会得到 true,此时开发者应当在 登录标识已失效的设备上处理此类异常,重新换取 SessionKey 并生成新的登录标识。
未登录时的降级方案
由于宿主应用并不一定强制用户登录,因此用户也有可能处于未登录状态。此时开 发者可能不希望通过调用 swan.login()强制用户登录,而是希望直接使用用户的 设备标识来关联用户,存储一些非敏感的数据。因此智能小程序还提供一个 SwanID 的标识,可视作用户的设备标识。
SwanID 可以通过 swan.getSwanId()获取(接口文档)。SwanID 具有以下特征:
用户在同一台设备上使用同一个开发者所开发的不同智能小程序,得到的是相同的 SwanID。
用户在同一台设备上使用不同开发者所开发的不同智能小程序,得到的SwanID 是不同的。
灵活地使用 SwanID 和 OpenID 进行搭配,可以做到许多特殊的功能,例如:
在未登录的时候,将数据与用户的 SwanID 关联,当用户登录后,通过 SwanID 找到用户登录前的数据,然后与 OpenID 关联,比如常见的场景例如电商类小 程序对购物车数据的同步。
通过 SwanID 限定一个指定的 OpenID 只能在一台设备上使用某个小程序,即 当同一个 OpenID 先后出现了两个 SwanID 时,可以将前一个 SwanID 的登录态 踢出,比较常见的有 IM 类的小程序。
总结
开发者可使用 OpenID+SessionKey 的方式使用宿主的用户体系完成自己的业 务需求。
安全地处理 OpenID 和 SessionKey,不直接暴露到前端,而是基于它们生成用 户的登录标识,保证登录标识只在有效期内可用非常重要。
当用户拒绝授权获取用户信息时,再次调用 swan.getUserInfo()方法无法重 新弹框让用户同意,此时可通过 swan.openSetting()方法让用户重新授权。
当用户未登录时,可使用 SwanID 作为降级方案,处理一些不太敏感的业务。
智能小可爱 发布于2018-10-25 17:54