使用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> ); } });
界面效果如下:
点击得到完整源码