Skip to content

1.存储 4 AVUser

jwfing edited this page Jun 2, 2018 · 2 revisions

用户系统几乎是每款应用都要加入的功能。除了基本的注册、登录和密码重置,移动端开发还会使用手机号一键登录、短信验证码登录等功能。LeanStorage 提供了一系列接口来帮助开发者快速实现各种场景下的需求。

AVUser 是用来描述一个用户的特殊对象,与之相关的数据都保存在 _User 数据表中。

用户的属性

默认属性

用户名、密码、邮箱是默认提供的三个属性,访问方式如下:

    String currentUsername = AVUser.getCurrentUser().getUsername();
    String currentEmail = AVUser.getCurrentUser().getEmail();

    // 请注意,以下代码无法获取密码
    String currentPassword = AVUser.getCurrentUser().getPassword();// 无 getPassword() 此方法

请注意代码中,密码是仅仅是在注册的时候可以设置的属性(这部分代码可参照 用户名和密码注册),它在注册完成之后并不会保存在本地(SDK 不会以明文保存密码这种敏感数据),所以在登录之后,再访问密码这个字段是为空的

自定义属性

用户对象和普通对象一样也支持添加自定义属性。例如,为当前用户添加年龄属性 age

    AVUser.getCurrentUser().put("age", 25);
    AVUser.getCurrentUser().save();

修改属性

很多开发者会有这样的疑问:「为什么我不能修改任意一个用户的属性?」

因为很多时候,就算是开发者也不要轻易修改用户的基本信息,例如用户的手机号、社交账号等个人信息都比较敏感,应该由用户在 App 中自行修改。所以为了保证用户的数据仅在用户自己已登录的状态下才能修改,云端对所有针对 AVUser 对象的数据操作都要做验证。

例如,先为当前用户增加一个 age 属性,保存后再更改它的值:

    AVUser.getCurrentUser().put("age", 25);
    AVUser.getCurrentUser().save();
    AVUser.getCurrentUser().put("age", 27);
    AVUser.getCurrentUser().save();

AVUser 的自定义属性在使用上与 AVObject 没有本质区别。

注册

手机号码注册

一些应用为了提高首次使用的友好度,一般会允许用户浏览一些内容,直到用户发起了一些操作才会要求用户输入一个手机号,而云端会自动发送一条验证码的短信给用户的手机号,最后验证一下,完成一个用户注册并且登录的操作,例如很多团购类应用都有这种用户场景。

首先调用发送验证码的接口:

    try {
        AVOSCloud.requestSMSCode("13577778888");
    } catch (AVException e) {
        // 发送失败可以查看 e 里面提供的信息
        e.printStackTrace();
    }

然后在 UI 上给与用户输入验证码的输入框,用户点击登录的时候调用如下接口:

    AVUser avUser = AVUser.signUpOrLoginByMobilePhone("13577778888", "123456");
    // 如果没有抛异常就表示登录成功了,并且 user 是一个全新的用户

用户名和密码注册

采用「用户名 + 密码」注册时需要注意:密码是以明文方式通过 HTTPS 加密传输给云端,云端会以密文存储密码,并且我们的加密算法是无法通过所谓「彩虹表撞库」获取的,这一点请开发者放心。换言之,用户的密码只可能用户本人知道,开发者不论是通过控制台还是 API 都是无法获取。另外我们需要强调在客户端,应用切勿再次对密码加密,这会导致重置密码等功能失效

例如,注册一个用户的示例代码如下(用户名 Tom 密码 cat!@#123):

    try {
        AVUser user = new AVUser();// 新建 AVUser 对象实例
        user.setUsername("Tom");// 设置用户名
        user.setPassword("cat!@#123");// 设置密码
        user.setEmail("tom@leancloud.cn");// 设置邮箱
        user.signUp();
    } catch (AVException e) {
        // 失败的原因可能有多种,常见的是用户名已经存在。
        e.printStackTrace();
    }

我们建议在可能的情况下尽量使用异步版本的方法,这样就不会影响到应用程序主 UI 线程的响应。

如果注册不成功,请检查一下返回的错误对象。最有可能的情况是用户名已经被另一个用户注册,错误代码 202,即 _User 表中的 username 字段已存在相同的值,此时需要提示用户尝试不同的用户名来注册。同样,邮件 email 和手机号码 mobilePhoneNumber 字段也要求在各自的列中不能有重复值出现,否则会出现 203214 错误。

开发者也可以要求用户使用 Email 做为用户名注册,即在用户提交信息后将 _User 表中的 usernameemail 字段都设为相同的值,这样做的好处是用户在忘记密码的情况下可以直接使用「邮箱重置密码」功能,无需再额外绑定电子邮件。

关于自定义邮件模板和验证链接,请参考《自定义应用内用户重设密码和邮箱验证页面》。

设置手机号码

微信、陌陌等流行应用都会建议用户将账号和一个手机号绑定,这样方便进行身份认证以及日后的密码找回等安全模块的使用。我们也提供了一整套发送短信验证码以及验证手机号的流程,这部分流程以及代码演示请参考 (Java SDK 文档待补充)。

验证邮箱

许多应用会通过验证邮箱来确认用户注册的真实性。如果在 控制台 > 存储 > 设置 > 用户账号 中勾选了 用户注册时,发送验证邮件,那么当一个 AVUser 在注册时设置了邮箱,云端就会向该邮箱自动发送一封包含了激活链接的验证邮件,用户打开该邮件并点击激活链接后便视为通过了验证。

有些用户可能在注册之后并没有点击激活链接,而在未来某一个时间又有验证邮箱的需求,这时需要调用如下接口让云端重新发送验证邮件:

  AVUser.requestEmailVerify("abc@xyz.com", new RequestEmailVerifyCallback() {
            @Override
            public void done(AVException e) {
                if (e == null) {
                    // 求重发验证邮件成功
                }
            }
        });

当用户 AVUser 通过 setEmail() 更新成一个新邮箱并成功 save 后,云端会自动向新邮箱发一封验证邮件,因此开发者不需要再单独调用 requestEmailVerify 接口来发送验证邮件。

登录

我们提供了多种登录方式,以满足不同场景的应用。

用户名和密码登录

    AVUser avUser = AVUser.logIn("Tom", "cat!@#123");

手机号和密码登录

(Java SDK 文档待补充) 可以帮助你更好地理解手机号匹配密码登录的流程以及适用范围,所以推荐详细阅读。

    AVUser avUser = AVUser.loginByMobilePhoneNumber("13577778888", "cat!@#123");

以上的手机号码即使没有经过验证,只要密码正确也可以成功登录。如果希望阻止未验证的手机号码用于登录,则需要在 控制台 > 存储 > 设置 > 用户账号 中勾选 未验证手机号码的用户,禁止登录。这种方式也提高了用户账号的合法性与安全性。

手机号和验证码登录

首先,调用发送登录验证码的接口:

    AVUser.requestLoginSmsCode("13577778888");

然后在界面上引导用户输入收到的 6 位短信验证码:

    AVUser avUser = AVUser.signUpOrLoginByMobilePhone("13577778888", "238825");

测试用的手机号和固定验证码

对于使用「手机号 + 验证码」登录的应用来说,在上架前提交至 Apple Store 进行审核的过程中,可能会面临 Apple 人员因没有有效的手机号码而无法登录来进行评估审核,或者开发者也无法提供固定手机号和验证码的尴尬情况。

另外,开发者在开发测试过程中也会面临在短时间内需要多次登录或注销的操作,由于验证码有时间间隔与总次数限制,这样就会带来种种不便。

为解决这些问题,我们允许为每个应用设置一个用于测试目的的手机号码,LeanCloud 平台会为它生成一个固定的验证码,每次使用这一对号码组合进行验证都会得到成功的结果。

进入 应用控制台 > 消息 > 短信 > 设置 > 其他 来设置 测试手机号

单点登录

如果想实现在当前设备 A 上登录后,强制令之前在其他设备上的登录失效,可以按照以下方案来实现:

  1. 建立一个设备表,记录用户登录信息和当前设备的信息。
  2. 设备 A 登录成功后,更新设备表,将当前设备标记为当前用户登录的最新设备。
  3. 设备 B 中的应用启动时,检查设备表,发现最新设备不是当前设备,调用 AVUser 的 logout 方法退出登录。

当前用户

打开微博或者微信,它不会每次都要求用户都登录,这是因为它将用户数据缓存在了客户端。 同样,只要是调用了登录相关的接口,LeanCloud SDK 都会自动缓存登录用户的数据。 例如,判断当前用户是否为空,为空就跳转到登录页面让用户登录,如果不为空就跳转到首页:

    AVUser currentUser = AVUser.getCurrentUser();
    if (currentUser != null) {
        // 跳转到首页
    } else {
        //用户对象为空时,可打开用户注册界面…
    }

如果不调用 登出 方法,当前用户的缓存将永久保存在客户端。

SessionToken

所有登录接口调用成功之后,云端会返回一个 SessionToken 给客户端,客户端在发送 HTTP 请求的时候,Java SDK 会在 HTTP 请求的 Header 里面自动添加上当前用户的 SessionToken 作为这次请求发起者 AVUser 的身份认证信息。

如果在 控制台 > 存储 > 设置 > 用户账号 中勾选了 密码修改后,强制客户端重新登录,那么当用户密码再次被修改后,已登录的用户对象就会失效,开发者需要使用更改后的密码重新调用登录接口,使 SessionToken 得到更新,否则后续操作会遇到 403 (Forbidden) 的错误。

使用 SessionToken 登录

在没有用户名密码的情况下,客户端可以使用 SessionToken 来登录。常见的使用场景有:

  • 应用内根据以前缓存的 SessionToken 登录
  • 应用内的某个页面使用 WebView 方式来登录 LeanCloud
  • 在服务端登录后,返回 SessionToken 给客户端,客户端根据返回的 SessionToken 登录。
// 1. getSessionToken()
// 2. become(sessionToken)
// 3. 在成功的回调中更新 currentUser 的信息 become(sessionToken)

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

账户锁定

输入错误的密码或验证码会导致用户登录失败。如果在 15 分钟内,同一个用户登录失败的次数大于 6 次,该用户账户即被云端暂时锁定,此时云端会返回错误码 {"code":1,"error":"登录失败次数超过限制,请稍候再试,或者通过忘记密码重设密码。"},开发者可在客户端进行必要提示。

锁定将在最后一次错误登录的 15 分钟之后由云端自动解除,开发者无法通过 SDK 或 REST API 进行干预。在锁定期间,即使用户输入了正确的验证信息也不允许登录。这个限制在 SDK 和云引擎中都有效。

重置密码

邮箱重置密码

我们都知道,应用一旦加入账户密码系统,那么肯定会有用户忘记密码的情况发生。对于这种情况,我们为用户提供了一种安全重置密码的方法。

重置密码的过程很简单,用户只需要输入注册的电子邮件地址即可:

    AVUser.requestPasswordReset("myemail@example.com");

密码重置流程如下:

  1. 用户输入注册的电子邮件,请求重置密码;
  2. LeanStorage 向该邮箱发送一封包含重置密码的特殊链接的电子邮件;
  3. 用户点击重置密码链接后,一个特殊的页面会打开,让他们输入新密码;
  4. 用户的密码已被重置为新输入的密码。

关于自定义邮件模板和验证链接,请参考《自定义应用内用户重设密码和邮箱验证页面》。

手机号码重置密码

与使用 邮箱重置密码 类似,「手机号码重置密码」使用下面的方法来获取短信验证码:

    AVUser.requestPasswordResetBySmsCode("18612340000");

注意!用户需要先绑定手机号码,然后使用短信验证码来重置密码:

    AVUser.resetPasswordBySmsCode("123456", "password");

登出

用户登出系统时,SDK 会自动清理缓存信息。

    AVUser.logOut();// 清除缓存用户对象
    AVUser currentUser = AVUser.getCurrentUser();// 现在的 currentUser 是 null 了

用户的查询

为了安全起见,新创建的应用的 _User 表默认关闭了 find 权限,这样每位用户登录后只能查询到自己在 _User 表中的数据,无法查询其他用户的数据。如果需要让其查询其他用户的数据,建议单独创建一张表来保存这类数据,并开放这张表的 find 查询权限。

设置数据表权限的方法,请参考 数据与安全 · Class 级别的权限。我们推荐开发者在 云引擎 中封装用户查询,只查询特定条件的用户,避免开放 _User 表的全部查询权限。

查询用户代码如下:

    AVQuery<AVUser> userQuery = new AVQuery<>("_User");

浏览器中查看用户表

用户表是一个特殊的表,专门存储用户对象。在浏览器端,你会看到一个 _User 表。

匿名用户

使用匿名用户,可以让应用不提供注册步骤也能创建用户:

AVAnonymousUtils.logIn(new LogInCallback() {
  public void done(T user, AVException e) {
  };
});

如果有需要,应用还可以让匿名用户完善信息,例如设置用户名、密码和邮箱等:

AVUser currentUSer = AVUser.getCurrentUser();
currentUser.setEmail("email");
currentUser.setPassword("password");
currentUser.setUsername("username");
currentUser.signUpInBackground(new SignUpCallback() {
  public void done(AVException e) {
  }
});

如果没有完善信息,匿名用户一旦登出,其身份就会永久消失。

角色

关于用户与角色的关系,我们有一个更为详尽的文档介绍这部分的内容,并且针对权限管理有深入的讲解,详情请阅读《ACL 权限管理指南》。

子类化

LeanCloud 希望设计成能让人尽快上手并使用。你可以通过 AVObject.get 方法访问所有的数据。但是在很多现有成熟的代码中,子类化能带来更多优点,诸如简洁、可扩展性以及 IDE 提供的代码自动完成的支持等等。子类化不是必须的,你可以将下列代码转化:

    AVObject student = new AVObject("Student");
    student.put("name", "小明");
    student.save();

可改写成:

    Student student = new Student();
    student.put("name", "小明");
    student.save();

这样代码看起来是不是更简洁呢?

子类化 AVObject

要实现子类化,需要下面几个步骤:

  1. 首先声明一个子类继承自 AVObject
  2. 添加 @AVClassName 注解。它的值必须是一个字符串,也就是你过去传入 AVObject 构造函数的类名。这样以来,后续就不需要再在代码中出现这个字符串类名;
  3. 确保你的子类有一个 public 的默认(参数个数为 0)的构造函数。切记不要在构造函数里修改任何 AVObject 的字段;
  4. 在你的应用初始化的地方,在调用 AVOSCloud.initialize() 之前注册子类 AVObject.registerSubclass(YourClass.class)

下面是实现 Student 子类化的例子:

// Student.java
import com.avos.avoscloud.AVClassName;
import com.avos.avoscloud.AVObject;

@AVClassName("Student")
public class Student extends AVObject {
}

// App.java
import com.avos.avoscloud.AVOSCloud;
import android.app.Application;

public class App extends Application {
  @Override
  public void onCreate() {
    super.onCreate();

    AVObject.registerSubclass(Student.class);
    AVOSCloud.initialize("{{appid}}","{{appkey}}");
  }
}

访问器、修改器和方法

添加方法到 AVObject 的子类有助于封装类的逻辑。你可以将所有跟子类有关的逻辑放到一个地方,而不是分成多个类来分别处理商业逻辑和存储/转换逻辑。

你可以很容易地添加访问器和修改器到你的 AVObject 子类。像平常那样声明字段的gettersetter 方法,但是通过 AVObject 的 getput 方法来实现它们。下面是这个例子为 Student 类创建了一个 content 的字段:

// Student.java
@AVClassName("Student")
public class Student extends AVObject {
  public String getContent() {
    return getString("content");
  }
  public void setContent(String value) {
    put("content", value);
  }
}

现在你就可以使用 student.getContent() 方法来访问 content 字段,并通过 student.setContent("blah blah blah") 来修改它。这样就允许你的 IDE 提供代码自动完成功能,并且可以在编译时发现到类型错误。 +

各种数据类型的访问器和修改器都可以这样被定义,使用各种 get() 方法的变种,例如 getInt()getAVFile() 或者 getMap()。 +

如果你不仅需要一个简单的访问器,而是有更复杂的逻辑,你可以实现自己的方法,例如:

ublic void takeAccusation() {
  // 处理用户举报,当达到某个条数的时候,自动打上屏蔽标志
  increment("accusation", 1);
  if (getAccusation() > 50) {
    setSpam(true);
  }
}

初始化子类

你可以使用你自定义的构造函数来创建你的子类对象。你的子类必须定义一个公开的默认构造函数,并且不修改任何父类 AVObject 中的字段,这个默认构造函数将会被 SDK 使用来创建子类的强类型的对象。

要创建一个到现有对象的引用,可以使用 AVObject.createWithoutData():

Student postReference = AVObject.createWithoutData(Student.class, student.getObjectId());

子类的序列化与反序列化

如果希望 AVObject 子类也支持 Parcelable,则需要至少满足以下几个要求:

  1. 确保子类有一个 public 并且参数为 Parcel 的构造函数,并且在内部调用父类的该构造函数。
  2. 内部需要有一个静态变量 CREATOR 实现 Parcelable.Creator
// Stduent.java
@AVClassName("Student")
public class Student extends AVObject {
  public Student(){
    super();
  }

  public Student(Parcel in){
    super(in);
  }
  //此处为我们的默认实现,当然你也可以自行实现
  public static final Creator CREATOR = AVObjectCreator.instance;
}

查询子类

你可以通过 AVObject.getQuery() 或者 AVQuery.getQuery 的静态方法获取特定的子类的查询对象。下面的例子就查询了用户发表的所有微博列表:

AVQuery<Student> query = AVObject.getQuery(Student.class);
query.whereEqualTo("pubUser", AVUser.getCurrentUser().getUsername());
List<Student> results = query.find();
for (Student a : results) {
    // ...
}

AVUser 的子类化

AVUser 作为 AVObject 的子类,同样允许子类化,你可以定义自己的 User 对象,不过比起 AVObject 子类化会更简单一些,只要继承 AVUser 就可以了:

import com.avos.avoscloud.AVObject;
import com.avos.avoscloud.AVUser;

public class MyUser extends AVUser {
    public void setNickName(String name) {
  this.put("nickName", name);
    }

    public String getNickName() {
  return this.getString("nickName");
    }
}

不需要添加 @AVClassname 注解,所有 AVUser 的子类的类名都是内建的 _User。同样也不需要注册 MyUser。

当用户子类化 AVUser 后,如果希望以后查询 AVUser 所得到的对象会自动转化为用户子类化的对象,则需要在调用 AVOSCloud.initialize() 之前添加:

AVUser.alwaysUseSubUserClass(subUser.class);

注册跟普通的 AVUser 对象没有什么不同,但是登录如果希望返回自定义的子类,必须这样:

MyUser cloudUser = AVUser.logIn(username, password,
    MyUser.class);
由于 fastjson 内部的 bug,请在定义 AVUser 时不要定义跟 AVRelation 相关的 `get` 方法。如果一定要定义的话,请通过在 Class 上添加 `@JSONType(ignores = {"属性名"})` 的方式,将其注释为非序列化字段。