TDS Authentication Guide
Starting from TapSDK 3.0, there will be a built-in account system for you to use in your game. You can generate user accounts (TDSUser
) in your game with the results of TapTap OAuth. You can also link the authentication results of third-party platforms to this account.
The Friends and Achievements services provided by the TapSDK also depend on this account system.
Initialization
See TapSDK Quickstart for how to initialize the SDK.
TDSUser
and LCUser
TDSUser
is inherited from the LCUser
class.
LCUser
is the account system provided by LeanCloud, and TDSUser
basically inherited all the interfaces provided by LCUser
. TDSUser
includes some minor adjustments we made on functionalities and interfaces to fulfill the needs of TDS, so we recommend that you implement the account system in your game with TDSUser
.
TapTap Login
See Integrate TapTap Login for more details.
Guest Login
To create a guest account in the account system:
- Unity
- Android
- iOS
try{
// tdsUSer will hold a unique identifier of the user, if it exists
var tdsUser = await TDSUser.LoginAnonymously();
}catch(Exception e){
// Failed to log in
Debug.Log($"{e.code} : {e.message}");
}
TDSUser.logInAnonymously().subscribe(new Observer<TDSUser>() {
@Override
public void onSubscribe(Disposable disposable) {
}
@Override
public void onNext(TDSUser resultUser) {
// Successfully logged in and obtained an account instance
String userId = resultUser.getObjectId();
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onComplete() {
}
});
[TDSUser loginAnonymously:^(TDSUser * _Nullable user, NSError * _Nullable error) {
if (user) {
NSString *userId = user.objectId;
} else {
NSLog(@"%@", error);
}
}];
The guest account ensures that the player will have access to the same account on different logins. However, if the player deletes the game and then reinstalls the game, it is not guaranteed that the player will still have access to the same account.
Current User
Once the user has logged in, the SDK will automatically save the session to the client so that the user won’t need to log in again the next time they open the client. The code below checks if there is a logged-in user:
- Unity
- Android
- iOS
TDSUser currentUser = await TDSUser.GetCurrent();
if (currentUser != null) {
// Go to homepage
} else {
// Show the sign-up or the log-in page
}
TDSUser currentUser = TDSUser.getCurrentUser();
if (currentUser != null) {
// Go to homepage
} else {
// Show the sign-up or the log-in page
}
TDSUser *currentUser = [TDSUser currentUser];
if (currentUser != nil) {
// Go to homepage
} else {
// Show the sign-up or the log-in page
}
The session will remain valid until the user logs out:
- Unity
- Android
- iOS
await TDSUser.Logout();
// currentUser becomes null
TDSUser currentUser = await TDSUser.GetCurrent();
TDSUser.logOut();
// currentUser becomes null
TDSUser currentUser = TDSUser.getCurrentUser();
[TDSUser logOut];
// currentUser becomes nil
TDSUser *currentUser = [TDSUser currentUser];
Setting the Current User
A session token will be returned to the client after a user is logged in. It will be cached by our SDK and will be used for authenticating requests made by the same TDSUser
in the future. The session token will be included in the header of each HTTP request made by the client, which helps the cloud identify the TDSUser
sending the request.
Below are the situations when you may need to log a user in with their session token:
- A session token is already cached on the client which can be used to automatically log the user in (you can get the session token of the current user by accessing the
sessionToken
property; you can also get thesessionToken
of any user on the server side with your Master Key (also called Server Secret)). - A WebView within the app needs to know the current user.
- The user is logged in on the server side using your own authentication routines and the server is able to provide the session token to the client.
The code below logs a user in with a session token (the session token will be validated before proceeding):
- Unity
- Android
- iOS
await TDSUser.BecomeWithSessionToken("anmlwi96s381m6ca7o7266pzf");
TDSUser.becomeWithSessionTokenInBackground("anmlwi96s381m6ca7o7266pzf").subscribe(new Observer<TDSUser>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(TDSUser user) {
// Update currentUser
TDSUser.changeCurrentUser(user, true);
}
public void onError(Throwable throwable) {
// session token is invalid
}
public void onComplete() {}
});
[TDSUser becomeWithSessionTokenInBackground:@"anmlwi96s381m6ca7o7266pzf" block:^(TDSUser * _Nullable user, NSError * _Nullable error) {
if (user != nil) {
// Successfully logged in
} else {
// session token is invalid
}
}];
For security reasons, please avoid passing URLs containing session tokens in non-private environments. This increases the risk of your session tokens being captured by attackers.
If Log out the user when password is updated is enabled on Developer Center > Your game > Game Services > Cloud Services > TDS Authentication > Settings, the session token of a user will be reset in the cloud after this user changes the password and the client needs to prompt the user to log in again. Otherwise, 403 (Forbidden)
will be returned as an error.
The code below checks if a session token is valid:
- Unity
- Android
- iOS
TDSUser currentUser = await TDSUser.GetCurrent();
bool isAuthenticated = await currentUser.IsAuthenticated();
if (isAuthenticated) {
// session token is valid
} else {
// session token is invalid
}
boolean authenticated = TDSUser.getCurrentUser().isAuthenticated();
if (authenticated) {
// session token is valid
} else {
// session token is invalid
}
TDSUser *currentUser = [TDSUser currentUser];
NSString *token = currentUser.sessionToken;
[currentUser isAuthenticatedWithSessionToken:token callback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
// session token is valid
} else {
// session token is invalid
}
}];
Setting Other User Properties
The account system allows you to store nickname
and avatar
data associated with users. For example, you can store users’ nicknames by using nickname
field.
- Unity
- Android
- iOS
var currentUser = await TDSUser.GetCurrent(); // Get the instance of the current user
currentUser["nickname"] = "Tarara";
await currentUser.Save();
TDSUser currentUser = TDSUser.currentUser(); // Get the instance of the current user
currentUser.put("nickname", "Tarara");
currentUser.saveInBackground().subscribe(new Observer<LCObject>() {
@Override
public void onSubscribe(@NotNull Disposable d) {
}
@Override
public void onNext(@NotNull LCObject lcObject) {
// Saved; the properties of currentUser are updated
TDSUser tdsUser = (TDSUser) lcObject;
}
@Override
public void onError(@NotNull Throwable e) {
}
@Override
public void onComplete() {
}
});
TDSUser *currentUser = [TDSUser currentUser];
currentUser[@"nickname"] = @"Tarara";
[currentUser saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
// Saved
} else {
NSLog(@"%@", error);
}
}];
The account system supports only two extra fields besides the built-in ones: nickname
and avatar
. Adding other new fields will cause an error.
The account system contains users’ authentication data as well as emails and phone numbers, so there will be strict permission settings imposed on it to prevent the leak of the data.
Besides the security concerns, having too much data in the account system can also lead to performance issues like the occurrence of slow queries.
Therefore, we restrict the use of fields. If you want to store other user information, we suggest that you create a dedicated class (like UserProfile
) to store it.
We recommend that you store users’ nicknames with the nickname
field because TDS’s Friends module uses the data in this field when looking up friends with nicknames or generating invitation links.
If you log a user in with the result of TapTap OAuth, the SDK will automatically set the nickname
of the user to be the username of their TapTap account.
Queries on Users
TDSUser
is a subclass of LCObject
. This means that you can create, read, update, and delete user objects in the same way as you do with LCObject
s. See Data Storage Overview for more details.
For security reasons, the account system (the _User
table) has its find
permission disabled by default. Each user can only access their own data in the _User
table and cannot access that of others. If you need to allow each user to view other users’ data, we recommend that you create a new table to store such data and enable the find
permission of this table. You may also encapsulate queries on users within Cloud Engine and avoid opening up find
permissions of _User
tables.
See Security of User Objects for other restrictions applied to the _User
table and Data Security for more information regarding class-level permission settings.
Associations
Associations involving TDSUser
s work in the same way as that of LCObject
s. The code below saves a new book for an author and retrieves all the books written by that author:
- Unity
- Android
- iOS
LCObject book = new LCObject("Book");
TDSUser author = await LCUser.GetCurrent();
book["title"] = "My Fifth Book";
book["author"] = author;
await book.Save();
LCQuery<LCObject> query = new LCQuery<LCObject>("Book");
query.WhereEqualTo("author", author);
// books is an array of Book objects by the same author
ReadOnlyCollection<LCObject> books = await query.Find();
LCObject book = new LCObject("Book");
TDSUser author = TDSUser.getCurrentUser();
book.put("title", "My Fifth Book");
book.put("author", author);
book.saveInBackground().subscribe(new Observer<LCObject>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(LCObject book) {
// Find all the books by the same author
LCQuery<LCObject> query = new LCQuery<>("Book");
query.whereEqualTo("author", author);
query.findInBackground().subscribe(new Observer<List<LCObject>>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(List<LCObject> books) {
// books is an array of Book objects by the same author
}
public void onError(Throwable throwable) {}
public void onComplete() {}
});
}
public void onError(Throwable throwable) {}
public void onComplete() {}
});
LCObject *book = [LCObject objectWithClassName:@"Book"];
TDSUser *author = [TDSUser currentUser];
[book setObject:@"My Fifth Book" forKey:@"title"];
[book setObject:author forKey:@"author"];
[book saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
// Find all the books by the same author
LCQuery *query = [LCQuery queryWithClassName:@"Book"];
[query whereKey:@"author" equalTo:author];
[query findObjectsInBackgroundWithBlock:^(NSArray *books, NSError *error) {
// books is an array of Book objects by the same author
}];
}];
Security of User Objects
The TDSUser
class is secured by default. You are not able to invoke any save- or delete-related methods unless the TDSUser
was obtained using an authenticated method like logging in. This ensures that each user can only update their own data.
The reason behind this is that most data stored in TDSUser
can be very personal and sensitive, such as mobile phone numbers, social network account IDs, etc. Even the app’s owner should avoid tampering with these data for the sake of users’ privacy.
The code below illustrates this security policy:
- Unity
- Android
- iOS
try {
TDSUser tdsUser = await TDSUser.LoginWithTapTap();
// Attempt to change username
user["username"] = "Jerry";
// This will work since the user is authenticated
await user.Save();
// Get the user with a non-authenticated method
LCQuery<TDSUser> userQuery = TDSUser.GetQuery();
TDSUser unauthenticatedUser = await userQuery.Get(user.ObjectId);
unauthenticatedUser["username"] = "Toodle";
// This will cause an error since the user is unauthenticated
unauthenticatedUser.Save();
} catch (LCException e) {
print($"{e.code} : {e.message}");
}
TDSUser.loginWithTapTap(MainActivity.this, new Callback<TDSUser>() {
@Override
public void onSuccess(TDSUser resultUser) {
Toast.makeText(MainActivity.this, "Logged in with TapTap.", Toast.LENGTH_SHORT).show();
// This will work since the user is authenticated
resultUser.put("username", "Toodle");
// For demonstration only; please use the asynchronous method in your project to avoid blocking the thread
resultUser.save();
// Get the user with a non-authenticated method
LCQuery<TDSUser> query = new LCQuery<>("_User");
query.getInBackground(user.getObjectId()).subscribe(new Observer<TDSUser>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(TDSUser unauthenticatedUser) {
unauthenticatedUser.put("username", "Toodle");
// This will cause an error since the user is unauthenticated
unauthenticatedUser.save();
}
public void onError(Throwable throwable) {}
public void onComplete() {}
});
}
@Override
public void onFail(TapError error) {
Toast.makeText(MainActivity.this, error.getMessage(), Toast.LENGTH_SHORT).show();
}
}, "public_profile");
[TDSUser loginByTapTapWithPermissions:@[@"public_profile"] callback:^(TDSUser * _Nullable user, NSError * _Nullable error) {
if (user) {
// Attempt to change username
[user setObject:@"Jerry" forKey:@"username")];
// Save changes
[user saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (succeeded) {
// This will work since the user is authenticated
// Get the user with a non-authenticated method
LCQuery *query = [TDSUser query];
[query getObjectInBackgroundWithId:user.objectId block:^(LCObject *unauthenticatedUser, NSError *error) {
[unauthenticatedUser setObject:@"Toodle" forKey:@"username"];
[unauthenticatedUser saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (succeeded) {
// This will not succeed since the user is unauthenticated
} else {
// Failure is expected
}
}];
}];
} else {
// Error handling
}
}];
} else {
// Error handling
}
}];
The LCUser
obtained from TDSUser.GetCurrent()
will always be authenticated.
To check if a TDSUser
is authenticated, you can invoke the isAuthenticated
method. You do not need to check if TDSUser
is authenticated if it is obtained via an authenticated method.
Security of Other Objects
For each given object, you can specify which users are allowed to read it and which are allowed to modify it. To support this type of security, each object has an access control list implemented by an ACL
object. More details can be found in ACL Guide.
Third-Party Sign-on
We have already introduced how to implement quick log-in with TapTap.
Besides TapTap, you can also use other services (like Apple, WeChat, and QQ) to implement your account system. You can also associate existing accounts with these services so that the users can quickly log in with their existing accounts on these services.
Technically, we have implemented our interfaces in an open-ended manner. You can specify your own platform identifiers and authorization data, which means that our account system supports whatever third-party services you wish to connect to. For example, once you get the authorization data from Facebook, you can use TDSUser.loginWithAuthData
to log the user in (you may set the platform name to be facebook
).
The code below shows how you can log a user in with WeChat:
- Unity
- Android
- iOS
Dictionary<string, object> thirdPartyData = new Dictionary<string, object> {
// Optional
{ "openid", "OPENID" },
{ "access_token", "ACCESS_TOKEN" },
{ "expires_in", 7200 },
{ "refresh_token", "REFRESH_TOKEN" },
{ "scope", "SCOPE" }
};
TDSUser currentUser = await TDSUser.LoginWithAuthData(thirdPartyData, "weixin");
Map<String, Object> thirdPartyData = new HashMap<String, Object>();
// Optional
thirdPartyData.put("expires_in", 7200);
thirdPartyData.put("openid", "OPENID");
thirdPartyData.put("access_token", "ACCESS_TOKEN");
thirdPartyData.put("refresh_token", "REFRESH_TOKEN");
thirdPartyData.put("scope", "SCOPE");
TDSUser.loginWithAuthData(TDSUser.class, thirdPartyData, "weixin").subscribe(new Observer<TDSUser>() {
public void onSubscribe(Disposable disposable) {
}
public void onNext(TDSUser user) {
System.out.println("Logged in.");
}
public void onError(Throwable throwable) {
System.out.println("An error occurred.");
}
public void onComplete() {
}
});
NSDictionary *thirdPartyData = @{
// Optional
@"openid":@"OPENID",
@"access_token":@"ACCESS_TOKEN",
@"expires_in":@7200,
@"refresh_token":@"REFRESH_TOKEN",
@"scope":@"SCOPE",
};
TDSUser *user = [TDSUser user];
LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new];
option.platform = LeanCloudSocialPlatformWeiXin;
[user loginWithAuthData:thirdPartyData platformId:LeanCloudSocialPlatformWeiXin options:option callback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
NSLog(@"Logged in.");
} else {
NSLog(@"An error occurred: %@",error.localizedFailureReason);
}
}];
loginWithAuthData
requires two arguments to locate a unique account:
- The name of the third-party platform, which is
weixin
in the example above. You can decide this name on your own. - The authorization data from the third-party platform, which is the
thirdPartyData
in the example above (depending on the platform, it usually includesuid
,access_token
, andexpires_in
).
The cloud will then verifies that the provided authData
is valid and checks if a user is already associated with it. If so, it returns the status code 200 OK
along with the details (including a sessionToken
of the user). If the authData
is not linked to any accounts, you will instead receive the status code 201 Created
, indicating that a new user has been created. The body of the response contains objectId
, createdAt
, sessionToken
, and an automatically-generated unique username
. For example:
{
"username": "k9mjnl7zq9mjbc7expspsxlls",
"objectId": "5b029266fb4ffe005d6c7c2e",
"createdAt": "2018-05-21T09:33:26.406Z",
"updatedAt": "2018-05-21T09:33:26.575Z",
"sessionToken": "…",
// authData won't be returned in most cases; see explanations below
"authData": {
"weixin": {
"openid": "OPENID",
"access_token": "ACCESS_TOKEN",
"expires_in": 7200,
"refresh_token": "REFRESH_TOKEN",
"scope": "SCOPE"
}
}
// …
}
Now we will see a new record showing up in the _User
table that has an authData
field. Within this field is the authorization data from the third-party platform. For security reasons, the authData
field won’t be returned to the client unless the current user owns it.
You will need to implement the authentication process involving the third-party platform yourself (usually with OAuth 1.0 or 2.0) to obtain the authentication data, which will be used to log a user in.
Sign in with Apple
If you plan to implement Sign in with Apple, the cloud can help you verify identityToken
s and obtain access_token
s from Apple. Below is the structure of authData
for Sign in with Apple:
{
"lc_apple": {
"uid": "The User Identifier obtained from Apple",
"identity_token": "The identityToken obtained from Apple",
"code": "The Authorization Code obtained from Apple"
}
}
Each authData
has the following fields:
lc_apple
: The cloud will run the logic related toidentity_token
andcode
only when the platform name islc_apple
.uid
: Required. The cloud tells if the user exists withuid
.identity_token
: Optional. The cloud will automatically validateidentity_token
if this field exists. Please make sure you have provided relevant information on Developer Center > Your game > Game Services > Cloud Services > TDS Authentication > Settings > Third-party accounts.code
: Optional. The cloud will automatically obtainaccess_token
andrefresh_token
from Apple if this field exists. Please make sure you have provided relevant information on Developer Center > Your game > Game Services > Cloud Services > TDS Authentication > Settings > Third-party accounts.
Getting Client ID
Client ID is used to verify identity_token
and to obtain access_token
. It is the identifier of an Apple app (AppID
or serviceID
). For native apps, it is the Bundle Identifier in Xcode, which looks like com.mytest.app
. See Apple’s docs for more details.
Getting Private Key and Private Key ID
Private Key is used to obtain access_token
. You can go to Apple Developer, select “Keys” from “Certificates, Identifiers & Profiles”, add a Private Key for Sign in with Apple, and then download the .p8
file. You will also obtain the Private Key ID from the page you download the key. See Apple’s docs for more details.
The last step is to fill in the Key ID on the Developer Center and upload the downloaded Private Key. You can only upload Private Keys, but cannot view or download them.
Getting Team ID
Team ID is used to obtain access_token
. You can view your team’s Team ID by going to Apple Developer and looking at the top-right corner or the Membership page. Make sure to select the team matching the selected Bundle ID.
Logging in to Cloud Services With Sign in with Apple
After you have filled in all the information on the Developer Center, you can log a user in with the following code:
- Unity
- Android
- iOS
Dictionary<string, object> appleAuthData = new Dictionary<string, object> {
// Required
{ "uid", "USER IDENTIFIER" },
// Optional
{ "identity_token", "IDENTITY TOKEN" },
{ "code", "AUTHORIZATION CODE" }
};
TDSUser currentUser = await TDSUser.LoginWithAuthData(appleAuthData, "lc_apple");
// Not supported yet
NSDictionary *appleAuthData = @{
// Required
@"uid":@"USER IDENTIFIER",
// Optional
@"identity_token":@"IDENTITY TOKEN",
@"code":@"AUTHORIZATION CODE",
};
TDSUser *user = [TDSUser user];
[user loginWithAuthData:appleAuthData platformId:"lc_apple" options:nil callback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
NSLog(@"Logged in.");
}else{
NSLog(@"Failed to log in: %@",error.localizedFailureReason);
}
}];
Storing Authentication Data
The authData
of each user is a JSON object with platform names as keys and authentication data as values.
A user associated with a WeChat account will have the following object as its authData
:
{
"weixin": {
"openid": "…",
"access_token": "…",
"expires_in": 7200,
"refresh_token": "…",
"scope": "…"
}
}
A user associated with a Weibo account will have the following object as its authData
:
{
"weibo": {
"refresh_token": "2.0xxx",
"uid": "271XFEFEW273",
"expires_in": 115057,
"access_token": "2.00xxx"
}
}
A user can be associated with multiple third-party platforms. If a user is associated with both WeChat and Weibo, their authData
may look like this:
{
"weixin": {
"openid": "…",
"access_token": "…",
"expires_in": 7200,
"refresh_token": "…",
"scope": "…"
},
"weibo": {
"refresh_token": "2.0xxx",
"uid": "271XFEFEW273",
"expires_in": 115057,
"access_token": "2.00xxx"
}
}
It’s important to understand the data structure of authData
. When a user logs in with the following authentication data:
"platform": {
"openid": "OPENID",
"access_token": "ACCESS_TOKEN",
"expires_in": 7200,
"refresh_token": "REFRESH_TOKEN",
"scope": "SCOPE"
}
The cloud will first look at the account system to see if there is an account that has its authData.platform.openid
to be the OPENID
. If there is, return the existing account. If not, create a new account and write the authentication data into the authData
field of this new account, and then return the new account’s data as the result.
The cloud will automatically create a unique index for the authData.<PLATFORM>.<uid>
of each user, which prevents the formation of duplicate data.
For some of the platforms specially supported by us, <uid>
refers to the openid
field. For others (the other platforms specially supported by us, and those not specially supported by us), it refers to the uid
field.
Automatically Validating Third-Party Authorization Data
The cloud can automatically validate access tokens for certain platforms, which prevents counterfeit account data from entering your app’s account system. If the validation fails, the cloud will return the invalid authData
error, and the association will not be created. For those services that are not recognized by the cloud, you need to validate the access tokens yourself.
You can validate access tokens when a user signs up or logs in by using LeanEngine’s beforeSave
hook and beforeUpdate
hook.
To enable the feature, please configure the platforms’ App IDs and Secret Keys on Developer Center > Your game > Game Services > Cloud Services > TDS Authentication > Settings.
To disable the feature, please uncheck Validate access tokens when logging in with third-party accounts on Developer Center > Your game > Game Services > Cloud Services > TDS Authentication > Settings.
The reason for configuring the platforms is that when a TDSUser
is created, the cloud will use the relevant data to validate the thirdPartyData
to ensure that the TDSUser
matches a real user, which ensures the security of your app.
Linking Third-Party Accounts
If a user is already logged in, you can link third-party accounts to this user. For example, if a user first logs in as a guest and then links their TapTap or other third-party accounts, the user will be able to access the same account when they log in with the same TapTap or third-party accounts in the future.
After a user links their third-party account, the account information will be added to the authData
field of the corresponding TDSUser
.
The following code links a WeChat account to a user:
- Unity
- Android
- iOS
await currentUser.AssociateAuthData(weixinData, "weixin");
user.associateWithAuthData(weixinData, "weixin").subscribe(new Observer<TDSUser>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(TDSUser user) {
System.out.println("Account linked.");
}
@Override
public void onError(Throwable e) {
System.out.println("Failed to link the account: " + e.getMessage());
}
@Override
public void onComplete() {
}
});
[user associateWithAuthData:weixinData platformId:LeanCloudSocialPlatformWeiXin options:nil callback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
NSLog(@"Account linked.");
} else{
NSLog(@"Failed to link the account: %@",error.localizedFailureReason);
}
}];
The code above omitted the authorization data of the platform. See Third-Party Sign-on for more details.
Unlinking
Similarly, a third-party account can be unlinked.
For example, the code below unlinks a user’s WeChat account:
- Unity
- Android
- iOS
TDSUser currentUser = await TDSUser.GetCurrent();
await currentUser.DisassociateWithAuthData("weixin");
TDSUser user = TDSUser.currentUser();
user.dissociateWithAuthData("weixin").subscribe(new Observer<TDSUser>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(TDSUser user) {
System.out.println("Unlinked.");
}
@Override
public void onError(Throwable e) {
System.out.println("Failed to unlink: " + e.getMessage());
}
@Override
public void onComplete() {
}
});
[user disassociateWithPlatform:LeanCloudSocialPlatformWeiXin callback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
NSLog(@"Unlinked.");
} else{
NSLog(@"Failed to unlink: %@",error.localizedFailureReason);
}
}];
扩展:接入 UnionID 体系,打通不同子产品的账号系统
随着第三方平台的账户体系变得日渐复杂,它们的第三方鉴权信息出现了一些较大的变化。下面我们以最典型的微信开放平台为例来进行说明。
当一个用户在移动应用内登录微信账号时,会被分配一个 OpenID;在微信小程序内登录账号时,又会被分配另一个不同的 OpenID。这样的架构会导致的问题是,使用同一个微信号的用户,也无法在微信开放平台下的移动应用和小程序之间互通。
微信官方为了解决这个问题,引入了 UnionID
的体系,以下为其官方说明:
通过获取用户基本信息接口,开发者可通过 OpenID 来获取用户基本信息,而如果开发者拥有多个公众号,可使用以下办法通过 UnionID 机制来在多公众号之间进行用户帐号互通。只要是同一个微信开放平台帐号下的公众号,用户的 UnionID 是唯一的。换句话说,同一用户,对同一个微信开放平台帐号下的不同应用,UnionID 是相同的。
其他平台,如 QQ 和微博,与微信的设计也基本一致。
云服务支持 UnionID
体系。你只需要给 loginWithauthData
和 associateWithauthData
接口传入更多的第三方鉴权信息,即可完成新 UnionID 体系的集成。新增加的第三方鉴权登录选项包括:
- unionId,指第三方平台返回的 UnionId 字符串。
- unionId platform,指 unionId 对应的 platform 字符串,由应用层自己指定,后面会详述。
- asMainAccount,指示是否把当前平台的鉴权信息作为主账号来使用。如果作为主账号,那么就由当前用户唯一占有该 unionId,以后其他平台使用同样的 unionId 登录的话,会绑定到当前的用户记录上来;否则,当前应用的鉴权信息会被绑定到其他账号上去。
下面让我们通过一个例子来说明如何使用这些参数完成 UnionID 登录。
假设云服务在微信开放平台上有两个应用,一个是「云服务通讯」,一个是「云服务技术支持」,这两个应用在接入第三方鉴权的时候,分别使用了 wxleanoffice
和 wxleansupport
作为 platform 来进行登录。现在我们开启 UnionID 的用户体系,希望同一个微信用户在这两个应用中都能对应到同一个账户系统(_User
表中的同一条记录),同时我们决定将 wxleanoffice
平台作为主账号平台。
假设对于用户 A,微信给 ta 为云服务分配的 UnionId 为 unionid4a
,而对两个应用的授权信息分别为:
"wxleanoffice": {
"access_token": "officetoken",
"openid": "officeopenid",
"expires_in": 1384686496
},
"wxleansupport": {
"openid": "supportopenid",
"access_token": "supporttoken",
"expires_in": 1384686496
}
现在,用户 A 在「云服务通讯」中通过微信登录,其调用请求为:
- Unity
- Android
- iOS
Dictionary<string, object> thirdPartyData = new Dictionary<string, object> {
// 必须
{ "uid", "officeopenid" },
{ "access_token", "officetoken" },
{ "expires_in", 1384686496 },
{ "unionId", "unionid4a" }, // 新增属性
// 可选
{ "refresh_token", "..." },
{ "scope", "SCOPE" }
};
LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption();
option.AsMainAccount = true;
option.UnionIdPlatform = "weixin";
TDSUser currentUser = await TDSUser.LoginWithAuthDataAndUnionId(
thirdPartyData, "wxleanoffice", "unionid4a",
option: option);
Map<String, Object> thirdPartyData = new HashMap<String, Object>();
thirdPartyData.put("expires_in", 1384686496);
thirdPartyData.put("uid", "officeopenid");
thirdPartyData.put("access_token", "officetoken");
thirdPartyData.put("scope", "SCOPE");
TDSUser.loginWithAuthData(TDSUser.class, thirdPartyData, "wxleanoffice",
"unionid4a", "weixin", true) // 新增参数,分别表示 uniondId,unionIdPlatform,asMainAccount
// 对于 unionIdPlatform,这里使用「weixin」来指代微信平台。
.subscribe(new Observer<TDSUser>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(TDSUser user) {
System.out.println("登录成功");
}
@Override
public void onError(Throwable e) {
System.out.println("登录失败:" + e.getMessage());
}
@Override
public void onComplete() {
}
});
NSDictionary *thirdPartyData = @{
@"access_token":@"officetoken",
@"expires_in":@1384686496,
@"uid":@"officeopenid",
@"scope":@"SCOPE",
@"unionid":@"unionid4a" // 新增属性
};
TDSUser *currentuser = [TDSUser user];
LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new];
option.platform = @"weixin"; // 这里指定 UnionIdPlatform,使用「weixin」来指代微信平台。
option.unionId = thirdPartyData[@"unionid"];
option.isMainAccount = true;
[currentuser loginWithAuthData:thirdPartyData platformId:@"wxleanoffice" options:option callback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
NSLog(@"登录成功");
}else{
NSLog(@"登录失败:%@",error.localizedFailureReason);
}
}];
注意代码中将微信传回来的 openid 属性改为了 uid,这是因为云端要求对于自定义的 platform,只能使用 uid 这样的属性名,才能保证自动建立 authData.<PLATFORM>.uid
的唯一索引,具体可以参考数据存储 REST API 使用详解的《连接用户账户和第三方平台》一节。
如果用户 A 是第一次在「云服务通讯」中通过微信登录,那么 _User
表中会增加一个新用户(假设其 objectId
为 ThisIsUserA
),其 authData
的结果如下:
{
"wxleanoffice": {
"platform": "weixin",
"uid": "officeopenid",
"expires_in": 1384686496,
"main_account": true,
"access_token": "officetoken",
"unionid": "unionid4a"
},
// 新增键值对
"_weixin_unionid": {
"uid": "unionid4a"
}
}
可以看到,与之前的第三方登录 API 相比,这里由于登录时指定了 asMainAccount
为 true
,所以 authData
的第一级子目录中增加了 _weixin_unionid
的键值对,这里的 weixin
就是我们指定的 unionIdPlatform
的值。_weixin_unionid
这个增加的键值对非常重要,以后我们判断是否存在同样 UnionID 的账户就是依靠它来查找的,而是否增加这个键值对,则是由登录时指定的 asMainAccount
的值决定的:
- 当
asMainAccount
为true
时,云端会在authData
下面增加名为_{unionIdPlatform}_unionid
的键值对,当前账号就会作为这一个 UnionID 对应的主账号被唯一确定。 - 当
asMainAccount
为false
时,云端不会在authData
下面增加名为_{unionIdPlatform}_unionid
的键值对,此时如果通过提供的 UnionID 可以找到主账号,则会将当前的鉴权信息合并进主账号的authData
属性里,同时返回主账号对应的_User
表记录;如果通过提供的 UnionID 找不到主账号,则会根据平台的openid
去查找账户,找到匹配的账户就返回匹配的,找不到就新建一个账户,此时的处理逻辑与不使用 UnionID 时的逻辑完全一致。
接下来,用户 A 继续在「云服务技术支持」中进行微信登录,其登录逻辑为:
- Unity
- Android
- iOS
Dictionary<string, object> thirdPartyData = new Dictionary<string, object> {
// 必须
{ "uid", "supportopenid" },
{ "access_token", "supporttoken" },
{ "expires_in", 1384686496 },
{ "unionId", "unionid4a" },
// 可选
{ "refresh_token", "..." },
{ "scope", "SCOPE" }
};
LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption();
option.AsMainAccount = false;
option.UnionIdPlatform = "weixin"; // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。
TDSUser currentUser = await TDSUser.LoginWithAuthDataAndUnionId(
thirdPartyData, "wxleansupport", "unionid4a",
option: option);
Map<String, Object> thirdPartyData = new HashMap<String, Object>();
thirdPartyData.put("expires_in", 1384686496);
thirdPartyData.put("uid", "supportopenid");
thirdPartyData.put("access_token", "supporttoken");
thirdPartyData.put("scope", "SCOPE");
TDSUser.loginWithAuthData(TDSUser.class, thirdPartyData, "wxleansupport", "unionid4a",
"weixin", // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。
false).subscribe(new Observer<TDSUser>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(TDSUser user) {
System.out.println("登录成功");
}
@Override
public void onError(Throwable e) {
System.out.println("登录失败:" + e.getMessage());
}
@Override
public void onComplete() {
}
});
NSDictionary *thirdPartyData = @{
@"access_token":@"supporttoken",
@"expires_in":@1384686496,
@"uid":@"supportopenid",
@"scope":@"SCOPE",
@"unionid":@"unionid4a"
};
TDSUser *currentuser = [TDSUser user];
LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new];
option.platform = @"weixin"; // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。
option.unionId = thirdPartyData[@"unionid"];
option.isMainAccount = false;
[currentuser loginWithAuthData:thirdPartyData platformId:@"wxleansupport" options:option callback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
NSLog(@"登录成功");
}else{
NSLog(@"登录失败:%@",error.localizedFailureReason);
}
}];
与「云服务通讯」中的登录过程相比,在「云服务技术支持」这个应用中,我们在登录时只是将 asMainAccount
设为了 false
。 这时我们看到,本次登录得到的还是 objectId
为 ThisIsUserA
的 _User
表记录(同一个账户),同时该账户的 authData
属性中发生了变化,多了 wxleansupport
的数据,如下:
{
"wxleanoffice": {
"platform": "weixin",
"uid": "officeopenid",
"expires_in": 1384686496,
"main_account": true,
"access_token": "officetoken",
"unionid": "unionid4a"
},
"_weixin_unionid": {
"uid": "unionid4a"
},
"wxleansupport": {
"platform": "weixin",
"uid": "supportopenid",
"expires_in": 1384686496,
"main_account": false,
"access_token": "supporttoken",
"unionid": "unionid4a"
}
}
在新的登录方式中,当一个用户以「平台名为 wxleanoffice
、uid 为 officeopenid
、UnionID 为 unionid4a
」的第三方鉴权信息登录得到新的 TDSUser
后,接下来这个用户以「平台名为 wxleansupport
、uid 为 supportopenid
、UnionID 为 unionid4a
」的第三方鉴权信息登录时,云端判定是同样的 UnionID,就直接把来自 wxleansupport
的新用户数据加入到已有账户的 authData
里了,不会再创建新的账户。
这样一来,云端通过识别平台性的用户唯一标识 UnionID,让来自同一个 UnionID 体系内的应用程序、小程序等不同平台的用户都绑定到了一个 TDSUser
上,实现互通。
为 UnionID 建立索引
云端会为 UnionID 自动建立索引,不过因为自动创建基于请求的抽样统计,可能会滞后。
因此,我们推荐自行创建相关索引,特别是用户量(_User
表记录数)很大的应用,更需要预先创建索引,否则用户使用 UnionID 账号登录时可能超时失败。
以上面的微信 UnionID 为例,建议在控制台预先创建下列唯一索引(允许缺失值):
authData.wxleanoffice.uid
authData.wxleansupport.uid
authData._weixin_unionid.uid
该如何指定 unionIdPlatform
从上面的例子可以看出,使用 UnionID 登录的时候,需要指定 unionIdPlatform
的主要目的,就是为了便于查找已经存在的唯一主账号。云端会在主账号对应账户的 authData
属性中增加一个 _{unionIdPlatform}_unionid
键值对来标记唯一性,终端用户在其他应用中登录的时候,云端会根据参数中提供的 uniondId
+ unionIdPlatform
的组合,在 _User
表中进行查找,这样来确定唯一的既存主账号。
本来 unionIdPlatform
的取值,应该是开发者可以自行决定的,但是 JavaScript SDK 基于易用性的目的,在 loginWithAuthDataAndUnionId
之外,还额外提供了两个接口:
AV.User.loginWithQQAppWithUnionId
,这里默认使用qq
作为unionIdPlatform
。AV.User.loginWithWeappWithUnionId
,这里默认使用weixin
作为unionIdPlatform
。
从我们的统计来看,这两个接口已经被很多开发者接受,在大量线上产品中产生着实际数据。所以为了避免数据在不同平台(例如 Android 和 iOS 应用)间发生冲突,建议大家统一按照 JavaScript SDK 的默认值来设置 unionIdPlatform
,即:
- 微信平台的多个应用,统一使用
weixin
作为unionIdPlatform
; - QQ 平台的多个应用,统一使用
qq
作为unionIdPlatform
; - 微博平台的多个应用,统一使用
weibo
作为unionIdPlatform
; - 除此之外的其他平台,开发者可以自行定义
unionIdPlatform
的名字,只要自己保证多个应用间统一即可。
主副应用不同登录顺序出现的不同结果
上面的流程是用户先登录了「云服务通讯」这个主应用,然后再登录「云服务技术支持」这个副应用,所以账号都被通过 UnionID 有效关联起来了。可能有人会想到另外一个问题,如果用户 B 先登录副应用,后登录主应用,这时候会发生什么事情呢?
用户 B 首先登录副应用的时候,提供了「平台名为 wxleansupport
、uid 为 supportopenid
、UnionID 为 unionid4a
」的第三方鉴权信息,并且指定「UnionIDPlatform 为 weixin
、asMainAccount
为 false
」(与上面的调用完全一致),此时云端由于找不到存在的 UnionID,会新建一个 TDSUser
对象,该账户 authData
结果为:
{
"wxleansupport": {
"platform": "weixin",
"uid": "supportopenid",
"expires_in": 1384686496,
"main_account": false,
"access_token": "supporttoken",
"unionid": "unionid4a"
}
}
用户 B 接着又使用了主应用,ta 再次通过微信登录,此时以「平台名为 wxleanoffice
、uid 为 officeopenid
、UnionID 为 unionid4a
」的第三方鉴权信息,以及「UnionIDPlatform 为 weixin
、asMainAccount
为 true
」的参数进行登录,此时云端由于找不到存在的 UnionID,会再次新建一个 TDSUser
对象,该账户 authData
结果为:
{
"wxleanoffice": {
"platform": "weixin",
"uid": "officeopenid",
"expires_in": 1384686496,
"main_account": true,
"access_token": "officetoken",
"unionid": "unionid4a"
},
"_weixin_unionid": {
"uid": "unionid4a"
}
}
还有更复杂的情况。如果某公司的产品之前就接入了微信登录,产生了很多存量用户,并且分散在不同的子产品中,这时候怎么办?我们接下来专门讨论此时的解决方案。
存量账户如何通过 UnionID 实现关联
还是以我们的两个子产品「云服务通讯」(后续以「产品 1」指代)和「云服务技术支持为例」(后续以「产品 2」指代)为例,在接入 UnionID 之前,我们就接入了之前版本的微信平台登录,这时候账户系统内可能存在多种账户:
- 只使用产品 1 的微信用户 A
- 只使用产品 2 的微信用户 B
- 同时使用两个产品的微信用户 C
此时的存量账户表如下所示:
objectId | 微信用户 | authData.{platform} | authData._{platform}_unionid |
---|---|---|---|
1 | UserA | openid1(对应产品 1) | N/A |
2 | UserB | openid2(对应产品 2) | N/A |
3 | UserC | openid3(对应产品 1) | N/A |
4 | UserC | openid4(对应产品 2) | N/A |
现在我们对两个子产品进行升级,接入 UnionID 体系。这时因为已经有同一个微信用户在不同子产品中创建了不同的账户(例如 objectId
为 3 和 4 的账户),我们需要确定以哪个平台的账号为主。比如决定使用「云服务通讯」上生成的账号为主账号,则在该应用程序更新版本时,使用 asMainAccount=true
参数。这个应用带着 UnionID 登录匹配或创建的账号将作为主账号,之后所有这个 UnionID 的登录都会匹配到这个账号。请注意这时 _User
表里会剩下一些用户数据,也就是没有被选为主账号的、其他平台的同一个用户的旧账号数据(例如 objectId
为 2 和 4 的账户)。这部分数据会继续服务于已经发布的但仍然使用 OpenID 登录的旧版应用。
接下来我们看一下,如果以产品 1 的账户作为「主账户」,按照前述的方式同时提供 openid/unionid 完成登录,则最后达到的结果是:
- 使用老版本的用户,不管在哪个产品里面,都可以和往常一样通过 openid 登录到正确的账户;
- 使用产品 1 的新版本的老用户,通过 openid/unionid 组合,还是绑定到原来的账户。例如 UserC 在产品 1 中通过 openid3/unionId3 还是会绑定到 objectId=3 的账户(会增加 uniondId 记录);而 UserC 在产品 2 的新版本中,通过 openid4/unionId3 的组合则会绑定到 objectId=3 的账户,而不再是原来的 objectId=4 的账户。
- 使用产品 1 的新版本的新用户,通过 openid/unionid 组合,会创建新的账户;之后该用户再使用产品 2 的新版本,也会绑定到刚才创建的新账户上。
以上面的三个用户为例,他们分别升级到两个产品的最新版,且最终都会启用两个产品,则账户表的最终结果如下:
objectId | 微信用户 | authData.{platform} | authData._{platform}_unionid |
---|---|---|---|
1 | UserA | openid1(对应产品 1)/openid6(对应产品 2) | unionId_user_A |
2 | UserB | openid2(对应产品 2) | N/A |
3 | UserC | openid3(对应产品 1)/openid4(对应产品 2) | unionId_user_C |
4 | UserC | openid4(对应产品 2) | N/A |
5 | UserB | openid5(对应产品 1)/openid2(对应产品 2) | unionId_user_B |
之后有新的用户 D,分别在两个产品的新版本中登录,则账户表中会增加一条新的 objectId=6 的记录,结果如下:
objectId | 微信用户 | authData.{platform} | authData._{platform}_unionid |
---|---|---|---|
1 | UserA | openid1(对应产品 1)/openid6(对应产品 2) | unionId_user_A |
2 | UserB | openid2(对应产品 2) | N/A |
3 | UserC | openid3(对应产品 1)/openid4(对应产品 2) | unionId_user_C |
4 | UserC | openid4(对应产品 2) | N/A |
5 | UserB | openid5(对应产品 1)/openid2(对应产品 2) | unionId_user_B |
6 | UserD | openid7(对应产品 1)/openid8(对应产品 2) | unionId_user_D |
如果之后我们增加了新的子产品 3,这些用户在子产品 3 中也进行微信登录的话,那么四个用户还是会继续绑定到 objectId 为 1/3/5/6 的主账户。此时账户表的结果会变为:
objectId | 微信用户 | authData.{platform} | authData._{platform}_unionid |
---|---|---|---|
1 | UserA | openid1(对应产品 1)/openid6(对应产品 2)/openid9(对应产品 3) | unionId_user_A |
2 | UserB | openid2(对应产品 2) | N/A |
3 | UserC | openid3(对应产品 1)/openid4(对应产品 2)/openid10(对应产品 3) | unionId_user_C |
4 | UserC | openid4(对应产品 2) | N/A |
5 | UserB | openid5(对应产品 1)/openid2(对应产品 2)/openid11(对应产品 3) | unionId_user_B |
6 | UserD | openid7(对应产品 1)/openid8(对应产品 2)/openid12(对应产品 3) | unionId_user_D |