使用JWT(JSON Web Tokens)实现React原生应用权限验证

  本文演示如何使用JSON Web Tokens对一个React Native应用进行权限验证,我们使用 this Auth0 sample API作为我们的应用后端。

设置和安装

首先需要准备基础环境,保证Node.js安装,如果你是OS X还需要安装 Xcode ,本文以OS X为基础环境。你需要按照React Native安装手册安装准备好基本目录,通过react-native run-ios运行你的起步项目,同时确保你的iOS模拟器已经能够运行。

下载克隆 this Auth0 sample API这个后端,部署到Node.js中,能够在本地运行。

我们也可以下载Tcomb’s Form Library以便容易加入表单到我们的app,可以通过命令npm install tcomb-form-native安装。

 

使用JWT验证我们的应用app

现在我们的后端已经下载并安装在本地,让我们浏览器访问:http://localhost:3001/api/random-quote,应该可以正常访问,返回200 OK。

下面开始布置我们的应用,现在开始我们的React Native app,下面是官方教程的一部分源码(见documentation ) ,源码是这样的:

/** 
 * Sample React Native App 
 * https://github.com/facebook/react-native 
 * @flow 
 */ 
import React, { Component } from  'react';
import {
  AppRegistry,
  StyleSheet,
  Text,
  View
} from  'react-native';
class  AwesomeProject  extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          Welcome to React Native!
        </Text>
        <Text style={styles.instructions}>
          To get started, edit index.ios.js
        </Text>
        <Text style={styles.instructions}>
          Press Cmd+R to reload,{ '\n'}
          Cmd+D or shake  for dev menu
        </Text>
      </View>
    );
  }
}
const styles = StyleSheet.create({
  container: {
、    flex:  1,
    justifyContent:  'center',
    alignItems:  'center',
    backgroundColor:  '#F5FCFF',
  },
  welcome: {
    fontSize:  20,
    textAlign:  'center',
    margin:  10,
  },
  instructions: {
    textAlign:  'center',
    color:  '#333333',
、    marginBottom:  5,
  },
});
AppRegistry.registerComponent( 'AwesomeProject', () => AwesomeProject);

 

让我们稍微修改一下这个源码,首先到前面头部,需要引入react和react-native两个,同时引入tcomb 库包:

var React = require( 'react');
var ReactNative = require( 'react-native');
var t = require( 'tcomb-form-native');
var {
    AppRegistry,
    AsyncStorage,
    StyleSheet,
    Text,
    View,
    TouchableHighlight,
    AlertIOS,
} = ReactNative;

然后把原来的样式置换出来,用下面的样式替代,注册我们自己的组件,这些代码会在应用最后面,是在所有函数和render以后:

var styles = StyleSheet.create({
    container: {
        justifyContent:  'center',
        marginTop:  50,
        padding:  20,
        backgroundColor:  '#ffffff',
    },
    title: {
        fontSize:  30,
        alignSelf:  'center',
        marginBottom:  30 
    },
    buttonText: {
        fontSize:  18,
        color:  'white',
        alignSelf:  'center' 
    },
    button: {
        height:  36,
        backgroundColor:  '#48BBEC',
        borderColor:  '#48BBEC',
        borderWidth:  1,
        borderRadius:  8,
        marginBottom:  10,
        alignSelf:  'stretch',
        justifyContent:  'center' 
    },
});
AppRegistry.registerComponent( 'AwesomeProject', () => AwesomeProject);

 

现在所有应用设置都已经完成了,现在我们需要增加用户名和密码登录表单了,用户能够选择登入或登出,一旦登入成功,就能按按钮进行上述应用的正常查询访问,如果是登出状态,访问该查询URL会得到错误结果。

 

JSON Web Tokens和AsyncStorage

我们是使用JSON Web Tokens对我们这个React native应用进行验证,当用户登入以后,后端API的响应将是一个JWT,任何访问受保护的URL都必须附上这个通过安全验证的标记Token,这样我们会在客户端需要一个保存后端发给我们的通过验证的JWT标记,以便每次发出请求时都能附上这个标记,这里我们使用AsyncStorage保存JWT这个标记。

在这个应用中,我们需要三个主要方法:

1. 一个方法名叫_userSignup,将会POST提交请求到后端端点/users,该请求提供一个用户名和密码,如果用户并不存在,它会将创建新的,一个JWT会在当前会话返回。

2.一个方法名_userLogin,会提交POST到后端/sessions/create,这个提交的请求再次带有用户名和密码,如果成功,会返回一个JWT。

3.最后,需要一个业务方法:_getProtectedQuote,查询受权限保护的资源,也就是端点api/protected/random-quote,提交的请求是GET方式,请求中包含本次会话的JWT,后端验证了这个JWT后会返回正常业务数据。

上述源码已经在 try it out 。我们解释一下,首先开始于:

var STORAGE_KEY =  'id_token';
var Form = t.form.Form;
var Person = t.struct({
    username: t. String,
    password: t. String 
});
const options = {};

 

第一行有一个STORAGE_KEY 变量,是用于隐藏加密我们的key,也就是id_token,然后设置tcombs表单库包,Person表单是由username和password组成,这是两个都需要的字段,都是字符串,我们不会增加另外选项,尽管我们能够拓展或分离登录或注册两个表单。

下面是存储JWT到应用的验证用户:

var AwesomeProject = React.createClass({
    async _onValueChange(item, selectedValue) {
        try {
            await AsyncStorage.setItem(item, selectedValue);
        }  catch (error) {
            console.log( 'AsyncStorage error: ' + error.message);
        }
    },

 

这个方法_onValueChange是在AsyncStorage中条目发生改变时被调用,它会被传入条目和值作为方法参数,当值改变或使用sets时会变化。

下面是通过JWT验证后,返回正常业务查询结果。

async  _getProtectedQuote() {
    var DEMO_TOKEN = await AsyncStorage.getItem(STORAGE_KEY);
    fetch( "http://localhost:3001/api/protected/random-quote", {
        method:  "GET",
        headers: {
            'Authorization':  'Bearer ' + DEMO_TOKEN
        }
    })
    .then((response) => response.text())
    .then((quote) => { 
        AlertIOS.alert(
        "Chuck Norris Quote:", quote)
    })
    .done();
},

这里弹出一个窗口Chuck Norris Quote表示这是一个正常业务查询结果。

_getProtectedQuote首先调用已经存储的JWT,一个id_token,如果存在会发出GET请求到后端,这是通过fetch方式实现,这里将把JWT放入GET请求的头部,头部中包含Authorization,其值是后端已经验证通过并保存在AsyncStorage中验证记号token,如果后端验证这个记号通过后,会发出给我们正常的业务响应,也就是弹出一个窗口显示查询结果。

那么关键问题来了,我们保存在AsyncStorage中的JWT是哪里来的呢?这是我们在注册用户时得到的一个JWT,代码见如下:

_userSignup() {
    var value =  this.refs.form.getValue();
    if (value) {  // if validation fails, value will be null 
        fetch( "http://localhost:3001/users", {
            method:  "POST", 
            headers: {
                'Accept':  'application/json',
、                'Content-Type':  'application/json' 
            },
            body: JSON.stringify({
                username: value.username, 
                password: value.password, 
            })
        })
        .then((response) => response.json())
        .then((responseData) => {
            this._onValueChange(STORAGE_KEY, responseData.id_token),
            AlertIOS.alert(
            "Signup Success!",
            "Click the button to get a Chuck Norris quote!" 
            )
        })
        .done();
    }
},

 

_userSignup 是在点按Signup按钮时被调用的,会从表单中收集username和passwd两个值,通过POST提交请求到后端API,后端会验证这个用户名和密码,注册一个新用户,然后返回一个当前会话的JWT,最后会调用_onValueChange方法,保存这个新的验证记号。

用户登录时也会返回一个验证的JWT,如下:

_userLogin() {
    var value =  this.refs.form.getValue();
    if (value) {  // if validation fails, value will be null 
        fetch( "http://localhost:3001/sessions/create", {
            method:  "POST", 
            headers: {
                'Accept':  'application/json',
                'Content-Type':  'application/json' 
            },
            body: JSON.stringify({
                username: value.username, 
                password: value.password, 
            })
        })
        .then((response) => response.json())
        .then((responseData) => {
            AlertIOS.alert(
            "Login Success!",
            "Click the button to get a Chuck Norris quote!" 
            ),
            this._onValueChange(STORAGE_KEY, responseData.id_token)
        })
        .done();
    }
},

 

登入用户成功后,会看到一个弹出窗口,同时保存后端验证通过返回的JWT。

如果用户登出退出,我们应该删除保存的JWT:

async  _userLogout() {
    try {
        await AsyncStorage.removeItem(STORAGE_KEY);
        AlertIOS.alert( "Logout Success!")
    }  catch (error) {
        console.log( 'AsyncStorage error: ' + error.message);
    }
},

 

好了,以上JWT的工作已经完成。

现在是输出一个登录页面让用户输入用户名和密码,页面有Signup按钮和Login按钮以及业务查询按钮:

render()
    {
       return (
        <View style={styles.container}>
            <View style={styles.row}>
                <Text style={styles.title}>Signup/Login below  for Chuck Norris Quotes!</Text
            </View>
            <View style={styles.row}>
                <Form
                    ref= "form" 
                    type={Person}
                    options={options}
                />
            </View>    
            <View style={styles.row}>
                <TouchableHighlight style={styles.button} onPress={ this._userSignup} underlayColor= '#99d9f4' > 
                    <Text style={styles.buttonText}>Signup</Text>
                </TouchableHighlight>
                <TouchableHighlight style={styles.button} onPress={ this._userLogin} underlayColor= '#99d9f4' > 
、                    <Text style={styles.buttonText}>Login</Text>
               </TouchableHighlight>
            </View>
            <View style={styles.row}>    
                <TouchableHighlight onPress={ this._getProtectedQuote} style={styles.button}>
                    <Text style={styles.buttonText}>Get a Chuck Norris Quote!</Text>
                </TouchableHighlight>
            </View>
        </View>
        );
    }
});

界面效果如下:

 

点击here 得到完整源码。

 

JSON Web Tokens(JWT)教程

React.JS专题

Javascript专题