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

  本文演示如何使用JSON Web Tokens对一个React Native应用进行权限验证,我们使用 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>
 );
 }
});

界面效果如下:

点击得到完整源码

 

 

React.JS专题

Javascript专题