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