От автора: сейчас пользователи интернета ожидают персонализированного подхода. Разработчики должны учиться строить сайты, которые предоставляют персонализированный опыт, сохраняя конфиденциальность информации о пользователе. Современные веб-приложения, как правило, обладают серверным API и клиентским интерфейсом. Довольно сложно дать понять обоим концам о том, что пользователь авторизовался. В этом уроке я расскажу вам про настройку Node API и создание React UI. У нас получится регистрация пользователя, которая сохраняет конфиденциальность информации о пользователе.
В этом уроке я не буду использовать библиотеки управления состояниями типа Redux или ReduxThunk. В более надежном приложении можно было бы привлечь эти библиотеки, но это легко связывает Redux и ReduxThunk, после чего добавить выражения fetch. Для упрощения я добавлю выражения fetch в функции componentDidMount.
Установка зависимостей Node и React
Для настройки базового приложения необходимо установить следующие инструменты:
Node (8+)
npm (5+)
create-react-app (npm пакет)
express-generator (npm пакет)
Также понадобится профиль разработчика Okta. Для установки Node и npm следуйте инструкциям для своей ОС //nodejs.org/en/. Затем просто установите 2 npm пакета с помощью команды:
1 |
npm i -g create-react-app express-generator |
Теперь можно настраивать структуру базового приложения.
Структура базового приложения
Перейдите в папку приложения и создайте новую папку:
1 2 3 4 |
mkdir MembershipSample cd MembershipSample express api create-react-app client |
Будет создано 2 папки в папке MembershipSample: api и client с Node JS и Express приложением в папке api и базовым приложением React в папке client. Структура папок теперь выглядит следующим образом:
MembershipSample
—api
—client
Для упрощения откройте 2 терминала или 2 вкладки в терминале. Один терминал для папки api приложения express и другой для папки client приложения React.
По умолчанию, приложения React и Node запускаются на порту 3000. Вам понадобится API для запуска на другом порту и прокси-сервер в клиентском приложении.
В папке api откройте файл /bin/www и измените порт, на котором будет запускаться API на 3001.
1 2 3 4 5 6 |
/** * 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:
1 2 |
"name": "client", "proxy": "//localhost:3001" |
Не забудьте запустить npm install или yarn install для всех подпапок (api и client) для установки изменений.
Теперь можно запустить оба приложения с помощью команд running npm start или yarn start в соответствующей папке для API и клиентского приложения.
Добавляем приложение Okta
Если еще не успели создайте бесплатный постоянный профиль разработчика на //developer.okta.com/signup/. После регистрации кликните на Applications в верхнем меню. Далее нажмите на кнопку Add Application.
Откроется мастер создания приложения. Выберите Single-Page App и нажмите снизу Next.
На следующем экране будут настройки по умолчанию из шаблона для одностраничного приложения. Измените название приложения на что-то более понятное типа «Membership Application». Также поменяйте настройки базового URI и URI редиректа авторизации на порт 3000, так как именно на этом порту будет запущено приложение. Остальные настройки можно не менять.
Нажмите Done. После создания приложения выберите его в списке и кликните на вкладку General для просмотра основных настроек приложения.
Внизу вы увидите настройку Client ID (ваша не будет размыта). Скопируйте ее, она понадобится для приложения React. Также вам понадобится URL организации Okta, который можно увидеть в верхней левой части страницы панели. Он будет выглядеть примерно так «//dev-XXXXXX.oktapreview.com».
Добавляем аутентификацию в приложение ReactJS
После создания приложения необходимо добавить аутентификацию через Okta. Для этого нужно добавить пару npm зависимостей. Из папки client запустите:
1 |
npm install @okta/okta-react react-router-dom --save |
Если вы используете пакетный менеджер yarn, то:
1 |
yarn add @okta/okta-react react-router-dom |
В папку client/src добавьте файл app.config.js. Код файла:
1 2 3 4 5 6 |
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 будет выглядеть так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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 Router и компонент Security из Okta React SDK. Файл app.config.js импортируется как config, поэтому можно использовать значения конфига в свойствах, необходимых для компонента Security.
Передав заданные значения, вы также окружили компонент App компонентами Router и Security. Метод onAuthRequired просто говорит Okta React SDK, что когда кто-то пытается получить доступ к безопасному роуту без авторизации, его необходимо перенаправлять на страницу авторизации.
Добавляем страницы в ReactJS App
Прежде чем добавлять роуты в React app, создайте пару компонентов для обработки добавляемых роутов.
Добавьте папку components в client/src. Здесь будут храниться все ваши компоненты, это простейший способ их организации. Затем создайте папку home для компонентов домашней страницы. Сейчас компонент домашней страницы будет всего один, но в будущем их может быть больше. Добавьте в папку файл HomePage.js:
1 2 3 4 5 6 7 8 9 |
import React from 'react'; export default class HomePage extends React.Component{ render(){ return( <h1>Home Page</h1> ); } } |
Пока что, это все, что нужно для домашней страницы. Самое важное – сделать компонент HomePage классом. Сейчас в нем всего один тег h1, но он будет страницей. То есть в нем будут другие компоненты, поэтому важно, чтобы он был компонентом-контейнером.
Далее создайте папку auth в components. Здесь будут храниться все компоненты, связанные с аутентификацией. В этой папке создайте файл LoginForm.js.
Первое, что нужно отметить – для оборачивания всей формы авторизации вы будете использовать withAuth, компонент более высокого порядка из Okta React SDK. Это добавит в компонент свойство auth, что позволит обращаться к функциям isAuthenticated и redirect компонента более высокого порядка.
Код компонента LoginForm:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
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. Код файла:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
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 (который вы создадите дальше), который будет обрабатывать регистрацию. После завершения регистрации через Node API компонент авторизует нового пользователя, и метод render (когда видит токен сессии в состоянии) перенаправляет пользователя на домашнюю страницу приложения.
Вы можете обратить внимание на свойство sessionToken в состоянии компонента. Его устанавливает функция handleSubmit() для обработки авторизации, если регистрация прошла успешно. Свойство использует метод render() для перенаправления после завершения авторизации и получения токена.
Добавляем роуты в React app
Сперва добавьте компонент навигации для будущих роутов. В client/src/components создайте папку shared. Здесь будут храниться все компоненты, используемые в нескольких местах в приложении. В этой папке создайте файл Navigation.js. Файл будет хранить базовый компонент со ссылками на все страницы приложения.
Компонент навигации необходимо обернуть в компонент более высокого порядка withAuth. Это позволит проверять аутентификацию пользователя и показывать кнопки входа или выхода.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
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. Конечная версия:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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> ); } } |
Здесь стоит отметить пару моментов. Импорт SecureRoute и ImplicitCallback компонентов из Okta React SDK. Компонент ImplicitCallback обрабатывает колбек из потока аутентификации и проверяет, есть ли конечная точка внутри приложения React для отлова обратного вызова из Okta. Компонент SecureRoute позволяет обезопасить роут и перенаправить не прошедшего аутентификацию пользователя на страницу авторизации.
Компонент Route из React Routeк делает ровно то, что вы думаете: он принимает путь, на который перешел пользователь, и устанавливает компонент для обработки этого роута. SecureRoute проводит дополнительные проверки авторизации пользователя, прежде чем разрешить доступ к роуту. Если проверки не пройдены, вызывается функция onAuthRequired в index.js, перенаправляющая пользователя на страницу авторизации.
Из странностей здесь только роут для пути авторизации. Вместо простой установки компонента для обработки пути он запускает метод render, который рендерит компонент LoginPage и задает baseUrl из настроек.
Добавляем API Endpoints в Node app
Вы, возможно, помните, что Node API проводит регистрацию. Поэтому для обработки этого запроса необходимо добавить endpoint в Node app. Для этого необходимо добавить Okta Node SDK. Из папки api запустите:
1 |
npm install @okta/okta-sdk-nodejs --save |
Затем измените файл users.js в папке api/routes. Код файла:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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 и выберите Tokens.
Нажмите Create Token. Укажите имя токена, например, Membership и кликните Create Token.
Скопируйте токен в безопасное мето, он нам позже понадобится.
Создайте файл oktaClient.js в папке lib в приложении Node. Файл настроит объект Client из Okta Node SDK с помощью токена API, который вы создали:
1 2 3 4 5 6 7 8 |
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. Измените настройки роута на следующие:
1 2 |
app.use('/api', index); app.use('/api/users', users); |
Если приложение Node все еще запущено, его необходимо остановить (CTRL+C) и запустить заново (npm start), чтобы обновления вступили в силу.
Сайту необходимо прописать стили, но теперь вы можете регистрировать пользователей, проходить авторизацию под созданным пользователем и переходить в профиль авторизованного пользователя!
Автор: Lee Brandt
Источник: //www.sitepoint.com/
Редакция: Команда webformyself.