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