Meteor開發指南 — 基于Meteor實現React Native用戶認證

原文來自Meteor Authentication from React Native,這是Meteor React Native系列的第二篇,第一篇在這里,第二部分的Repo會在稍后放出。

這篇文章是上篇如何輕松連接一個React Native應用到Meteor服務器的后續。我們將討論下一個你會接觸到的東西,也就是用戶認證系統。我們會討論如何通過用戶名密碼,email密碼或通過一個恢復令牌(resume token)來進行登錄。

創建應用

在上一篇文章中已經寫到了如何連接一個React Native應用到Meteor服務器上,所以在此就不在贅述。如果你需要幫助,請參見上一篇文章

作為開始,我們只需要clone上次的Github repo:

git clone https://github.com/spencercarli/quick-meteor-react-native

我們會采用這個倉庫的代碼作為起始代碼,但是我們需要做出一些小修改:

cd meteor-app && meteor add accounts-password

首先打開這個項目,然后添加accounts-password這個包。

然后,創建RNApp/app/ddp.js:

import DDPClient from 'ddp-client'; 
let ddpClient = new DDPClient();

export default ddpClient; 

然后打開RNApp/app/index.js,將如下代碼進行替換:

import DDPClient from 'ddp-client'; 
let ddpClient = new DDPClient(); 

替換為

import ddpClient from './ddp'; 

我們這么做是為了把注冊登錄邏輯放到index.js文件之外,讓項目結構更清晰規范。

創建用戶

在深入到登錄之前,我們需要了解如何創建用戶。我們將借助Meteor核心方法createUser。我們將使用它來完成email和password的認證。你可以在(Meteor docs)[http://docs.meteor.com/#/full/accounts_createuser]中查看這個方法有哪些參數和選項。

RNApp/app/ddp.js中,添加如下代碼:

import DDPClient from 'ddp-client';  
let ddpClient = new DDPClient();

ddpClient.signUpWithEmail = (email, password, cb) => {  
  let params = {
    email: email,
    password: password
  };

  return ddpClient.call('createUser', [params], cb);
};

ddpClient.signUpWithUsername = (username, password, cb) => {  
  let params = {
    username: username,
    password: password
  };

  return ddpClient.call('createUser', [params], cb);
};

export default ddpClient;  

接下來我們會為它創建相應UI。

探索Meteor方法login

Meteor核心提供了一個方法login,我們可以使用它來處理DDP連接的認證。這意味著this.userId在Meteor方法和發布中可用,你可以使用它來認證。這個login方法可以處理Meteor所有的登錄服務,包括通過email,username,resume token還有Oauth登錄(盡管這里并不涉及Oauth)。

使用login方法你傳遞一個object作為單一參數到函數中—object的形式決定了你如何登錄,下面是各種登錄形式:

For Email and Password:

{ user: { email: USER_EMAIL }, password: USER_PASSWORD }

For Username and Password:

{ user: { username: USER_USERNAME }, password: USER_PASSWORD }

For Resume Token:

{ resume: RESUME_TOKEN }

使用Email和Password登錄

RNApp/app/ddp.js中,添加如下代碼:

/*
 * Removed from snippet for brevity
 */

ddpClient.loginWithEmail = (email, password, cb) => {  
  let params = {
    user: {
      email: email
    },
    password: password
  };

  return ddpClient.call("login", [params], cb)
};

export default ddpClient; 

使用Username和Password登錄

RNApp/app/ddp.js中,添加如下代碼:

/*
 * Removed from snippet for brevity
 */

ddpClient.loginWithUsername = (username, password, cb) => {  
  let params = {
    user: {
      username: username
    },
    password: password
  };

  return ddpClient.call("login", [params], cb)
};

存儲用戶數據

我們將使用React Native中的AsyncStorage
API來存儲登錄令牌(login token),令牌失效期(login token expiration)和用戶ID(userId)。這些數據會在成功登錄或者創建賬戶后返回。

RNApp/app/ddp.js中,添加如下代碼:

import DDPClient from 'ddp-client';  
import { AsyncStorage } from 'react-native';

/*
 * Removed from snippet for brevity
 */

ddpClient.onAuthResponse = (err, res) => {  
  if (res) {
    let { id, token, tokenExpires } = res;

    AsyncStorage.setItem('userId', id.toString());
    AsyncStorage.setItem('loginToken', token.toString());
    AsyncStorage.setItem('loginTokenExpires', tokenExpires.toString());
  } else {
    AsyncStorage.multiRemove(['userId', 'loginToken', 'loginTokenExpires']);
  }
}

export default ddpClient;  

這會將我們的憑證持久化存儲,在下次重新打開app時就可以自動登錄了。

使用Resume Token登錄

存儲了用戶數據之后,我們就可以用Resume Token進行登錄了。

RNApp/app/ddp.js中,添加如下代碼:

/*
 * Removed from snippet for brevity
 */

ddpClient.loginWithToken = (loginToken, cb) => {  
  let params = { resume: loginToken };

  return ddpClient.call("login", [params], cb)
}

export default ddpClient;  

登出

RNApp/app/ddp.js中,添加如下代碼:

/*
 * Removed from snippet for brevity
 */

ddpClient.logout = (cb) => {  
  AsyncStorage.multiRemove(['userId', 'loginToken', 'loginTokenExpires']).
    then((res) => {
      ddpClient.call("logout", [], cb)
    });
}

export default ddpClient;  

先刪除AsyncStorage中的三個憑證,然后調用logout方法。

UI部分

First thing I want to do is break up RNApp/app/index
a bit. It'll make it easier to manage later on.

First, create RNApp/app/loggedIn.js:

import React, {  
  View,
  Text
} from 'react-native';

import Button from './button';

import ddpClient from './ddp';

export default React.createClass({  
  getInitialState() {
    return {
      posts: {}
    }
  },

  componentDidMount() {
    this.makeSubscription();
    this.observePosts();
  },

  observePosts() {
    let observer = ddpClient.observe("posts");
    observer.added = (id) => {
      this.setState({posts: ddpClient.collections.posts})
    }
    observer.changed = (id, oldFields, clearedFields, newFields) => {
      this.setState({posts: ddpClient.collections.posts})
    }
    observer.removed = (id, oldValue) => {
      this.setState({posts: ddpClient.collections.posts})
    }
  },

  makeSubscription() {
    ddpClient.subscribe("posts", [], () => {
      this.setState({posts: ddpClient.collections.posts});
    });
  },

  handleIncrement() {
    ddpClient.call('addPost');
  },

  handleDecrement() {
    ddpClient.call('deletePost');
  },

  render() {
    let count = Object.keys(this.state.posts).length;
    return (
      <View>
        <Text>Posts: {count}</Text>
        <Button text="Increment" onPress={this.handleIncrement}/>
        <Button text="Decrement" onPress={this.handleDecrement}/>
      </View>
    );
  }
});

你會發現上面的代碼和RNApp/app/index.js基本雷同。是的,我們基本上就是把整個現有的app代碼移到了loggedIn.js文件中。下一步,我們將修改RNApp/app/index.js來使用新創建的loggedIn.js文件。

修改RNApp/app/index.js代碼如下:

import React, {  
  View,
  StyleSheet
} from 'react-native';

import ddpClient from './ddp';  
import LoggedIn from './loggedIn';

export default React.createClass({  
  getInitialState() {
    return {
      connected: false
    }
  },

  componentDidMount() {
    ddpClient.connect((err, wasReconnect) => {
      let connected = true;
      if (err) connected = false;

      this.setState({ connected: connected });
    });
  },

  render() {
    let body;

    if (this.state.connected) {
      body = <LoggedIn />;
    }

    return (
      <View style={styles.container}>
        <View style={styles.center}>
          {body}
        </View>
      </View>
    );
  }
});

const styles = StyleSheet.create({  
  container: {
    flex: 1,
    justifyContent: 'center',
    backgroundColor: '#F5FCFF',
  },
  center: {
    alignItems: 'center'
  }
});

可以看到,這里我們在index.js中使用了loggedIn中定義的<LoggedIn />組件。

UI部分:登錄

我們來創建一些登錄用的UI。我們只創建email登錄用的,但是使用username登錄完全可以。

創建RNApp/app/loggedOut.js

import React, {  
  View,
  Text,
  TextInput,
  StyleSheet
} from 'react-native';

import Button from './button';  
import ddpClient from './ddp';

export default React.createClass({  
  getInitialState() {
    return {
      email: '',
      password: ''
    }
  },

  handleSignIn() {
    let { email, password } = this.state;
    ddpClient.loginWithEmail(email, password, (err, res) => {
      ddpClient.onAuthResponse(err, res);
      if (res) {
        this.props.changedSignedIn(true);
      } else {
        this.props.changedSignedIn(false);
      }
    });

    // Clear the input values on submit
    this.refs.email.setNativeProps({text: ''});
    this.refs.password.setNativeProps({text: ''});
  },

  handleSignUp() {
    let { email, password } = this.state;
    ddpClient.signUpWithEmail(email, password, (err, res) => {
      ddpClient.onAuthResponse(err, res);
      if (res) {
        this.props.changedSignedIn(true);
      } else {
        this.props.changedSignedIn(false);
      }
    });

    // Clear the input values on submit
    this.refs.email.setNativeProps({text: ''});
    this.refs.password.setNativeProps({text: ''});
  },

  render() {
    return (
      <View>
        <TextInput
          style={styles.input}
          ref="email"
          onChangeText={(email) => this.setState({email: email})}
          autoCapitalize="none"
          autoCorrect={false}
          placeholder="Email"
        />
        <TextInput
          style={styles.input}
          ref="password"
          onChangeText={(password) => this.setState({password: password})}
          autoCapitalize="none"
          autoCorrect={false}
          placeholder="Password"
          secureTextEntry={true}
        />

        <Button text="Sign In" onPress={this.handleSignIn} />
        <Button text="Sign Up" onPress={this.handleSignUp} />
      </View>
    )
  }
});

const styles = StyleSheet.create({  
  input: {
    height: 40,
    width: 350,
    padding: 10,
    marginBottom: 10,
    backgroundColor: 'white',
    borderColor: 'gray',
    borderWidth: 1
  }
});

現在我們需要在index中展示我們的登出組件。

RNApp/app/index.js添加和修改如下代碼:

/*
 * Removed from snippet for brevity
 */
import LoggedOut from './loggedOut';

export default React.createClass({  
  getInitialState() {
    return {
      connected: false,
      signedIn: false
    }
  },

  componentDidMount() {
    ddpClient.connect((err, wasReconnect) => {
      let connected = true;
      if (err) connected = false;

      this.setState({ connected: connected });
    });
  },

  changedSignedIn(status = false) {
    this.setState({signedIn: status});
  },

  render() {
    let body;

    if (this.state.connected && this.state.signedIn) {
      body = <LoggedIn changedSignedIn={this.changedSignedIn} />; // Note the change here as well
    } else if (this.state.connected) {
      body = <LoggedOut changedSignedIn={this.changedSignedIn} />;
    }

    return (
      <View style={styles.container}>
        <View style={styles.center}>
          {body}
        </View>
      </View>
    );
  }
});

快要完成了!只剩下最后兩步啦。下面,我們要讓用戶能夠登出。

RNApp/app/loggedIn.js中:

/*
 * Removed from snippet for brevity
 */

export default React.createClass({  
  /*
   * Removed from snippet for brevity
   */
  handleSignOut() {
    ddpClient.logout(() => {
      this.props.changedSignedIn(false)
    });
  },

  render() {
    let count = Object.keys(this.state.posts).length;
    return (
      <View>
        <Text>Posts: {count}</Text>
        <Button text="Increment" onPress={this.handleIncrement}/>
        <Button text="Decrement" onPress={this.handleDecrement}/>

        <Button text="Sign Out" onPress={() => this.props.changedSignedIn(false)} />
      </View>
    );
  }
});

最后一步!我們將實現自動登錄功能。如果一個用戶在其AsyncStorage中有合法的loginToken,我們幫他自動登錄:

In RNApp/app/loggedOut.js:

import React, {  
  View,
  Text,
  TextInput,
  StyleSheet,
  AsyncStorage // Import AsyncStorage
} from 'react-native';

import Button from './button';  
import ddpClient from './ddp';

export default React.createClass({  
  getInitialState() {
    return {
      email: '',
      password: ''
    }
  },

  componentDidMount() {
    // Grab the token from AsyncStorage - if it exists then attempt to login with it.
    AsyncStorage.getItem('loginToken')
      .then((res) => {
        if (res) {
          ddpClient.loginWithToken(res, (err, res) => {
            if (res) {
              this.props.changedSignedIn(true);
            } else {
              this.props.changedSignedIn(false);
            }
          });
        }
      });
  },

  handleSignIn() {
    let { email, password } = this.state;
    ddpClient.loginWithEmail(email, password, (err, res) => {
      ddpClient.onAuthResponse(err, res);
      if (res) {
        this.props.changedSignedIn(true);
      } else {
        this.props.changedSignedIn(false);
      }
    });

    // Clear the input values on submit
    this.refs.email.setNativeProps({text: ''});
    this.refs.password.setNativeProps({text: ''});
  },

  /*
   * Removed from snippet for brevity
   */
});

一切完成!現在我們就能夠使用Meteor作為后端為React Native應用提供用戶認證。它給你在Meteor Methods和Meteor Publications中提供了this.userId。我們可以更新meteor-app/both/posts.js文件中的addPost方法來測試一下:

'addPost': function() {  
  Posts.insert({
    title: 'Post ' + Random.id(),
    userId: this.userId
  });
},

看看userId是不是出現在新創建的post中了?

結論

我想在這里談一下安全性的問題,也是本篇文章所沒有涉及到的。當在生產環境下時,用戶傳輸的是他們的真實數據,請確保啟用SSL(對于Meteor應用來說也是一樣)。同樣,我們也沒有在客戶端做密碼的hash,所以密碼是以明文的形式傳輸的。這同樣對SSL提出了需求。但是這里談及密碼hash會使文章變得冗長。我們會在下篇文章中談及它。

你可以在Github上查看本項目完整代碼:
https://github.com/spencercarli/meteor-react-native-authentication

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容