RN 的官网并没有专门处理用户登陆的 doc。对于 IOS,已经有大神写了一个 cookie 的插件:react-cookie,然而并不支持 android(⊙o⊙)…于是便手动搭建一套简易的用户登陆以及 maintain auth 的流程。大概的思路是登陆后生成一个 token 并把它传回。RN 拿到 token 后存在本地,之后每一次请求都会在 header 中携带此 token,如果 auth fail 或者 token 不存在则清掉本地数据,并跳回登陆页。
后台用的是 Django Restful API。之前没有加上 auth 的服务,这里快速记录一下。
首先在 settings.py 中引入 auth 模块和 auth 的配置。这里有三种 auth 的模式可以选择:
最后就选择了 token。有兴趣的同学可以研究一下 OAuth2,其由于有 refresh token 和 expiry date 更为安全。
INSTALLED_APPS = (
'rest_framework.authtoken',
)
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',),
}
加好后需要 python manage.py syncdb 一下。Django 版本小于 1.7 的需要用Southmigrate,具体请参考 South 的 doc。完成后会生成与 User 关联的 Auth table。
接着在 serializers.py 中添加 User 的 serializer:
class userSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('username', 'email')
然后在你需要 auth 的 API 上加上 auth 的 decorators,例如下面:
@api_view(['POST'])
@authentication_classes((TokenAuthentication,))
@permission_classes((IsAuthenticated,))
def createComment(request, task_id=None):
created_time = datetime.datetime.now()
request.data['time'] = created_time
serializer = commentSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@authentication_classes 中的参数是 auth 的类型,@permission_classes用来与 request 中的 token 进行 authentication。
最后在 urls.py 中加上:
urlpatterns += patterns('',
(r'^api/token-auth$', views.obtain_auth_token),
)
这个 url 是 RESTFul 自带的 url,用来 generate token。只要 auth pass 就会返回一个 token 的 JSON。
这里存取 token 用的是 RN 的 AsyncStorage,这里是其官方 doc。引用原文:AsyncStorage is a simple, asynchronous, persistent, key-value storage system that is global to the app. It should be used instead of LocalStorage.
我们先在顶层 component 中加入 token 存取的方法,这里我加在 navigator 的 component 中。直接贴项目里面 code:
nav.js
const DRAWER_REF = 'drawer';
const NAV_NAME = {
todo: "TODO",
done: "DONE",
detail: "Detail",
newTodo: "New Todo",
signIn: "Sign In",
};
var NavBar = React.createClass({
getInitialState: function() {
return ({
title: null,
token: '',
});
},
getToken: async function() {
var new_token = await AsyncStorage.getItem('token');
this.setState({token: new_token});
},
updateToken: async function(value) {
await AsyncStorage.setItem('token', 'Token ' + value);
},
removeToken: async function() {
await AsyncStorage.removeItem('token');
},
authFail: async function() {
await this.removeToken();
await this._navigator.push({
name: NAV_NAME.signIn,
});
this.refs[DRAWER_REF].closeDrawer();
},
renderScene: function(router, navigator){
var Component = null;
this._navigator = navigator;
switch(router.name){
case NAV_NAME.done:
Component = DonePage;
break;
case NAV_NAME.todo: //default view
Component = TodoPage;
break;
case NAV_NAME.detail:
Component = DetailPage;
break;
case NAV_NAME.newTodo:
Component = NewTodoPage;
break;
case NAV_NAME.signIn:
Component = SignInPage;
break;
}
return <Component
navigator={navigator}
todo={router.todo}
token={this.state.token}
getToken={this.getToken}
updateToken={this.updateToken}
authFail={this.authFail}/>
},
...
switchNav: function(name) {
this.setState({title: name});
this._navigator.push({name: this.state.title});
this.refs[DRAWER_REF].closeDrawer();
},
...
navView: function() {
return (
<View style = {styles.nav}>
<View style = {styles.header}>
<Text style = {styles.headerText}>ANDWARD</Text>
<Text style = {styles.headerText}>.TODO</Text>
</View>
<View style = {styles.item}>
<Text style = {styles.text}
onPress= {() => {this.switchNav(NAV_NAME.todo);}}
>TODO</Text>
</View>
<View style = {styles.item}>
<Text style = {styles.text}
onPress= {() => {this.switchNav(NAV_NAME.done);}}
>DONE</Text>
</View>
<View style = {styles.item}>
<Text style = {styles.text}>SETTINGS</Text>
</View>
<View style = {styles.item}>
<Text style = {styles.text}
onPress= {() => {this.authFail();}}
>Log Out</Text>
</View>
</View>);
},
render: function() {
var title = this.state.title ? this.state.title : NAV_NAME.todo;
return (
<DrawerLayoutAndroid
ref={DRAWER_REF}
drawerWidth={Dimensions.get('window').width - 56}
drawerPosition={DrawerLayoutAndroid.positions.Left}
renderNavigationView={this.navView}>
<View style = {styles.container}>
<ToolbarAndroid
navIcon={{uri: apiList.resourceWrapper('menu_white.png')}}
title={title}
titleColor="white"
style={styles.toolbar}
onIconClicked={() => this.refs[DRAWER_REF].openDrawer()}
onActionSelected={this.onActionSelected} />
<Navigator
initialRoute={{name: NAV_NAME.todo}}
configureScene={this.configureScene}
renderScene={this.renderScene} />
</View>
</DrawerLayoutAndroid>
);
}
});
这里定义了存取删 token 的三个函数:getToken,updateToken,removeToken。注意所有的
AsyncStorage method 都是异步的,需要用 ES6 的 async/await,这三个函数都作为参数传入 compnent 中。
sign.js
var SignInPage = React.createClass({
getInitialState: function() {
return {
username: false,
password: false,
};
},
signIn: function() {
fetch(apiList.SIGN_IN, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: this.state.username,
password: this.state.password,
})
})
.then((response) => {
if (response.status >= 200 && response.status < 300) {
return response.json();
} else {
this.clearInput();
}
})
.then((repsonseData) => {
this.props.updateToken(repsonseData.token);
this.props.navigator.push({
name: 'TODO'
});
});
},
...
});
注:this.props.updateToken 用来存 generate 的 token。
todo.js
var TodoPage = React.createClass({
getInitialState: function() {
return {
dataSource: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
}),
};
},
componentDidMount: async function() {
await this.props.getToken();
await this.fetchData();
},
fetchData: function() {
fetch(apiList.TODO_API, {
headers: {
'Authorization': this.props.token,
}
})
.then((response) => {
if (response.status >= 200 && response.status < 300) {
return response.json();
} else {
this.props.authFail();
}
})
.then((responseData) => {
this.setState({
dataSource: this.state.dataSource.cloneWithRows(responseData),
loaded: true,
});
});
},
});
this.props.getToken() 从 storage 里面拿到 generate 的 token 并在顶层 component 中 setState。这样所有的 navigator 都可以通过 this.props.token 拿到 token,并用于请求。在 fetch 的 headers 中加入:'Authorization': this.props.token,server 端就可以对 request 进行 token authentication 了。