使用Node,React和Okta构建用户注册


现在的互联网用户期待个性化的体验。开发人员必须学会开发提供个性化体验的网站,同时保持用户信息的私密性。现代web应用程序也倾向于具有服务器端API和客户端用户界面。让两端都知道当前登录的用户可能是一个挑战。在本教程中,我将指导您设置一个节点API,该API提供给React UI,并构建一个用户注册,使用户的信息保持私有和个人信息。

在本教程中,我不会使用任何像Redux或reduxthunk这样的状态管理库。在一个更健壮的应用程序中,您可能希望这样做,但是将Redux和ReduxThunk连接起来,然后添加fetch这里用作你的thunks的语句。为了简单起见,也为了使本文的重点放在添加用户管理上,我将把fetch语句添加到componentDidMount功能。

安装节点和响应先决条件

要设置基本应用程序,请确保安装了以下基本工具:

  • 节点(8+)
  • 国家预防机制(5+)
  • create-react-app(npm包)
  • express-generator(npm包)

你还需要一个Okta developer account

要安装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调用的文件夹apiclient中的NodeJS和Express应用程序api文件夹中的基本React应用程序client文件夹。因此您的文件夹结构如下所示:

  • MembershipSample
    • API
    • 客户

为了使下一部分更容易,打开两个终端或终端标签;一个指向express app文件夹api另一个指向React app文件夹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对于每个子文件夹(apiclient)以确保已安装依赖项。

现在,您可以通过运行以下两个应用程序来运行npm start或者yarn start在API和客户端应用程序的适当文件夹中。

添加Okta应用程序

如果您还没有这样做,请在以下位置创建一个免费的永久开发人员帐户https://developer.okta.com/signup/

注册后,点击顶部菜单中的Applications。然后单击Add Application按钮。

然后您将被带到应用程序创建向导。选择单页App按钮,点击底部的Next。

在下一个屏幕上,您将看到单页应用程序模板提供的默认设置。将应用程序的名称更改为更具描述性的名称,如“成员资格应用程序”。另外,将基本URI和登录重定向URI设置更改为使用端口3000,因为应用程序将在该端口运行。其余的默认设置都可以。

然后单击底部的Done按钮。

创建应用程序后,从applications列表中选择它,然后单击General选项卡以查看应用程序的常规设置。

在底部,您将看到一个客户端ID设置(显然,您的设置不会被模糊掉)。复制它以在您的React应用程序中使用。您还需要您的Okta组织URL,您可以在仪表板页面的左上方找到该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’ folder called app.config.js`.该文件的内容为:

export default {
  url: '{yourOktaDomain}',
  issuer: '{yourOktaOrgUrl}/oauth2/default',
  redirect_uri: window.location.origin + '/implicit/callback',
  client_id: '{yourClientID}'
}


然后,设置index.js文件来使用React路由器和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组件(别名为“Router”),以及React路由器的Security来自OKTA的React SDK的组件。还认为app.config.js文件作为“config”导入,以便您可以在属性中使用Security组件。

您还将包围App组件的RouterSecurity组件,传入指定的值。这个onAuthRequired方法,简单地告诉OKTA的React SDK,当有人试图访问安全路由而他们没有登录时,将他们重定向到登录页面。

其他一切都将来自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>
    );
  }
}


这是所有您真正需要的主页此刻。最重要的一点是使主页组件成为类类型。即使现在它只包含一个h1标记,它意味着一个“页面”,这意味着它可能包含其他组件,因此它是一个容器组件是很重要的。

接下来,创建一个auth文件夹在components。这是所有与身份验证有关的组件的位置。在该文件夹中,创建LoginForm.js文件。

首先要注意的是,您将使用withAuthOkta的React SDK中的高级组件来包装整个登录表单。这将向名为auth,从而可以访问诸如isAuthenticatedredirect函数。

的代码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传给它。这是您的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组件,唯一的例外是它调用节点API(您将在稍后构建该API)来处理注册。一旦节点API完成注册,组件将新创建的用户登录进来,render方法(当它看到状态中的会话令牌时)将用户重定向到应用程序的主页。

您可能还会注意到sessionToken属性。这是由handleSubmit()函数,用于在注册成功时处理登录。则它也由render()方法在登录完成并且接收到令牌后执行重定向。

向React应用程序添加路由

首先,为将要添加的路线添加一个导航组件。在client/src/components文件夹,添加一个名为shared。这将是应用程序中几个地方使用的所有组件的位置。在新文件夹中,添加一个名为Navigation.js该文件包含一个基本组件,与应用程序中所有页面的链接。

您需要将导航组件包装在withAuth高阶分量。这样,您就可以检查是否存在经过身份验证的用户,并根据需要显示login或logout按钮。

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>
    );
  }
}


这里有几件事值得注意。的导入SecureRouteImplicitCallback来自OKTA的React SDK的组件。这个ImplicitCallback组件处理来自身份验证流的回调,以确保React应用程序中有一个端点来捕获来自OKTA的返回调用。这个SecureRoute组件允许您保护任何路由,并将未经身份验证的用户重定向到登录页。

Route组件执行您所期望的操作:它采用用户导航到的路径,并设置一个组件来处理该路径。这个SecureRoute组件执行额外的检查,以确保用户在允许访问该路由之前已登录。如果它们不是,则onAuthRequired中的函数index.js以强制用户进入登录页。

这里唯一另一个看起来很奇怪的地方是登录路径的路由。它不是简单地设置一个组件来处理路径,而是运行一个render方法,该方法呈现LoginPage组件,并从配置中设置baseUrl。

将API端点添加到节点应用程序

您可能还记得Node API正在进行注册,因此需要将端点添加到Node app以处理该调用。为此,您需要添加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对象已记录in Okta’s API documentation

对于要调用Okta应用程序的节点应用程序,它将需要一个API令牌。要创建一个,请进入Okta开发人员仪表板,将鼠标悬停在API菜单选项上,然后单击Tokens。

然后单击Create token。给令牌起一个类似“Membership”的名字,然后单击Create token。

将令牌复制到安全位置,以便以后使用。

创建一个名为oktaClient.js在名为lib在节点应用程序中。该文件将配置Client使用刚才创建的API令牌从Okta的Node SDK中创建如下所示的对象:

const okta = require('@okta/okta-sdk-nodejs');

const client = new okta.Client({
  orgUrl: '{yourOktaDomain}',
  token: '{yourApiToken}'
});

module.exports = client;


app.js文件,更新该文件,使所有调用路由到/api/<something>。你会看到下面的一个部分app.use声明。更改路由设置,使其看起来像这样:

app.use('/api', index);
app.use('/api/users', users);


如果您的节点应用程序仍在运行,您将需要停止应用程序(使用Ctrl+C)并重新运行它(使用npm start)以使更新生效。

即使网站仍然需要一些严肃的风格爱,你现在将能够注册用户,登录与新创建的用户和获得登录用户的配置文件显示在配置文件页面!

了解更多信息

如果您想了解本文中使用的技术的更多信息,可以查看以下文档:

另外,查看使用Okta进行身份验证的其他文章:

一如既往,如果您对文章有问题,评论或担忧,您可以在下面发表评论,发电子邮件给我,或将您的问题发到developer forums。欲了解更多文章和教程,请访问Twitter@OktaDev