[Firebase] 02 - User Management & Real-time DB

有太多东西要学,思绪不稳,干脆就跟着这哥们混就好了,完成全系列的学习。

 

1. Project name.

2. Package name.

Ref: Android Studio获取SHA1或MD5的方法

3. google-service.json in ./app

  

Main类:

Public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    private FirebaseAuth mAuth;
    private FirebaseAuth.AuthStateListener mAuthListener;

    // UI references.
    private EditText mEmail, mPassword;
    private Button btnSignIn,btnSignOut,btnViewDatabase;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
//declare buttons and edit texts in oncreate mEmail = (EditText) findViewById(R.id.email); mPassword     = (EditText) findViewById(R.id.password); btnSignIn     = (Button) findViewById(R.id.email_sign_in_button); btnSignOut = (Button) findViewById(R.id.email_sign_out_button); btnViewDatabase = (Button) findViewById(R.id.view_items_screen); mAuth = FirebaseAuth.getInstance(); mAuthListener = new FirebaseAuth.AuthStateListener() { @Override public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) {     // 指明一个参数,字段或者方法的返回值不可以为null FirebaseUser user = firebaseAuth.getCurrentUser(); if (user != null) { // User is signed in Log.d(TAG, "onAuthStateChanged:signed_in:" + user.getUid()); toastMessage("Successfully signed in with: " + user.getEmail()); } else { // User is signed out Log.d(TAG, "onAuthStateChanged:signed_out"); toastMessage("Successfully signed out."); } // ... } }; btnSignIn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { String email = mEmail.getText().toString(); String pass = mPassword.getText().toString(); if(!email.equals("") && !pass.equals("")){ mAuth.signInWithEmailAndPassword(email,pass); }else{ toastMessage("You didn't fill in all the fields."); } } }); btnSignOut.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { mAuth.signOut(); toastMessage("Signing Out..."); } }); btnViewDatabase.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, ViewDatabase.class);  // ----> read info from firebase startActivity(intent); } }); } @Override public void onStart() { super.onStart(); mAuth.addAuthStateListener(mAuthListener);    // 注意:触发mAuth发送mail登录信息,然后静等mAuthListener对回复结果的反应 } @Override public void onStop() { super.onStop(); if (mAuthListener != null) { mAuth.removeAuthStateListener(mAuthListener); } } /** * customizable toast * @param message */ private void toastMessage(String message){ Toast.makeText(this,message,Toast.LENGTH_SHORT).show(); } }

 

定义一个对应firebase节点的类:

public class UserInformation {

    private String name;
    private String email;
    private String phone_num;

    public UserInformation(){

    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getPhone_num() {
        return phone_num;
    }
    public void setPhone_num(String phone_num) {
        this.phone_num = phone_num;
    }
}

 

读firebase节点信息:

public class ViewDatabase extends AppCompatActivity {
    private static final String TAG = "ViewDatabase";

    //add Firebase Database stuff
    private FirebaseDatabase mFirebaseDatabase;
    private FirebaseAuth mAuth;
    private FirebaseAuth.AuthStateListener mAuthListener;
    private DatabaseReference myRef;
    private String userID;

    private ListView mListView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.view_database_layout);

        mListView = (ListView) findViewById(R.id.listview);

        //declare the database reference object. This is what we use to access the database.
        //NOTE: Unless you are signed in, this will not be useable.
        mAuth             = FirebaseAuth.getInstance();
        mFirebaseDatabase = FirebaseDatabase.getInstance();
        myRef             = mFirebaseDatabase.getReference();
FirebaseUser user
= mAuth.getCurrentUser(); userID = user.getUid(); mAuthListener = new FirebaseAuth.AuthStateListener() { @Override public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { FirebaseUser user = firebaseAuth.getCurrentUser(); if (user != null) { // User is signed in Log.d(TAG, "onAuthStateChanged:signed_in:" + user.getUid()); toastMessage("Successfully signed in with: " + user.getEmail()); } else { // User is signed out Log.d(TAG, "onAuthStateChanged:signed_out"); toastMessage("Successfully signed out."); } // ... } }; myRef.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { // This method is called once with the initial value and again // whenever data at this location is updated. showData(dataSnapshot); } @Override public void onCancelled(DatabaseError databaseError) { } }); } private void showData(DataSnapshot dataSnapshot) {
for(DataSnapshot ds : dataSnapshot.getChildren()){ UserInformation uInfo = new UserInformation(); uInfo.setName (ds.child(userID).getValue(UserInformation.class).getName()); //set the name uInfo.setEmail (ds.child(userID).getValue(UserInformation.class).getEmail()); //set the email uInfo.setPhone_num(ds.child(userID).getValue(UserInformation.class).getPhone_num()); //set the phone_num
       技巧性比较强的:UserInformation.class
       
//display all the information Log.d(TAG, "showData: name: " + uInfo.getName()); Log.d(TAG, "showData: email: " + uInfo.getEmail()); Log.d(TAG, "showData: phone_num: " + uInfo.getPhone_num()); ArrayList<String> array = new ArrayList<>(); array.add(uInfo.getName()); array.add(uInfo.getEmail()); array.add(uInfo.getPhone_num()); ArrayAdapter adapter = new ArrayAdapter(this ,android.R.layout.simple_list_item_1, array);  // <---- 套路 mListView.setAdapter(adapter); } } @Override public void onStart() { super.onStart(); mAuth.addAuthStateListener(mAuthListener); } @Override public void onStop() { super.onStop(); if (mAuthListener != null) { mAuth.removeAuthStateListener(mAuthListener); } } /** * customizable toast * @param message */ private void toastMessage(String message){ Toast.makeText(this,message,Toast.LENGTH_SHORT).show(); } }

 

 

插曲:规则设置


默认规则要求进行身份验证,允许通过身份验证的应用用户具有完全读写权限。

当您希望数据对应用的所有用户开放,但不对整个世界开放时,此功能会很有用。 

 

一、基础

Ref: 安全性与规则

规则类型
.read 描述是否允许用户读取数据以及何时读取。
.write 描述是否允许写入数据以及何时写入。
.validate 定义值的正确格式、值是否包含子属性以及值的数据类型。
.indexOn 指定用于编制索引的子属性,以支持排序和查询。

 

  • 身份验证

Firebase 身份验证支持为各种常见的身份验证方法(如 GoogleFacebook电子邮件地址密码登录匿名登录等等)使用访客身份验证。

用户身份是一个重要的安全概念。不同的用户拥有不同的数据,有时他们还拥有不同的能力权限。

例如,在聊天应用中,每条消息均与创建该消息的用户关联。用户也可以删除自己的消息,但不能删除其他用户发布的消息。

  • 授权

允许任何人读取路径 /foo/,但不允许任何人对其进行写入:

{
  "rules": {
    "foo": {
      ".read": true,
      ".write": false
    }
  }
}

向已通过身份验证的用户授予对 /users/<uid>/ 的写入权限,其中 <uid> 是通过 Firebase 身份验证获取的用户 ID。

{
  "rules": {
    "users": {
      "$uid": {
        ".write": "$uid === auth.uid"
      }
    }
  }
}
  • 数据验证

写入 /foo/ 的数据必须为少于 100 个字符的字符串:

{
  "rules": {
    "foo": {
      ".validate": "newData.isString() && newData.val().length < 100"
    }
  }
}
  • 定义数据库索引

索引使用 .indexOn 规则进行指定。以下是索引声明的示例,将为恐龙列表的高度和长度字段编制索引:

{
  "rules": {
    "dinosaurs": {
      ".indexOn": ["height", "length"]
    }
  }
}

 

二、保障数据安全

定义数据集:

{
  "messages": {
    "message0": {
      "content": "Hello",
      "timestamp": 1405704370369
    },
    "message1": {
      "content": "Goodbye",
      "timestamp": 1405704395231
    },
    ...
  }
}

对获得的消息进行规范和限制。 

{
  "rules": {
    "messages": {
      "$message": {
        // only messages from the last ten minutes can be read
        ".read": "data.child('timestamp').val() > (now - 600000)",

        // new messages must have a string content and a number timestamp
        ".validate": "newData.hasChildren(['content', 'timestamp']) && newData.child('content').isString() && newData.child('timestamp').isNumber()"
      }
    }
  }
}

 

  • 预定义变量

在安全规则定义中可访问很多有用的预定义变量。

预定义变量
now 当前时间,以从 Linux 计时原点开始计算的毫秒数表示。此变量非常适合验证使用 SDK 的 firebase.database.ServerValue.TIMESTAMP 创建的时间戳。
root RuleDataSnapshot,表示在尝试操作之前存在于 Firebase 数据库中的根路径
newData RuleDataSnapshot,表示尝试操作之后应存在的数据。该数据包含写入的新数据现有数据
data RuleDataSnapshot,表示尝试操作之前存在的数据
$ variables 用于表示 ID 和动态子键的通配符路径。
auth 表示通过身份验证的用户的令牌有效负载。

  • 现有数据与新数据
{
  "rules": {
      "foo": {
        // /foo is readable by the world
        ".read": true,
        // /foo is writable by the world
        ".write": true,
  // data written to /foo must be a string less than 100 characters   ".validate": "newData.isString() && newData.val().length
< 100" } } }

允许我们创建新记录或删除现有记录,但不允许对现有的非 null 数据做出更改:

// we can write as long as old data or new data does not exist
// in other words, if this is a delete or a create, but not an update
".write": "!data.exists() || !newData.exists()"
  • 引用其他路径的数据
".write": "root.child('allow_writes').val() === true &&    // /allow_writes/ 节点的值为 true
          !data.parent().child('readOnly').exists() &&     // 父节点没有 readOnly 标志设置
          newData.child('foo').exists()"                      // 新写入的数据中有一个名为 foo 的子节点
  • 读取和写入规则级联

浅层规则将重写深层规则,例如:

只要 /foo/ 包含值为 true 的子节点 baz,此安全结构即允许读取 /bar/

{
  "rules": {
     "foo": {
        // allows read to /foo/*
        ".read": "data.child('baz').val() === true",
        "bar": {
          /* ignored, since read was allowed already */
          ".read": false  // false 规则此时不起作用,因为子路径无法撤消以上的访问权限。
        }
     }
  }
} 
  • 将数据编入索引

From: https://firebase.google.com/docs/database/security/indexing-data?hl=zh-cn

使用 orderByChild 编制索引

{
  "lambeosaurus": {
    "height" : 2.1,
    "length" : 12.5,
    "weight": 5000
  },
  "stegosaurus": {
    "height" : 4,
    "length" : 9,
    "weight" : 2500
  }
}

进一步查询优化:

{
  "rules": {
    "dinosaurs": {
      ".indexOn": ["height", "length"]  // 使用 .indexOn 告诉 Firebase 也优化使用高度和长度进行的查询
    }
  }
}

使用 orderByValue 编制索引

{
  "scores": {
    "bruhathkayosaurus" : 55,
    "lambeosaurus" : 21,
    "linhenykus" : 80,
    "pterodactyl" : 93,
    "stegosaurus" : 5,
    "triceratops" : 22
  }
}

进一步查询优化:

{
  "rules": {
    "scores": {
      ".indexOn": ".value"  // 可以通过在 /scores 节点上添加 .value 规则来优化查询
    }
  }
}

 

阅后感:

基于用户的安全机制这一部分很麻烦,这应该是数据库管理人员的事情。日后再详看,点到为止。

 

 

  

Firebase初探:实时数据库(1)

Firebase初探:实时数据库(2)

此链接阅读可作为学习补充。

服务器端

为了保证效率

  • 多层嵌套的写法:对于效率来说是很不利。
"record" {
    "record1": {
        "title": "TITLE1"
        "content": "..."
        "writer": "S"
        "reply": {
            "reply1": {
                "sender": "S1",
                "content": "..."
            }
            "reply2": {
            ...
        }
    },
    "record2": {
    ...
}
  • 平展数据结构写法:说白了就是将各个属性分开成不同的树来存放,一个表拆成多个表,也就是反规范化。上面的例子就可以这样写:
"record": {
    "record1": {
        "title": "TITLE1"
        "content": "..."
        "writer": "S"
    }
    "record2": {
    ...
}

"reply": {
    "record1": {
        "reply1": {
            "sender": "S1",
            "content": "..."
        }
        "reply2": {
        ...
    }
    "record2": {
    ...
}

 

双向关系的解决方法

例如:对于一个用户来说可以从属于一个群组,一个群组又包含了用户。可以使用索引来解决。

存在双向关系的两个元组,将对方的元素作为”索引“来使用。这样就使得在访问这样的数据时可以保证效率,当然这里就要牺牲了一点数据冗余度了

"user": {
    "u1": {
        groups: {
            "g1": true
            ...
        }
    }
    ...
}

"group": {
    "g1": {
        members: {
            "u1": true
            ...
        }
    }
}

 

窗口有“数据”“规则”“备份”“使用情况” 四个选项卡。

 

root,newData,data是RuleDataSnapshot类型。

这些都是规则所用的数据快照,因此我们可以使用相关的一些函数获得其中的信息:

RuleDataSnapshot函数说明
val() 读取明确的子节点内容,这要求需要先用child()定位到叶子节点,然后再用val()读取内容。直接对一块数据进行val()不会返回想要的“一块数据”
child() 返回参数所给的相对路径,所对应的节点的快照数据
parent() 返回当前节点的父节点,所对应的快照数据。如果父节点不存在,会导致这条规则直接失败
hasChild(path) 如果这个子节点存在返回true
hasChildren([children]) 如果子节点列表都存在返回true
exists() 如果此数据快照中有数据则返回true
getPriority() 返回此数据快照中数据的优先级
isNumber() 如果其中的数据是数字,返回true
isString() 如果其中的数据是字符串,返回true
isBoolean() 如果其中的数据是布尔类型,返回true

 

$ 变量

获得当前节点名字的字符串。

{
    "rules": {
        "students": {
            "$stu_id": {   // <--id会有限制
                "name": {  // <--修改的是名字
                    ".write": "$stu_id.contains('sysu')"  // 对于修改一个学生的姓名,要求该学生的id中必须包含“sysu”
                }
            }
        }
    }
}

 

 

 

客户端

为了和数据库交互,需要得到数据库的一个实例,而且具体的交互对象是数据库中的某个节点,这时就需要获得这个节点的一个引用:

FirebaseDatabase database = FirebaseDatabase.getInstance();  // 数据库的实例
DatabaseReference mRef = database.getReference("students");  // 节点的引用,读写操作

 

write - setValue

  • Boolean
  • Long
  • Double
  • Map< String, Object >  // 利用映射的特点,可以将节点路径作为键,要插入的数据作为值,就可以做到在多个位置同时进行写入
  • List< Object >    // 对于同时插入多个数据很有帮助
  • 自定义的对象
    • 类必须要有默认构造器
    • 类必须为成员变量提供getter方法。没有getter方法的成员变量对应的键值将会被设为缺省值

 

write - push

在当前节点下添加一个新的子节点,该子节点的键基于时间戳,因此是唯一的。

 

write - updateChildren

updateChildren()方法可以修改部分数据,而不影响该节点下的其他数据,只要在参数对象中提供需要修改的数据就行了。
Firebase推荐使用平展开的数据结构,一个对象的属性可能会分散在多个JSON树中,因此对于这种数据扇出的情况,使用updateChildren(Map< String, Children >)可以很方便地一次性更新分散开来的数据,这种方式的更新具有原子性。

 

write - runTransaction

事务是数据库的重要课题之一。对于一篇帖子,一个用户点一次赞,数据库中对应的点赞数就要加一,这中间的过程为:客户端查找并获得该帖子的点赞数,加一,然后更新该帖子的点赞数。那么问题就来了,如果多个用户进行点赞时就需要考虑数据的同步问题 。 

mRef.runTransaction(new Transaction.Handler() {
    @Override
    public Transaction.Result doTransaction(MutableData mutableData) {
        Post post = mutableData.getValue(Post.class);
        post.like = post.like + 1;

        mutableData.setValue(post);
        return Transaction.success(mutableData);
    }

    @Override
    public void onComplete(DatabaseError databaseError, boolean b, DataSnapshot dataSnapshot) {
        // ...
    }
})

 

 

read - ValueEventListener

数据库主动告知你数据的变化,因此SDK提供的检索数据不是read形式,而是使用监听器来实时获得更新数据。

尽量监听低层节点,不要在高层节点设置监听器,以防拉取大型的数据快照 。

ValueEventListener listener = new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        Post post = dataSnapshot.getValue(Post.class);
        // ...
    }

    @Override
    public void onCancelled(DatabaseError databaseError) {
        // ...
    }
};
mRef.addValueEventListener(listener);

 

移除 event listener:

引用调用removeEventListener(ValueEventListener)或removeEventListener(ChildEventListener),即可移除指定的监听器。
如果在同一路径设置多个监听器,则需要一个一个移除掉,因此尽量不要在addValueEventListener()和addChildEventListener()直接使用无引用的匿名内部类。
父节点移除监听器不会影响到子节点的监听器。

 

read - ChildEventListener

  • onChildAdded():该方法会对当前的所有子节点都触发一次,并且会在未来添加子节点时触发。因此这个方法可用于遍历当前节点
  • onChildChanged():每当有子节点被修改,该方法被触发
  • onChildRemoved():每当有子节点被删除,该方法被触发
  • onChildMoved():每当子节点的次序发生变化时(例如修改优先级),该方法被触发

 

排序

方法说明
orderByChild() 按指定子节点的值进行排序
orderByKey() 按键进行排序
orderByValue() 按值进行排序
orderByPriority() 按优先级进行排序

一个引用只能使用一种排序方法;引用调用了排序方法后,返回的是Query类型引用

DatabaseReference是继承自Query类的,Query提供了各种排序和过滤的方法。

 

过滤

方法说明
limitToFirst() 从排序结果开始的最大项目数
limitToLast() 从排序结尾开始的最大项目数
startAt() 返回等于或大于指定键,值或优先级的项目
endAt() 返回小于或等于指定键,值或优先级的项目
equalTo() 返回等于指定键,值或优先级的项目

其中startAt(),endAt(), equal()可以指定键,值或优先级,因此有很多重载版本。

 

删除

数据库引用调用removeValue()方法;

通过设置值为null的方式进行删除。

 

离线

数据持久化

如果有数据需要写入数据库,而此时失去了网络连接,应用仍可继续运行,写操作队列会保存在缓存中,连接恢复后继续写入数据库。那如果接着用户关闭了应用呢?不用着急手动保存,开启磁盘持久化功能可以让数据保存在ROM中,即使应用重启也不会丢失。 
开启磁盘持久化功能只需要一句代码:

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

保持数据更新

不用添加监听器的方式来保持数据更新:【默认情况下,客户端可以在本地缓存10MB的数据】

DatabaseReference stuRef = FirebaseDatabase.getInstance().getReference("students");
stuRef.keepSynced(true);

断开连接

DatabaseReference presenceRef = FirebaseDatabase.getInstance().getReference("userLog");
presenceRef.onDisconnect().setValue("Disconnected!");

 

 

监听连接状态

客户端在数据库实例中加了一个“/.info/connected”的节点,该节点会根据连接状态而持续更新,因此可以监听这个节点的值变化,来达到监听连接状态的效果。

 

延迟

客户端时间和服务器时间不一定相同,两者之间存在时间偏差,因此我们在记录一些跟时间有关的数据,尽量使用ServerValue.TIMESTAMP,而不是本地时间。

 

posted @ 2018-03-12 12:25  郝壹贰叁  阅读(461)  评论(0)    收藏  举报