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阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容