使用 Node、React 和 Okta 构建用户注册
当今的互联网用户期望获得个性化的体验。开发者必须学会开发既能提供个性化体验又能保护用户信息隐私的网站。现代 Web 应用程序往往同时拥有服务器端 API 和客户端用户界面。让两端都知晓当前登录的用户可能颇具挑战性。在本教程中,我将指导您设置一个 Node API 来驱动 React UI,并构建一个用户注册系统,以保护用户信息的私密性和个性化。
在本教程中,我不会使用任何状态管理库,例如 Redux 或 ReduxThunk。在更健壮的应用程序中,您可能需要这样做,但将 Redux 和 ReduxThunk 连接起来,然后将fetch
此处使用的语句添加为 thunk 会很容易。为了简单起见,并且为了让本文专注于添加用户管理,我将在componentDidMount
函数中添加 fetch 语句。
安装 Node 和 React 先决条件
要设置基本应用程序,请确保已安装以下基本工具:
- 节点(8+)
- npm (5+)
- create-react-app(npm 包)
- express-generator(npm 包)
您还需要一个Okta 开发者帐户。
要安装 Node 和 npm,您可以按照https://nodejs.org/en/上适合您的操作系统的说明进行操作。
然后只需使用 npm 命令行安装这两个 npm 包:
npm i -g create-react-app express-generator
现在您已准备好设置基本的应用程序结构。
搭建基础应用程序
转到您希望应用程序所在的文件夹并为其创建一个新文件夹:
mkdir MembershipSample
cd MembershipSample
express api
create-react-app client
这将在 文件夹中创建两个MembershipSample
名为api
和 的文件夹client
,其中一个文件夹中包含 NodeJS 和 Express 应用程序api
,另一个文件夹中包含一个基础 React 应用程序client
。因此,您的文件夹结构将如下所示:
- 会员样本
- API
- 客户
为了使下一部分更容易,打开两个终端或终端选项卡;一个到 express 应用程序文件夹api
,另一个到 React 应用程序文件夹client
。
默认情况下,React 应用程序和 Node 应用程序都将在开发中的端口 3000 上运行,因此您需要让 API 在不同的端口上运行,然后在客户端应用程序中代理它。
在api
文件夹中,打开/bin/www
文件并将 API 将运行的端口更改为3001
。
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3001');
app.set('port', port);
然后在客户端应用程序中设置 API 的代理,以便您仍然可以调用它/api/{resource}
并将其从端口 3000 代理到端口 3001。在client/package.json
文件中,添加proxy
以下设置name
:
"name": "client",
"proxy": "http://localhost:3001"
最后,不要忘记对每个子文件夹运行npm install
或yarn install
(api
和client
)以确保依赖项已安装。
现在,您可以通过在API 和客户端应用程序的相应文件夹中运行npm start
或来运行这两个应用程序。yarn start
添加 Okta 应用程序
如果您还没有这样做,请在https://developer.okta.com/signup/创建一个永久免费的开发者帐户。
注册完成后,点击顶部菜单中的“应用程序” 。然后点击“添加应用程序”按钮。
然后,您将进入应用程序创建向导。选择“单页应用程序”按钮,然后单击底部的“下一步” 。
在下一个屏幕上,您将看到单页应用程序模板提供的默认设置。将应用程序的名称更改为更具描述性的名称,例如“Membership Application”。此外,将基本 URI 和登录重定向 URI 设置更改为使用端口 3000,因为这是应用程序将运行的位置。其余默认设置即可。
然后点击底部的完成按钮。
创建应用程序后,从应用程序列表中选择它,然后单击“常规”选项卡以查看应用程序的常规设置。
在底部,你会看到一个客户端 ID设置(你的 ID 显然不会被模糊处理)。复制它以在你的 React 应用程序中使用。你还需要你的 Okta 组织 URL,它可以在仪表板页面的左上角找到。它可能看起来像“ https://dev-XXXXXX.oktapreview.com ”。
向 ReactJS 应用程序添加身份验证
现在应用程序已创建,通过添加几个 npm 依赖项来使用 Okta 添加身份验证。从client
文件夹运行:
npm install @okta/okta-react react-router-dom --save
或者,如果您使用yarn包管理器:
yarn add @okta/okta-react react-router-dom
client/src
在名为 的文件夹中添加一个文件app.config.js
。文件内容如下:
export default {
url: '{yourOktaDomain}',
issuer: '{yourOktaOrgUrl}/oauth2/default',
redirect_uri: window.location.origin + '/implicit/callback',
client_id: '{yourClientID}'
}
然后,设置index.js
文件以使用 React Router 和 Okta 的 React SDK。index.js
文件完成后,它将如下所示:
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { Security } from '@okta/okta-react';
import './index.css';
import config from './app.config';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
function onAuthRequired({ history }) {
history.push('/login');
}
ReactDOM.render(
<Router>
<Security issuer={config.issuer}
client_id={config.client_id}
redirect_uri={config.redirect_uri}
onAuthRequired={onAuthRequired}>
<App />
</Security>
</Router>,
document.getElementById('root')
);
registerServiceWorker();
BrowserRouter
完成后,您将从React Router 中添加组件(别名为“Router”),并Security
从 Okta 的 React SDK 中添加组件。此外,该app.config.js
文件已导入为“config”,以便您可以在组件所需的属性中使用配置值Security
。
您还将使用和组件包围该App
组件,并传入指定的值。该方法只是告诉 Okta 的 React SDK,当有人尝试访问安全路由但未登录时,将其重定向到登录页面。Router
Security
onAuthRequired
其余所有内容都来自create-react-app
您之前运行的命令。
将页面添加到 ReactJS 应用
在向 React 应用程序添加任何路由之前,请创建一些组件来处理您想要添加的路由。
components
向文件夹添加一个文件夹client/src
。所有组件都存放在这里,这也是组织它们最简单的方法。然后home
为主页组件创建一个文件夹。目前只有一个,但以后可能会有更多仅用于主页的组件。HomePage.js
向文件夹中添加一个包含以下内容的文件:
import React from 'react';
export default class HomePage extends React.Component{
render(){
return(
<h1>Home Page</h1>
);
}
}
目前,主页所需的内容就这些了。最重要的一点是将 HomePage 组件设为 Class 类型。尽管它现在只包含一个h1
标签,但它应该是一个“页面”,这意味着它很可能包含其他组件,因此,让它成为一个容器组件至关重要。
接下来,auth
在 中创建一个文件夹components
。所有与身份验证相关的组件都将存放在这里。在该文件夹中,创建一个LoginForm.js
文件。
首先要注意的是,你将使用withAuth
Okta React SDK 中的高阶组件来包装整个登录表单。这会向组件添加一个名为 的 prop auth
,从而可以访问该高阶组件上的isAuthenticated
和函数等。redirect
该组件的代码LoginForm
如下:
import React from 'react';
import OktaAuth from '@okta/okta-auth-js';
import { withAuth } from '@okta/okta-react';
export default withAuth(class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
sessionToken: null,
error: null,
username: '',
password: ''
}
this.oktaAuth = new OktaAuth({ url: props.baseUrl });
this.handleSubmit = this.handleSubmit.bind(this);
this.handleUsernameChange = this.handleUsernameChange.bind(this);
this.handlePasswordChange = this.handlePasswordChange.bind(this);
}
handleSubmit(e) {
e.preventDefault();
this.oktaAuth.signIn({
username: this.state.username,
password: this.state.password
})
.then(res => this.setState({
sessionToken: res.sessionToken
}))
.catch(err => {
this.setState({error: err.message});
console.log(err.statusCode + ' error', err)
});
}
handleUsernameChange(e) {
this.setState({ username: e.target.value });
}
handlePasswordChange(e) {
this.setState({ password: e.target.value });
}
render() {
if (this.state.sessionToken) {
this.props.auth.redirect({ sessionToken: this.state.sessionToken });
return null;
}
const errorMessage = this.state.error ?
<span className="error-message">{this.state.error}</span> :
null;
return (
<form onSubmit={this.handleSubmit}>
{errorMessage}
<div className="form-element">
<label>Username:</label>
<input
id="username" type="text"
value={this.state.username}
onChange={this.handleUsernameChange} />
</div>
<div className="form-element">
<label>Password:</label>
<input
id="password" type="password"
value={this.state.password}
onChange={this.handlePasswordChange} />
</div>
<input id="submit" type="submit" value="Submit" />
</form>
);
}
});
这里需要注意的另一件事是OktaAuth
导入的库。这是使用您之前创建的 Okta 应用程序执行登录等操作的基础库。您会注意到,OktaAuth
构造函数中创建了一个对象,该对象会传递一个 属性baseUrl
。这是您文件中发卡机构的 URL app.config.js
。该LoginForm
组件应该包含在其他组件中,因此您必须创建一个LoginPage.js
文件来包含此组件。您将withAuth
再次使用高阶组件来访问该isAuthenticated
函数。 的内容LoginPage.js
将是:
import React, { Component } from 'react';
import { Redirect } from 'react-router-dom';
import LoginForm from './LoginForm';
import { withAuth } from '@okta/okta-react';
export default withAuth(class Login extends Component {
constructor(props) {
super(props);
this.state = { authenticated: null };
this.checkAuthentication = this.checkAuthentication.bind(this);
this.checkAuthentication();
}
async checkAuthentication() {
const authenticated = await this.props.auth.isAuthenticated();
if (authenticated !== this.state.authenticated) {
this.setState({ authenticated });
}
}
componentDidUpdate() {
this.checkAuthentication();
}
render() {
if (this.state.authenticated === null) return null;
return this.state.authenticated ?
<Redirect to={{ pathname: '/profile' }} /> :
<LoginForm baseUrl={this.props.baseUrl} />;
}
});
虽然它比登录表单组件中的少一点,但这里仍然有一些重要的部分需要指出。
再次强调,您使用的是withAuth
高阶组件。对于每个需要使用 Okta 身份验证或授权流程的组件来说,这都是一个反复出现的主题。在本例中,它主要用于获取isAuthenticated
函数。该checkAuthentication()
方法在构造函数和生命周期方法中执行,componentDidUpdate
以确保在创建组件时对其进行检查,并在组件的每次后续更改中再次进行检查。
当isAuthenticated
返回 true 时,它会被设置到组件的状态中。然后在 render 方法中检查它,以决定是显示LoginForm
组件,还是重定向到用户的个人资料页面(接下来您将创建该组件)。
ProfilePage.js
现在在文件夹中创建组件auth
。组件的内容如下:
import React from 'react';
import { withAuth } from '@okta/okta-react';
export default withAuth(class ProfilePage extends React.Component {
constructor(props){
super(props);
this.state = { user: null };
this.getCurrentUser = this.getCurrentUser.bind(this);
}
async getCurrentUser(){
this.props.auth.getUser()
.then(user => this.setState({user}));
}
componentDidMount(){
this.getCurrentUser();
}
render() {
if(!this.state.user) return null;
return (
<section className="user-profile">
<h1>User Profile</h1>
<div>
<label>Name:</label>
<span>{this.state.user.name}</span>
</div>
</section>
)
}
});
这里的组件withAuth
允许你访问该getUser
函数。它在这里被调用,componentDidMount
这是一个常用的地方,用于提取render
方法中需要用到的数据。你可能看到的唯一奇怪的事情是,该render
方法的第一行在异步调用实际返回用户之前什么都不会渲染getUser
。一旦用户处于该状态,它就会渲染个人资料内容,在本例中只是显示当前登录用户的姓名。
接下来,您将添加一个注册组件。这可以像登录表单一样完成,其中一个LoginForm
组件包含在LoginPage
组件中。为了演示另一种显示方式,您只需创建一个RegistrationForm
将作为主容器组件的组件。在文件夹中创建一个包含以下内容的RegistrationForm.js
文件:auth
import React from 'react';
import OktaAuth from '@okta/okta-auth-js';
import { withAuth } from '@okta/okta-react';
import config from '../../app.config';
export default withAuth(class RegistrationForm extends React.Component{
constructor(props){
super(props);
this.state = {
firstName: '',
lastName: '',
email: '',
password: '',
sessionToken: null
};
this.oktaAuth = new OktaAuth({ url: config.url });
this.checkAuthentication = this.checkAuthentication.bind(this);
this.checkAuthentication();
this.handleSubmit = this.handleSubmit.bind(this);
this.handleFirstNameChange = this.handleFirstNameChange.bind(this);
this.handleLastNameChange = this.handleLastNameChange.bind(this);
this.handleEmailChange = this.handleEmailChange.bind(this);
this.handlePasswordChange = this.handlePasswordChange.bind(this);
}
async checkAuthentication() {
const sessionToken = await this.props.auth.getIdToken();
if (sessionToken) {
this.setState({ sessionToken });
}
}
componentDidUpdate() {
this.checkAuthentication();
}
handleFirstNameChange(e){
this.setState({firstName:e.target.value});
}
handleLastNameChange(e) {
this.setState({ lastName: e.target.value });
}
handleEmailChange(e) {
this.setState({ email: e.target.value });
}
handlePasswordChange(e) {
this.setState({ password: e.target.value });
}
handleSubmit(e){
e.preventDefault();
fetch('/api/users', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(this.state)
}).then(user => {
this.oktaAuth.signIn({
username: this.state.email,
password: this.state.password
})
.then(res => this.setState({
sessionToken: res.sessionToken
}));
})
.catch(err => console.log);
}
render(){
if (this.state.sessionToken) {
this.props.auth.redirect({ sessionToken: this.state.sessionToken });
return null;
}
return(
<form onSubmit={this.handleSubmit}>
<div className="form-element">
<label>Email:</label>
<input type="email" id="email" value={this.state.email}
onChange={this.handleEmailChange}/>
</div>
<div className="form-element">
<label>First Name:</label>
<input type="text" id="firstName" value={this.state.firstName}
onChange={this.handleFirstNameChange} />
</div>
<div className="form-element">
<label>Last Name:</label>
<input type="text" id="lastName" value={this.state.lastName}
onChange={this.handleLastNameChange} />
</div>
<div className="form-element">
<label>Password:</label>
<input type="password" id="password" value={this.state.password}
onChange={this.handlePasswordChange} />
</div>
<input type="submit" id="submit" value="Register"/>
</form>
);
}
});
这个组件看起来与之前的组件非常相似,LoginForm
只是它调用了 Node API(稍后您将构建该 API)来处理注册。一旦 Node API 完成注册,组件就会登录新创建的用户,并且 render 方法(当它在 state 中看到会话令牌时)会将用户重定向到应用程序的主页。
您可能还会注意到sessionToken
组件状态的属性。该函数设置该handleSubmit()
属性是为了在注册成功时处理登录。然后,该render()
方法还会在登录完成并收到令牌后使用它来执行重定向。
向 React 应用添加路由
首先,为要添加的路由添加一个导航组件。在client/src/components
文件夹中,添加一个名为 的文件夹shared
。应用程序中多个位置使用的所有组件都将存放在这里。在该新文件夹中,添加一个名为 的文件Navigation.js
。该文件包含一个基本组件,其中包含指向应用程序中所有页面的链接。
您需要将导航组件包装在withAuth
高阶组件中。这样,您就可以检查是否存在经过身份验证的用户,并根据需要显示登录或注销按钮。
import React from 'react';
import { Link } from 'react-router-dom';
import { withAuth } from '@okta/okta-react';
export default withAuth(class Navigation extends React.Component {
constructor(props) {
super(props);
this.state = { authenticated: null };
this.checkAuthentication = this.checkAuthentication.bind(this);
this.checkAuthentication();
}
async checkAuthentication() {
const authenticated = await this.props.auth.isAuthenticated();
if (authenticated !== this.state.authenticated) {
this.setState({ authenticated });
}
}
componentDidUpdate() {
this.checkAuthentication();
}
render() {
if (this.state.authenticated === null) return null;
const authNav = this.state.authenticated ?
<ul className="auth-nav">
<li><a href="javascript:void(0)" onClick={this.props.auth.logout}>Logout</a></li>
<li><Link to="/profile">Profile</Link></li>
</ul> :
<ul className="auth-nav">
<li><a href="javascript:void(0)" onClick={this.props.auth.login}>Login</a></li>
<li><Link to="/register">Register</Link></li>
</ul>;
return (
<nav>
<ul>
<li><Link to="/">Home</Link></li>
{authNav}
</ul>
</nav>
)
}
});
现在您已经有了可用于处理所有路由的组件,接下来创建与之配合的路由。更新App.js
文件,使最终版本如下所示:
import React, { Component } from 'react';
import { Route } from 'react-router-dom';
import { SecureRoute, ImplicitCallback } from '@okta/okta-react';
import Navigation from './components/shared/Navigation';
import HomePage from './components/home/HomePage';
import RegistrationForm from './components/auth/RegistrationForm';
import config from './app.config';
import LoginPage from './components/auth/LoginPage';
import ProfilePage from './components/auth/ProfilePage';
import './App.css';
export default class App extends Component {
render() {
return (
<div className="App">
<Navigation />
<main>
<Route path="/" exact component={HomePage} />
<Route path="/login" render={() => <LoginPage baseUrl={config.url} />} />
<Route path="/implicit/callback" component={ImplicitCallback} />
<Route path="/register" component={RegistrationForm} />
<SecureRoute path="/profile" component={ProfilePage} />
</main>
</div>
);
}
}
这里有几点需要注意。首先,需要从 Okta 的 React SDK导入SecureRoute
和组件。该组件负责处理身份验证流程的回调,以确保 React 应用程序中有一个端点可以捕获来自 Okta 的返回调用。此外,该组件还允许您保护任何路由,并将未经身份验证的用户重定向到登录页面。ImplicitCallback
ImplicitCallback
SecureRoute
React Router 中的组件Route
正如您所期望的那样:它获取用户导航到的路径,并设置一个组件来处理该路由。该SecureRoute
组件会进行额外的检查,以确保用户在允许访问该路由之前已登录。如果没有登录,则会调用onAuthRequired
in 函数index.js
强制用户进入登录页面。
这里唯一看起来很奇怪的地方是登录路径的路由。它不是简单地设置一个组件来处理路径,而是运行一个render
方法来渲染LoginPage
组件并从配置中设置 baseUrl。
将 API 端点添加到 Node 应用
您可能还记得 Node API 负责注册,因此您需要将端点添加到 Node 应用来处理该调用。为此,您需要添加 Okta 的 Node SDK。从“api”文件夹运行:
npm install @okta/okta-sdk-nodejs --save
然后,您将更改users.js
中的文件api/routes
。该文件将如下所示:
const express = require('express');
const router = express.Router();
const oktaClient = require('../lib/oktaClient');
/* Create a new User (register). */
router.post('/', (req, res, next) => {
if (!req.body) return res.sendStatus(400);
const newUser = {
profile: {
firstName: req.body.firstName,
lastName: req.body.lastName,
email: req.body.email,
login: req.body.email
},
credentials: {
password: {
value: req.body.password
}
}
};
oktaClient.createUser(newUser)
.then(user => {
res.status(201);
res.send(user);
})
.catch(err => {
res.status(400);
res.send(err);
})
});
module.exports = router;
这里最需要注意的是导入lib/oktaClient
(稍后您将添加)、对createUser
函数的调用oktaClient
以及对象的形状newUser
。对象的形状newUser
记录在 Okta 的 API 文档 中。
要让你的 Node 应用程序调用 Okta 应用程序,你需要一个 API 令牌。要创建令牌,请进入 Okta 开发者面板,将鼠标悬停在 API 菜单选项上,然后点击“令牌”。
然后点击“创建令牌”。为令牌命名,例如“Membership”,然后点击“创建令牌”。
将令牌复制到安全位置以供稍后使用。
在 Node 应用程序中oktaClient.js
名为 的新文件夹中创建一个名为 的文件。该文件将使用您刚刚创建的 API 令牌从 Okta 的 Node SDK 配置一个对象,如下所示:lib
Client
const okta = require('@okta/okta-sdk-nodejs');
const client = new okta.Client({
orgUrl: '{yourOktaDomain}',
token: '{yourApiToken}'
});
module.exports = client;
app.js
在Node 应用根目录下的文件中,更新该文件,使所有调用都路由到/api/<something>
。您将在语句块下方看到一个部分app.use
。将路由设置更改为如下所示:
app.use('/api', index);
app.use('/api/users', users);
如果您的 Node 应用程序仍在运行,您将需要停止该应用程序(使用 CTRL+C)并重新运行它(使用npm start
)以使更新生效。
尽管该网站仍然需要一些严肃的风格爱,但您现在可以注册用户,使用新创建的用户登录并获取已登录用户的个人资料以显示在个人资料页面上!
了解更多
如果您想了解有关本文中使用的技术的更多信息,可以查看以下文档:
另外,请查看其他使用 Okta 进行身份验证的文章:
- Randall Degges 在Simple Node 网站上发表的关于 Okta 的文章
- 我的文章在 React 中使用 Okta 登录小部件
- Matt Raible 关于渐进式 Web 应用的文章
与往常一样,如果您对本文有任何疑问、意见或疑虑,可以在下方留言,发送电子邮件至lee.brandt@okta.com ,或在开发者论坛上提问。如需更多文章和教程,请在 Twitter 上关注我们@OktaDev。
文章来源:https://dev.to/oktadev/build-user-registration-with-node-react-and-okta-3944