原文來自Meteor Authentication from React Native戚啥,這是Meteor React Native系列的第二篇,第一篇在這里,第二部分的Repo會在稍后放出。
這篇文章是上篇如何輕松連接一個React Native應(yīng)用到Meteor服務(wù)器的后續(xù)拓萌。我們將討論下一個你會接觸到的東西,也就是用戶認(rèn)證系統(tǒng)升略。我們會討論如何通過用戶名密碼微王,email密碼或通過一個恢復(fù)令牌(resume token)來進(jìn)行登錄。
創(chuàng)建應(yīng)用
在上一篇文章中已經(jīng)寫到了如何連接一個React Native應(yīng)用到Meteor服務(wù)器上品嚣,所以在此就不在贅述炕倘。如果你需要幫助,請參見上一篇文章翰撑。
作為開始罩旋,我們只需要clone上次的Github repo:
git clone https://github.com/spencercarli/quick-meteor-react-native
我們會采用這個倉庫的代碼作為起始代碼,但是我們需要做出一些小修改:
cd meteor-app && meteor add accounts-password
首先打開這個項(xiàng)目,然后添加accounts-password
這個包涨醋。
然后瓜饥,創(chuàng)建RNApp/app/ddp.js
:
import DDPClient from 'ddp-client';
let ddpClient = new DDPClient();
export default ddpClient;
然后打開RNApp/app/index.js
,將如下代碼進(jìn)行替換:
import DDPClient from 'ddp-client';
let ddpClient = new DDPClient();
替換為
import ddpClient from './ddp';
我們這么做是為了把注冊登錄邏輯放到index.js
文件之外浴骂,讓項(xiàng)目結(jié)構(gòu)更清晰規(guī)范乓土。
創(chuàng)建用戶
在深入到登錄之前,我們需要了解如何創(chuàng)建用戶溯警。我們將借助Meteor核心方法createUser
趣苏。我們將使用它來完成email和password的認(rèn)證。你可以在(Meteor docs)[http://docs.meteor.com/#/full/accounts_createuser]中查看這個方法有哪些參數(shù)和選項(xiàng)愧膀。
在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;
接下來我們會為它創(chuàng)建相應(yīng)UI。
探索Meteor方法login
Meteor核心提供了一個方法login
檩淋,我們可以使用它來處理DDP連接的認(rèn)證。這意味著this.userId
在Meteor方法和發(fā)布中可用萄金,你可以使用它來認(rèn)證蟀悦。這個login
方法可以處理Meteor所有的登錄服務(wù),包括通過email氧敢,username日戈,resume token還有Oauth登錄(盡管這里并不涉及Oauth)。
使用login
方法你傳遞一個object作為單一參數(shù)到函數(shù)中—object的形式?jīng)Q定了你如何登錄孙乖,下面是各種登錄形式:
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)
};
存儲用戶數(shù)據(jù)
我們將使用React Native中的AsyncStorage
API來存儲登錄令牌(login token)唯袄,令牌失效期(login token expiration)和用戶ID(userId)弯屈。這些數(shù)據(jù)會在成功登錄或者創(chuàng)建賬戶后返回。
在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登錄
存儲了用戶數(shù)據(jù)之后蔬顾,我們就可以用Resume Token進(jìn)行登錄了宴偿。
在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中的三個憑證窄刘,然后調(diào)用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>
);
}
});
你會發(fā)現(xiàn)上面的代碼和RNApp/app/index.js
基本雷同舷胜。是的娩践,我們基本上就是把整個現(xiàn)有的app代碼移到了loggedIn.js
文件中。下一步,我們將修改RNApp/app/index.js
來使用新創(chuàng)建的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部分:登錄
我們來創(chuàng)建一些登錄用的UI穆趴。我們只創(chuàng)建email登錄用的脸爱,但是使用username登錄完全可以。
創(chuàng)建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
}
});
現(xiàn)在我們需要在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>
);
}
});
最后一步!我們將實(shí)現(xiàn)自動登錄功能化戳。如果一個用戶在其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
*/
});
一切完成!現(xiàn)在我們就能夠使用Meteor作為后端為React Native應(yīng)用提供用戶認(rèn)證点楼。它給你在Meteor Methods和Meteor Publications中提供了this.userId
扫尖。我們可以更新meteor-app/both/posts.js
文件中的addPost
方法來測試一下:
'addPost': function() {
Posts.insert({
title: 'Post ' + Random.id(),
userId: this.userId
});
},
看看userId
是不是出現(xiàn)在新創(chuàng)建的post中了?
結(jié)論
我想在這里談一下安全性的問題掠廓,也是本篇文章所沒有涉及到的换怖。當(dāng)在生產(chǎn)環(huán)境下時,用戶傳輸?shù)氖撬麄兊恼鎸?shí)數(shù)據(jù)蟀瞧,請確保啟用SSL(對于Meteor應(yīng)用來說也是一樣)沉颂。同樣,我們也沒有在客戶端做密碼的hash悦污,所以密碼是以明文的形式傳輸?shù)闹搿_@同樣對SSL提出了需求。但是這里談及密碼hash會使文章變得冗長塞关。我們會在下篇文章中談及它抬探。
你可以在Github上查看本項(xiàng)目完整代碼:
https://github.com/spencercarli/meteor-react-native-authentication