meteor 开发者

Meteor开发指南 — 基于Meteor实现React Nat

2016-02-24  本文已影响1163人  时见疏星

原文来自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

上一篇下一篇

猜你喜欢

热点阅读