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