跳到主要内容
版本:v3

内建账户指南

从 TapSDK 3.0 开始,我们提供了一个内建账户系统供游戏使用:开发者可以直接用 TapTap OAuth 授权的结果生成一个游戏内的账号(TDSUser),同时我们也支持将更多第三方认证登录的结果绑定到该账号上来。 TapSDK 提供的游戏内好友、成就等服务和功能,也都基于这一账户系统。

环境要求

  • Unity 2019.4 或更高版本
  • iOS 11 或更高版本,Xcode 版本 14.1 或更高版本
  • Android 5.0(API level 21)或更高版本

权限说明

集成前准备

  1. 参考 准备工作 创建应用、开启内建账户应用服务、绑定 API 域名;

SDK 获取

SDK 可以通过 Unity Package Manager 导入或手动导入,二者任选其一。请根据项目需要选择。

方法一:使用 Unity Package Manager

从 3.29.1 版本开始, SDK 修改 JSON 解析库为 Newtonsoft-json,如果当前工程已接入该依赖库,则不需额外处理,否则需在 Packages/manifest.json 添加如下依赖:

"com.unity.nuget.newtonsoft-json":"3.2.1"
NPMJS 安装

从 3.25.0 版本开始,TapSDK 支持了 NPMJS 安装,优势是只需要配置版本号,并且支持嵌套依赖。

在项目的 Packages/manifest.json 文件中添加以下依赖:

"dependencies":{
"com.taptap.tds.bootstrap":"3.29.4",
"com.taptap.tds.login":"3.29.4",
"com.taptap.tds.common":"3.29.4",
}

但需要注意的是,要在 Packages/manifest.jsondependencies 同级下声明 scopedRegistries

"scopedRegistries": [
{
"name": "NPMJS",
"url": "https://registry.npmjs.org/",
"scopes": ["com.tapsdk", "com.taptap", "com.leancloud"]
}
]

GitHub 安装

在项目的 Packages/manifest.json 文件中添加以下依赖:

"dependencies":{
"com.taptap.tds.login":"https://github.com/TapTap/TapLogin-Unity.git#3.29.4",
"com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#3.29.4",
"com.taptap.tds.bootstrap":"https://github.com/TapTap/TapBootstrap-Unity.git#3.29.4",
"com.leancloud.realtime":"https://github.com/leancloud/csharp-sdk-upm.git#realtime-2.3.0",
"com.leancloud.storage":"https://github.com/leancloud/csharp-sdk-upm.git#storage-2.3.0",
}

在 Unity 顶部菜单中选择 Window > Package Manager 可查看已经安装在项目中的包。

方法二:手动导入

  1. 下载页 找到 TapSDK Unity 下载地址,下载 TapSDK-UnityPackage.zip

  2. 在 Unity 项目中依次转到 Assets > Import Packages > Custom Packages,从解压后的 TapSDK-UnityPackage.zip 中,选择希望在游戏中使用的 TapSDK 包导入,其中:

    • TapTap_Bootstrap.unitypackage TapSDK 启动器,必选
    • TapTap_Common.unitypackage TapSDK 基础库,必选
    • TapTap_Login.unitypackage TapTap 登录库,必选
  3. 如果当前项目已集成 Newtonsoft.Json 依赖,则忽略该步骤,否则在 NuGet.org Newtonsoft.Json 页面中通过点击右侧 「Download package」 下载库文件,并将下载的文件后缀从.nupkg 修改为 .zip,同时解压该文件并复制内部的 Newtonsoft.Json.dll 文件拷贝到工程 AssetsPlugins 目录下,另外为了避免导出 IL2CPP 平台时删除必要数据,需在 Assets 目录下创建 link.xml 文件(如果已有该文件,则添加如下内容),其内容如下:

<linker>
<assembly fullname="System.Core">
<type fullname="System.Linq.Expressions.Interpreter.LightLambda" preserve="all" />
</assembly>
</linker>

iOS 配置

Assets/Plugins/iOS/Resource 目录下创建 TDS-Info.plist 文件,复制以下代码并且替换其中的 ClientId。如果游戏使用了 TapTap 内嵌动态数据分析服务,需要配置相关权限并替换授权文案

提示

复制使用以下内容时,请删除空行以及注释,以免出现 XML 解析时报错,ApplicationException: expected a key node

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>taptap</key>
<dict>
<key>client_id</key>
<string>ClientId</string>
</dict>
<!--使用内嵌动态服务,需要相册、相机、麦克风权限-->
<key>NSPhotoLibraryUsageDescription</key>
<string>说明为何应用需要此项权限</string>
<key>NSCameraUsageDescription</key>
<string>说明为何应用需要此项权限</string>
<key>NSMicrophoneUsageDescription</key>
<string>说明为何应用需要此项权限</string>
<!--使用数据分析服务需要 IDFA 权限。如应用程序不想弹框,可以设置 TapDB.AdvertiserIDCollectionEnabled(false)-->
<key>NSUserTrackingUsageDescription</key>
<string>说明为何应用需要此项权限</string>
</dict>
</plist>

SDK 初始化

初始化 TapSDK 时需传入 Client ID、区域等应用配置信息。

using TapTap.Bootstrap; // 命名空间
using TapTap.Common; // 命名空间

var config = new TapConfig.Builder()
.ClientID("your_client_id") // 必须,开发者中心对应 Client ID
.ClientToken("your_client_token") // 必须,开发者中心对应 Client Token
.ServerURL("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API
.RegionType(RegionType.CN) // 非必须,CN 表示中国大陆,IO 表示其他国家或地区
.ConfigBuilder();
TapBootstrap.Init(config);

初始化的时候,必须填入 client_idclient_tokenserver_url,其中:

  • client_idclient_token 信息可在 开发者中心 > 你的游戏 > 游戏服务 > 应用配置 查看。

  • server_url使用 HTTPS 协议,参考文档关于 域名 的说明。

TDSUserLCUser

TDSUser 类继承自 LCUser 类。 LCUser 是 LeanCloud 提供的账户系统,TDSUser 基本沿用了其功能和接口,并针对 TDS 的需求进行了细微调整,所以我们推荐大家使用 TDSUser 类来构建玩家账户系统。

TapTap 登录

直接调用一键登录方法即可,详见接入 TapTap 登录

游客登录

内建账户系统支持玩家在游戏中创建一个游客账号,其调用示例如下:

try{
// 通过 tdsUSer 给出用户唯一标识,如果有的话
var tdsUser = await TDSUser.LoginAnonymously();
}catch(Exception e){
// 登录失败
Debug.Log($"{e.code} : {e.message}");
}
信息

这里的「游客账户」可以保证玩家在同一个设备上多次登录都得到同一个账户,但是如果玩家卸载游戏重装之后再以「游客」身份登录则无法保证账户的唯一性。

当前用户

用户登录后,SDK 会自动将会话信息存储到客户端,这样用户在下次打开客户端时无需再次登录。下面的代码检查是否有已经登录的用户:

TDSUser currentUser = await TDSUser.GetCurrent();
if (currentUser != null) {
// 跳到首页
} else {
// 显示注册或登录页面
}

会话信息会长期有效,直到用户主动登出:

await TDSUser.Logout();

// currentUser 变为 null
TDSUser currentUser = await TDSUser.GetCurrent();

设置当前用户

用户登录后,云端会返回一个 session token 给客户端,它会由 SDK 缓存起来并用于日后同一 TDSUser 的鉴权请求。session token 会被包含在每个客户端发起的 HTTP 请求的 header 里面,这样云端就知道是哪个 TDSUser 发起的请求了。

以下是一些应用可能需要用到 session token 的场景:

  • 应用根据以前缓存的 session token 登录(可以通过 sessionToken 属性获取到当前用户的 session token;在服务端等受信任的环境下,可以通过 Master Key(即 Server Secret)读取任意用户的 sessionToken 字段以获取 session token)。
  • 应用内的某个 WebView 需要知道当前登录的用户。
  • 在服务端登录后,返回 session token 给客户端,客户端根据返回的 session token 登录。

下面的代码使用 session token 登录一个用户(云端会验证 session token 是否有效):

await TDSUser.BecomeWithSessionToken("anmlwi96s381m6ca7o7266pzf");

请避免在外部浏览器使用 URL 来传递 session token,以防范信息泄露风险。

如果在 开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 内建账户 > 设置 中勾选了 密码修改后,强制客户端重新登录,那么当一个用户修改密码后,该用户的 session token 会被重置。此时需要让用户重新登录,否则会遇到 403 (Forbidden) 错误。

下面的代码检查 session token 是否有效:

TDSUser currentUser = await TDSUser.GetCurrent();
bool isAuthenticated = await currentUser.IsAuthenticated();
if (isAuthenticated) {
// session token 有效
} else {
// session token 无效
}

设置其他用户属性

开发者可以使用内建账户系统设置 nicknameavatar,比如通过设置 nickname 字段来添加昵称:

var currentUser = await TDSUser.GetCurrent();  // 获取当前登录的账户实例
currentUser["nickname"] = "Tarara";
await currentUser.Save();

内建账户系统只支持内置字段和两个自定义字段: nickname(昵称) 以及 avatar(头像),添加其他新的字段会报错。

内建账户系统保存了用户的鉴权信息,也可能保存了邮箱、手机等敏感信息,因此会设置非常严格的权限,以防用户信息泄露。另外,在内建账户系统中保存过多数据,也容易导致慢查询等性能问题。所以,我们限制了自定义字段的使用,如需保存其他的用户信息,建议另外创建专门的 Class(比如 UserProfile)。

提示

建议开发者使用 nickname 字段存储昵称信息,TDS 游戏好友模块 根据昵称查找好友、好友邀请链接功能都使用了 nickname 字段

通过 TapTap OAuth 授权结果直接登录这一方式创建的玩家,SDK 会自动将 nickname 字段设置为 TapTap 账户的用户名。

用户的查询

TDSUserLCObject 的子类,所以 LCObject 支持的数据增删改查方式,TDSUser 也都可用。感兴趣的读者可以参考数据存储的开发文档了解更多信息。

不过,为了安全起见,内建账户系统(_User 表)默认关闭了 find 权限,这样每位用户登录后只能查询到自己在 _User 表中的数据,无法查询其他用户的数据。如果需要让其查询其他用户的数据,建议单独创建一张表来保存这类数据,并开放这张表的 find 查询权限。除此之外,还可以在云引擎里封装用户查询相关的方法。

可以参见 用户对象的安全 来了解 _User 表的一些限制,还可以阅读数据和安全来了解更多 class 级权限设置的方法。

关联用户对象

关联 TDSUser 的方法和 LCObject 是一样的。下面的代码为一名作者保存了一本书,然后获取所有该作者写的书:

LCObject book = new LCObject("Book");
TDSUser author = await LCUser.GetCurrent();
book["title"] = "我的第五本书";
book["author"] = author;
await book.Save();

LCQuery<LCObject> query = new LCQuery<LCObject>("Book");
query.WhereEqualTo("author", author);
// books 是包含同一作者所有 Book 对象的数组
ReadOnlyCollection<LCObject> books = await query.Find();

用户对象的安全

TDSUser 类自带安全保障,只有通过登录等经过鉴权的方法获取到的 TDSUser 才能进行保存或删除相关的操作,保证每个用户只能修改自己的数据。

这样设计是因为 TDSUser 中存储的大多数数据都比较敏感,包括手机号、社交网络账号等等。为了用户的隐私安全,即使是应用的开发者也应避免直接接触这些数据。

下面的代码展现了这种安全措施:

try {
TDSUser tdsUser = await TDSUser.LoginWithTapTap();
// 试图修改用户名
user["username"] = "Jerry";
// 可以执行,因为用户已鉴权
await user.Save();

// 绕过鉴权直接获取用户
LCQuery<TDSUser> userQuery = TDSUser.GetQuery();
TDSUser unauthenticatedUser = await userQuery.Get(user.ObjectId);
unauthenticatedUser["username"] = "Toodle";

// 会出错,因为用户未鉴权
unauthenticatedUser.Save();
} catch (LCException e) {
print($"{e.code} : {e.message}");
}

通过 TDSUser.GetCurrent() 获取的 LCUser 总是经过鉴权的。

要查看一个 TDSUser 是否经过鉴权,可以调用 isAuthenticated 方法。通过经过鉴权的方法获取到的 TDSUser 无需进行该检查。

其他对象的安全

对于给定的一个对象,可以指定哪些用户有权限读取或修改它。为实现该功能,每个对象都有一个由 ACL 对象组成的访问控制表。请参阅ACL 权限管理开发指南

第三方账户登录

我们在登录功能的开发指南中已经介绍了如何一键完成 TapTap 登录

其实除了 TapTap 外,我们也支持直接使用第三方社交平台(例如 Apple、Facebook 等)的账户信息来创建自己的账户体系并完成登录,也允许将既有账户与第三方账户绑定起来,这样终端用户后续可以直接用第三方账户信息来便捷登录。

事实上,TapSDK 采用了开放的接口设计,平台标识和唯一授权信息都由开发者指定,所以是可以兼容所有的第三方账户登录需求的。例如,海外开发者拿到 Facebook 授权信息之后,一样可以调用 TDSUser.loginWithAuthData 接口完成玩家账户的登录(平台名字可指定为 facebook)。

例如以下的代码展示了终端用户使用微信登录的处理流程:

Dictionary<string, object> thirdPartyData = new Dictionary<string, object> {
// 可选参数
{ "openid", "OPENID" },
{ "access_token", "ACCESS_TOKEN" },
{ "expires_in", 7200 },
{ "refresh_token", "REFRESH_TOKEN" },
{ "scope", "SCOPE" }
};
TDSUser currentUser = await TDSUser.LoginWithAuthData(thirdPartyData, "weixin");

loginWithAuthData 系列方法需要两个参数来唯一确定一个账户:

  • 第三方平台的名字,就是前例中的 weixin,该名字由应用层自己决定。
  • 第三方平台的授权信息,就是前例中的 thirdPartyData(一般包括 uidaccess_tokenexpires_in 等信息,与具体的第三方平台有关)。

云端会使用第三方平台的鉴权信息来查询是否已经存在与之关联的账户。如果存在的话,则返回 200 OK 状态码,同时附上用户的信息(包括 sessionToken)。如果第三方平台的信息没有和任何账户关联,客户端会收到 201 Created 状态码,意味着新账户被创建,同时附上用户的 objectIdcreatedAtsessionToken 和一个自动生成的 username,例如:

{
"username": "k9mjnl7zq9mjbc7expspsxlls",
"objectId": "5b029266fb4ffe005d6c7c2e",
"createdAt": "2018-05-21T09:33:26.406Z",
"updatedAt": "2018-05-21T09:33:26.575Z",
"sessionToken": "…",
// authData 通常不会返回,继续阅读以了解其中原因
"authData": {
"weixin": {
"openid": "OPENID",
"access_token": "ACCESS_TOKEN",
"expires_in": 7200,
"refresh_token": "REFRESH_TOKEN",
"scope": "SCOPE"
}
}
// …
}

这时候我们会看到 _User 表中出现了一条新的账户记录,账户中有一个名为 authData 的列,保存了第三方平台的授权信息。出于安全考虑,authData 不会被返回给客户端,除非它属于当前用户。

开发者需要自己完成第三方平台的鉴权流程(一般通过 OAuth 1.0 或 2.0),以获取鉴权信息,继而到云端来登录。

Sign in with Apple

如果你需要开发 Sign in with Apple,云服务可以帮你校验 identityToken,并获取 Apple 的 access_token。Apple Sign In 的 authData 结构如下:

{
"lc_apple": {
"uid": "从 Apple 获取到的 User Identifier",
"identity_token": "从 Apple 获取到的 identityToken",
"code": "从 Apple 获取到的 Authorization Code"
}
}

authData 中的 key 的作用:

  • lc_apple:只有 platform 为 lc_apple 时,云服务才会执行 identity_tokencode 的逻辑。
  • uid:必填。云服务通过 uid 判断是否存在用户。
  • identity_token:可选。authData 中有 identity_token 时云端会自动校验 identity_token 的有效性。开发者需要在 开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 内建账户 > 设置 > 第三方集成 中填写 Apple 的相关信息。
  • code:可选。authData 中有 code 时云端会自动用该 code 向 Apple 换取 access_tokenrefresh_token。开发者需要在 开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 内建账户 > 设置 > 第三方集成 中填写 Apple 的相关信息。

获取 Client ID

Client ID 用于校验 identity_token 及获取 access_token,指的是 Apple 应用的 identifier,也就是 AppIDserviceID。对于原生应用来说,指的是 Xcode 中的 Bundle Identifier,例如 com.mytest.app。详情请参考 Apple 的文档

获取 Private Key 及 Private Key ID

Private Key 用于获取 access_token。登录 Apple 开发者平台,在左侧的「Certificates, Identifiers & Profiles」中选择「Keys」,添加一个用于 Apple Sign In 的 Private Key,下载 .p8 文件,同时在下载 Key 的页面获得 Private Key ID。详情请参考 Apple 的文档

将 Key ID 填写到控制台,将下载下来的 Private Key 文件上传到控制台。控制台只能上传 Private Key 文件,无法查看及下载其内容。

获取 Team ID

Team ID 用于获取 access_token。登录 Apple 开发者平台,在右上角或 Membership 页面即可看到自己所属开发团队的 Team ID。注意选择 Bundle ID 对应的 Team。

使用 Apple Sign In 登录云服务

在控制台填写完成所有信息后,使用以下代码登录。

// 使用 Apple 登录时,uid、identity_token、code 都必传;
Dictionary<string, object> appleAuthData = new Dictionary<string, object> {
{ "uid", "USER IDENTIFIER" },
{ "identity_token", "IDENTITY TOKEN" },
{ "code", "AUTHORIZATION CODE" }
};
TDSUser currentUser = await TDSUser.LoginWithAuthData(appleAuthData, "lc_apple");

鉴权数据的保存

每个用户的 authData 是一个以平台名为键名,鉴权信息为键值的 JSON 对象。

理解 authData 的数据结构至关重要。一个终端用户通过如下的鉴权信息来登录的时候,

"platform": {
"openid": "OPENID",
"access_token": "ACCESS_TOKEN",
"expires_in": 7200,
"refresh_token": "REFRESH_TOKEN",
"scope": "SCOPE"
}

云端首先会查找账户系统,看看是否存在 authData.platform.openid 等于 OPENID 的账户,如果存在,则返回现有账户,如果不存在那么就创建一个新账户,同时将上面的鉴权信息写入新账户的 authData 属性中,并将新账户的数据当成结果返回。

云端会自动为每个用户的 authData.<PLATFORM>.<uid> 创建唯一索引,从而避免重复数据。 <uid>部分云服务内建支持的第三方平台上为 openid 字段,在其他第三方平台(包括部分云服务专门支持的第三方平台和所有云服务没有专门支持的第三方平台)上为 uid 字段。

自动验证第三方平台授权信息

为了确保账户数据的有效性,云端还支持对部分平台的 Access Token 的有效性进行自动验证,以防止伪造账户数据。如果有效性验证不通过,云端会返回 invalid authData 错误,关联不会被建立。对于云端无法识别的服务,开发者需要自己去验证 Access Token 的有效性。 比如,注册、登录时分别通过云引擎的 beforeSave hook、beforeUpdate hook 来验证 Access Token 有效性。

如果希望使用这一功能,则在开始使用前,需要在 开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 内建账户 > 设置 配置相应平台的 应用 ID应用 Secret Key

如果不希望云端自动验证 Access Token,可以在 开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 内建账户 > 设置 里面取消勾选 第三方登录时,验证用户 AccessToken 合法性

配置平台账号的目的在于创建 TDSUser 时,云端会使用相关信息去校验请求参数 thirdPartyData 的合法性,确保 TDSUser 实际对应着一个合法真实的用户,确保平台安全性。

绑定第三方账户

如果用户已经登录,也可以在当前账户上绑定或解绑更多第三方平台信息。例如,先用游客身份登录,再绑定 TapTap 或其他第三方账号,那么以后通过 TapTap 或其他第三方授权登录,都可以得到完全相同的账号。

绑定成功后,新的第三方账户信息会被添加到 TDSUserauthData 字段里。

例如,下面的代码可以关联微信账户:

await currentUser.AssociateAuthData(weixinData, "weixin");

为节省篇幅,上面的代码示例中没有给出具体的平台授权信息,相关内容请参考上面的「第三方账户登录」一节。

解除与第三方账户的关联

类似地,可以解绑第三方账户。

例如,下面的代码可以解除用户和微信账户的关联:

TDSUser currentUser = await TDSUser.GetCurrent();
await currentUser.DisassociateWithAuthData("weixin");