React Native Android - 简单的用户登陆和auth处理(Django RESTful)

05 Jan 2016

RN的官网并没有专门处理用户登陆的doc。对于IOS,已经有大神写了一个cookie的插件:react-cookie,然而并不支持android(⊙o⊙)…于是便手动搭建一套简易的用户登陆以及maintain auth的流程。大概的思路是登陆后生成一个token并把它传回。RN拿到token后存在本地,之后每一次请求都会在header中携带此token,如果auth fail或者token不存在则清掉本地数据,并跳回登陆页。

Server端Auth认证

后台用的是Django Restful API。之前没有加上auth的服务,这里快速记录一下。

首先在settings.py中引入auth模块和auth的配置。这里有三种auth的模式可以选择:

  • BasicAuthentication是最基本的auth认证,需要每次都提供username和pwd,故不选择。
  • SessionAuthentication最好是RN端有cookie机制,故也不选择。
  • TokenAuthentication利用token key送到sever端与value匹配。

最后就选择了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)

@authenticationclasses中的参数是auth的类型,@permissionclasses用来与request中的token进行authentication。

最后在urls.py中加上:

urlpatterns += patterns('',
(r'^api/token-auth$', views.obtain_auth_token),
)

这个url是RESTFul自带的url,用来generate token。只要auth pass就会返回一个token的JSON。

RN端搭建

这里存取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.

Token存取

我们先在顶层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=
            title={title}
            titleColor="white"
            style={styles.toolbar}
            onIconClicked={() => this.refs[DRAWER_REF].openDrawer()}
            onActionSelected={this.onActionSelected} />
          <Navigator 
          initialRoute=
          configureScene={this.configureScene}
          renderScene={this.renderScene} />
          </View>
          </DrawerLayoutAndroid>
        );
    }
});

这里定义了存取删token的三个函数:getToken,updateToken,removeToken。注意所有的 AsyncStorage method都是异步的,需要用ES6的async/await,这三个函数都作为参数传入compnent中。

登录页面的验证以及获取token:

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了。

稍后附上效果图~

comments powered by Disqus