- 投稿日:2021-01-24T21:59:42+09:00
フレームワークexpressのインストールと起動
初めに
以下コマンドでコンソール起動
vagrant up vagrant ssh次に
以下コマンドで Express application generatorをインストール
yarn global add express-generator@4.16.0最後に
以下コマンドでファイルでファイル作成
hoge部分はファイル名express --view=pug hoge以下コマンドでPORT=8000で起動
http://localhost:8000/ にアクセスすると開けるyarn install DEBUG=hoge:* PORT=8000 yarn start
- 投稿日:2021-01-24T21:00:26+09:00
Puppeteerで3rd-party cookieを保存・利用する
Puppeteerで3rd-party cookieを保存・利用したい場合の処理についてまとめました。ログイン処理がクロスサイトになっているWebページをPuppeteerで利用する場合など、以下で紹介するコードが必要になることがあります。
cookieのセーブ
cookieを保存するファイルをtmpdirに作る方針のコードです。別のパスに保存する場合はよしなに変更してください。
const os = require('os'); const path = require('path'); const fs = require('fs').promises; /*(中略)*/ try { const client = await page.target().createCDPSession(); const allBrowserCookies = (await client.send('Network.getAllCookies')).cookies; const cookiePath = path.join(os.tmpdir(), 'cookies.json'); await fs.writeFile(cookiePath, JSON.stringify(allBrowserCookies, null, 2)); } catch(err) { // do nothing console.log(err) }このコードのポイントはCDP (Chrome DevTools Protocol)のメソッドを呼び出してcookieを取り出している部分です。
page.cookies()
だと現在のURLに紐付いているcookieしか返してくれないので、3rd-party cookieが必要な場合は使えません。CDPのプロトコルメソッドNetwork.getAllCookies
なら全cookieを取り出せます。cookieの読み込み・利用
cookieの読み込みは特に注意点はありません。
下記コードではファイルが見つからなかったときのエラーを無視していますが、仕事のコードならもう少し真面目にエラー処理を書いた方がいいと思います。
const os = require('os'); const path = require('path'); const fs = require('fs').promises; /*(中略)*/ try { const cookiePath = path.join(os.tmpdir(), 'cookies.json'); const cookiesString = await fs.readFile(cookiePath); const cookies = JSON.parse(cookiesString); await page.setCookie(...cookies); } catch(err) { // do nothing console.log(err) }元ネタ
- 投稿日:2021-01-24T20:11:36+09:00
nodenvでNode.jsをバージョン管理
※この記事はMacOSを前提として書かれています。
※Windowsの場合は、「nodistでNode.jsをバージョン管理」を参照してください。nodenvの環境の準備
nodenvを使ってNode.jsのインストールやバージョン管理をおこないます。
nodenvのインストール
まず、ターミナルで以下のコマンドを実行します。
ターミナルgit clone https://github.com/nodenv/nodenv.git ~/.nodenv次に以下のコマンドを実行します。このコマンドで失敗しても、
nodenv
は正常に動くので大丈夫です。1
:ターミナル
cd ~/.nodenv && src/configure && make -C src
nodenv
コマンドを実行できるように、パスを通します。ターミナルecho 'export PATH="$HOME/.nodenv/bin:$PATH"' >> ~/.bash_profile
nodenv
を初期化するために、以下のコマンドを実行します。ターミナル~/.nodenv/bin/nodenv initすると次のような結果が表示されます。
# Load nodenv automatically by appending # the following to ~/.bash_profile: eval "$(nodenv init -)"この指示の通り、
~/.bash_profile
に記述を加えます。~/.bash_profileeval "$(nodenv init -)"ここでターミナルを閉じて、新たにターミナルを立ち上げます。
node-buildプラグインのインストール
以下のコマンドを順に実行して、
node-build
プラグインをインストールします。ターミナルmkdir -p "$(nodenv root)"/plugins git clone https://github.com/nodenv/node-build.git "$(nodenv root)"/plugins/node-buildNode.jsのインストール
インストール可能なNode.jsのバージョンの一覧を表示します。
ターミナルnodenv install -l以下のようにNode.jsのバージョンが表示されます。
... 14.15.0 14.15.1 14.15.2 14.15.3 14.15.4 15.0.0 15.0.1 15.1.0 15.2.0 15.2.1 15.3.0 15.4.0 15.5.0 15.5.1 15.6.0 chakracore-dev chakracore-nightly ...ここでは
14.15.4
のNode.jsをインストールします。ターミナルnodenv install 14.15.4確認のために、Node.jsのバージョンを表示してみます。
ターミナルnode -vすると、以下の結果が表示されます。
nodenv: node: command not found The `node' command exists in these Node versions: 14.15.4
nodenv: node: command not found
と表示されるのは、インストールしたNode.jsがグローバルに設定されていないからだそうです。2
nodenv global 14.15.4
を実行してから、改めてNode.jsのバージョンを確認すると、以下のように結果が表示されました。v14.15.4npmのバージョンの確認
以下のコマンドで、現在の
npm
のバージョンを確認してみます。ターミナルnpm -v
npm
のバージョンは6.14.10
でした。Node.jsのリリース一覧を確認すると、Node.js 14.15.4に対応するnpmのバージョンと一致していました。
- 投稿日:2021-01-24T18:15:52+09:00
フロントサーバ、バックサーバの分離された構成でミニマムなサービスを作る。
はじめに
本記事はモノリシックでない分離されたアーキテクチャ(マイクロサービス)について理解を深めるため、
ミニマムなアプリを作ってみたときのまとめ記事です。
※当方初心者のため、間違いありましたら是非ともご指摘お願いいたします。著者について
以下著者のスペック
- エンジニア一年目
- WebアプリといえばMVCしか知らない
- マイクロサービス?なにそれおいしいの
背景
基本情報などの勉強中、よくこんな図が出てきて混乱していました。
- Webアプリのアーキテクチャ?、MVCしかしらんけど?
- アプリのサーバって一つだけじゃないの?
- でもReactとかは独立してサーバーが立っているぽい、、
- どうやってバックエンドと連携させるんやろ、、?
このような疑問を持った私は、「わかんないなら触ってみればいいじゃん!」と意気込み、
フロントエンドとバックエンドの機能をサーバごとに切り離したミニマムなアプリを作ろうと決めました。開発
概要
画面を担当するフロントサーバとロジックを担当するバックサーバの二つのサーバを立てて、
インターネットのニュースを検索できるアプリを作る。環境
Node.js v14.15.1
- サーバサイドのJavaScript
- こちらの記事が非常にわかりやすくおすすめ
- 今回は超簡単なロジックを実装するのみに使う
Express v4.17.1
- Node.jsで動く軽量なWebフレームワーク
- Webサーバを立てるのも非常に簡単
- 今回はAPIのルーティング等に使う
Yarn v1.22.4
- npmと互換性があるパッケージ管理システム
- 一度インストールしたパッケージをキャッシュするためインストールが高速
目標
画像のような簡単なニュース検索アプリを作る。
検索条件を入れ、「Search」ボタンを押下すれば、検索条件にヒットするニュースを検索する。
構成
重要であるのはフロントサーバとバックサーバをAPIで疎結合している点です。
フロントサーバはバックサーバのAPIを呼び出し、返却されたレスポンスをもとに画面描画をします。手順
バックサーバの実装
Node.js, Expressの解説は目的ではないため、重要な部分(独自APIを実装する部分)のみ示します。
以下はExpressでサーバを起動する部分です。backend/server.js'use strict'; const express = require('express'); const app = express(); const cors = require('cors'); const dotenv = require('dotenv'); dotenv.config({path: './.env'}); const morgan = require('morgan'); // CORS(クロスオリジンリソース共有)を許可 app.use(cors()); require('./routes/news')(app); // アクセスロガーを実装 app.use(morgan('dev')); // サーバをポート3000で起動 app.listen(process.env.PORT, () => console.log('listening on port ' + process.env.PORT)); module.exports = app;ここではCORS(クロスオリジン間リソース共有)を有効にしています。
CORSについては自分もよく理解しきれていないですがMDNに以下のような説明があります。オリジン間リソース共有Cross-Origin Resource Sharing (CORS) は、追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組みです。ウェブアプリケーションは、自分とは異なるオリジン (ドメイン、プロトコル、ポート番号) にあるリソースをリクエストするとき、オリジン間 HTTP リクエストを実行します。
https://developer.mozilla.org/ja/docs/Web/HTTP/CORSこちらの記事が詳しいため参考にしてください。
次はインターネットからニュースの情報を取得する処理です。
ここではNews APIというAPIを本アプリ用にラップしています。backend/routes/news.jsconst NewsAPI = require('newsapi'); const newsapi = new NewsAPI(process.env.NEWS_API_ACCESS_KEY); const morgam = require('morgan'); const router = require('express').Router(); module.exports = (app) => { router.route('/') .get((req, res) => res.json({message: 'This is a index page.'})); router.route('/news') .get((req, res) => { newsapi.v2.topHeadlines({ // 検索条件が指定されなかった場合はデフォルトの条件を指定する。 country: req.query.country || 'jp', category: req.query.category || 'general', q: req.query.q || '', pageSize: Number(req.query.pageSize) || 30 }).then(news => res.json(news)); }); //bind access logger app.use(morgam('dev')); app.use(router); };フロントサーバの実装
こちらもReactの解説は目的でないため、重要な部分(バックサーバと通信する部分)のみ示します。
またcreate-react-appを使用してテンプレを作成しました。以下はバックサーバのAPIを呼び出し、返却されたニュース一覧のJSONを画面上の変数に渡しています。
frontend/src/App.jsconst handleSubmit = async event => { // submitボタンを押すとブラウザのデフォルトでリロードされてしまうため // デフォルトの動作をさせないよう設定する event.preventDefault(); // バックサーバのAPIを呼び出す let articlesArr = await axios.get(endPoint + '/news', { // 画面に入力された検索条件を独自APIのリクエストに乗せる params: { country: country.value, category: category.value, q: keyword, pageSize: pageSize.value } }) // データが返却されたら変数articlesArrにデータを代入する。 .then(res => res.data.articles); // 画面上の変数にデータを代入する。 setArticles(articlesArr); };完成品
ソースは以下においてあります。
https://github.com/yasuaki640/news-api-app※コードレビュー歓迎
終わりに
業務でも趣味でもモノリシックなアーキテクチャしか触ったことがなく、
ツイッター上でマイクロサービスなどの用語を理解するのに時間がかかりました。
※現在は完全に理解した程度技術理解のために実際に触れてみるのはやはり強いですね、、、
本記事がどなたかのお役に立てれば幸いです。
※間違いありましたら是非ご指摘お願いいたします。
- 投稿日:2021-01-24T18:11:32+09:00
Node.js on Raspberry Pi Zero WHのはまりどころ
概要
Raspberry Pi Zero WH上にNode.jsの実行環境を構築しました。
やりたいことはRaspberry Pi Zero上での形態素解析と画像生成です。しかしRaspberry Pi Zero WHはARMv6コア、RAM512MBと最小限のアーキテクチャです。
リッチなことをやろうとすると一筋縄ではいきません。
色々工夫して何とか実行環境を構築できました。その時のはまりどころを情報共有します。
Agenda
主に以下3つではまったので、それぞれを説明します。
1. ARMv6コア起因
2. RAM不足起因
3. ライブラリ不足起因はまったところ
1. ARMv6コアによるnpmエラー
Raspberry Pi ZeroはARMv6コアを実装しています。
v6コアは2000年代前半のアーキテクチャでちょい古です。
なので、
- apt/apt-getのインストールしてもnpmがエラーを吐く
- スクリプトを使わず手動でインストールしようにもNode.js公式のTOPではv7以降しか載っていない
よって過去のdistを掘り当てて手動コピーしないといけませんでした。
手順は以下の通り。$ wget https://nodejs.org/dist/v11.15.0/node-v11.15.0-linux-armv6l.tar.gz $ tar zxvf node-v11.15.0-linux-armv6l.tar.gz $ cd [解凍したディレクトリ] $ rm CHANGELOG.md LICENSE README.md $ sudo cp -R * /usr/local/ $ node -v $ npm -vv11.15.0を選んだのは、現状ARMv6対応してるlatest versionだからです。
(間違ってたら指摘お願いします…!)2. メモリ不足によるmecab-ipadic-neologdコンパイル失敗
最適な形態素解析を行うため、mecabの新語辞書インストールの必要がありました。
しかしインストール時のコンパイルで2GBのRAMが要求されます。
Raspberry Pi ZeroはRAM512MBですw /(^o^)\公式にもあるスワップを使用したメモリ拡張で解決しました。手順は以下。
今のところ全く問題なく、MeCabれてます。$ sudo dd if=/dev/zero of=/swapfile1 bs=1M count=2048 # 2GBのスワップ領域を確保 $ sudo chmod 600 /swapfile1 # パーミッション設定 $ sudo mkswap /swapfile1 # スワップ領域を作る $ sudo swapon /swapfile1 # スワップ領域を有効化 $ ./bin/install-mecab-ipadic-neologd -n # インストール $ sudo swapoff /swapfile1 # スワップ領域を無効化 $ sudo rm /swapfile1 # スワップ領域を削除3. ライブラリ不足によるnode-canvasのコンパイルエラー
自分はRaspberry Pi OS Liteをインストールしました。
初期ライブラリが少ないからなのか、以下の通り色々はまりました。
- npm install canvas時にcanvas moduleが見つからない
- アプリ実行時にjsdom(nodeパッケージ)がimageをロードできない的なエラーを吐く
原因は、単にライブラリが足りてないだけでした。
以下ライブラリをインストール後、エラー関連のパッケージを再インストール&ビルドして解決。$ sudo apt-get install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev libjpeg8-dev libgif-dev g++ $ npm install canvas --build-from-sourceさいごに
本記事で実行しているアプリは、もともとherokuのslug size制限で動かせなかったものです。
こんな小さなボードでこれだけのことができることに驚愕です。
ラズパイゼロ最高。オンプレミス最高。参考記事
- 投稿日:2021-01-24T17:44:40+09:00
Reactでの認証時にJWTをCookieに設定する方法
SPAでの認証といえばJWTを使うことが多いと思いますが、
localStorageに保存するとセキュリティリスクが高いとかで、
CookieにHttpOnlyな値として保存するのが良いとしばしば言われることもあります。
今回はReact × ExpressでJWTをCookieに保存する具体的な方法を紹介します。(そもそもJWTを使うべきかとか、localStorageを使うことのリスクなどについては要件次第なのであまり言及しません)
調査にあたっては以下の記事を参考にしました。
React Authentication: How to Store JWT in a Cookie記事の方法そのままでは自分の環境では上手くいかなかったので、ハマりポイントも含めて手順を解説します。
最終的に出来上がったもの
JWTをCookieに保存する
https://github.com/Kanatani28/jwt-how-to-use/tree/fix_using_cookiesCSRF対策
https://github.com/Kanatani28/jwt-how-to-use/tree/fix_using_csrf_protection動作環境
以下のDockerイメージを使用して挙動を確認しました。
node:15.5.1-alpine3.12準備編
まずはlocalStorageにJWTを保存して動くサンプルアプリケーションを用意します。
上記の参考記事を見てもらっても良いですが、
こちらで用意した以下のリポジトリを見てもらっても良いです。
本記事ではこちらに準じて進めます。Reactの部分だけTypeScriptを使用 + Dockerを使った構成
https://github.com/Kanatani28/jwt-how-to-use(ちなみに自前でプロジェクトを作成したい場合は
create-react-app
でプロジェクトを作成して、各種ライブラリをインストールしてください。)ソースコードは以下のようになっています。
App.tsximport React, { useState } from 'react'; import axios from 'axios'; import './App.css'; const apiUrl = 'http://localhost:3001'; axios.interceptors.request.use( // allowedOriginと通信するときにトークンを付与するようにする設定 config => { const { origin } = new URL(config.url as string); const allowedOrigins = [apiUrl]; const token = localStorage.getItem('token'); if (allowedOrigins.includes(origin)) { config.headers.authorization = `Bearer ${token}`; } return config; }, error => { return Promise.reject(error); } ); type Food = { id: number description: string } function App() { const storedJwt = localStorage.getItem('token'); const [jwt, setJwt] = useState(storedJwt || null); const [foods, setFoods] = useState<Food[]>([]); const [fetchError, setFetchError] = useState(null); const getJwt = async () => { const { data } = await axios.get(`${apiUrl}/jwt`); localStorage.setItem('token', data.token); setJwt(data.token); }; const getFoods = async () => { try { const { data } = await axios.get(`${apiUrl}/foods`); setFoods(data); setFetchError(null); } catch (err) { setFetchError(err.message); } }; return ( <> <section style={{ marginBottom: '10px' }}> <button onClick={() => getJwt()}>Get JWT</button> {jwt && ( <pre> <code>{jwt}</code> </pre> )} </section> <section> <button onClick={() => getFoods()}> Get Foods </button> <ul> {foods.map((food, i) => ( <li>{food.description}</li> ))} </ul> {fetchError && ( <p style={{ color: 'red' }}>{fetchError}</p> )} </section> </> ); } export default App;server.jsconst express = require('express'); const jwt = require('express-jwt'); const jsonwebtoken = require('jsonwebtoken'); const cors = require('cors'); const app = express(); app.use(cors()); const jwtSecret = 'secret123'; app.get('/jwt', (req, res) => { // JWTを生成する(今回は固定値で作成している) res.json({ token: jsonwebtoken.sign({ user: 'johndoe' }, jwtSecret) }); }); app.use(jwt({ secret: jwtSecret, algorithms: ['HS256'] })); const foods = [ { id: 1, description: 'burritos' }, { id: 2, description: 'quesadillas' }, { id: 3, description: 'churos' } ]; app.get('/foods', (req, res) => { res.json(foods); }); app.listen(3001); console.log('App running on localhost:3001');アプリケーション概要
server.jsには
/jwt
と/foods
という2つのエンドポイントを用意しています。
/jwt
はJWTを、/foods
はJSONデータを返します。
App.tsxではボタンを2つ用意し、それぞれボタンを押したタイミングでサーバーと通信するようにしています。
docker-compose up
を実行するとlocalhost:3000
でReactのアプリケーションが立ち上がり、
その後docker-compose exec front node src/server.js
を実行すると
localhost:3001
でNode.jsのアプリケーションが立ち上がります。
localhost:3000
にアクセスすると以下のような画面が表示されるはずです。いきなりGet Foodsボタンを押すと401エラーが表示され、
Get JWTでJWTを取得後、Get Foodsボタンを押すと、今度は正常に通信できるはずです。localStorageを確認してみる
Chromeの開発者ツール > Applicationを開くとlocalStorageに取得したtokenが設定されているのが確認できます。
localStorageに保存されているので、当然JavaScriptで取得することができます。
localStorage.getItem("token")この状態があまりよろしくないので修正していきます。
修正編
JWTをCookieに保存する
まず最初にserver.jsのJWTを発行する部分を修正していきます。
そもそもCookieの仕組みって?
図にすると以下のようになります。
(知ってるよって人はスキップしてください)サーバーからのレスポンスヘッダーにSet-Cookieという値が設定されていた場合、
クライアントのCookieにその値がセットされます。
以降そのサーバーとの通信ではセットされたCookieの値が付与されることになります。
フルスタックなフレームワークだとこういった仕組みを提供しているものが多いです。Set-Cookieヘッダーを付与するようにする
Cookieをセットするためには、サーバーのレスポンスにSet-Cookieヘッダーを含める必要があります。
Cookieを使うため、JWT取得時に以下のようにSet-Cookieヘッダーを含めてレスポンスを返すようにします。server.jsapp.get('/jwt', (req, res) => { const token = jsonwebtoken.sign({ user: 'johndoe' }, jwtSecret); // Set-Cookieヘッダーにtokenをセットする処理 res.cookie('token', token, { httpOnly: true }); res.json({ token }); });今回はHttpOnlyをtrueとしているため、
document.cookie
のようなJavaScriptからはアクセスできず、
基本的にはHTTP通信するときのみ参照できるようになっています。
(HttpOnlyを設定していない場合はセキュリティ的にはlocalStorageに保存する方法と大差ないかと思います)CORS対応する(ハマりポイント)
こちらは元記事にはなかった手順になります。
SPAではよくある構成かと思いますが、今回はlocalhost:3000
とlocalhost:3001
と
クロスオリジンでアプリケーションを起動しています。
クロスオリジンでCookieを使用する場合、いくつか設定が必要になります。server.jsのcorsを設定している部分を以下のように修正します。
server.jsapp.use(cors({ credentials: true, origin: "http://localhost:3000" }));これで
localhost:3000
で起動しているアプリケーションともCookieをやり取りすることができるようになります。また、App.tsxの方にも以下を追記します。
App.tsxaxios.defaults.withCredentials = true;今回はサーバーとの通信にaxiosを使用していますが、
axiosはデフォルトではCookieを使う設定になっていないので、
上記のようにwithCredentialsをtrueにすることで通信時にCookieを送信できるようになります。ここまで設定できたら再度アプリケーションを動かしてみましょう。
Get JWTボタンを押すとJWTが取得でき、開発者ツールで確認すると
Cookieにtokenが設定できているはずです。Cookieに設定されたtokenを検証するようにする
server.jsでApp.tsxからのリクエスト時にCookieに設定されたtokenを検証する処理を追記・修正します。
まずは新しく
cookie-parser
というライブラリを追加します。docker-compose exec front yarn add cookie-parser次にserver.jsを以下のように修正します。
server.jsconst cookieParser = require('cookie-parser'); // 略 app.use(cookieParser()); app.use(jwt({ secret: jwtSecret, algorithms: ['HS256'], getToken: req => req.cookies.token }));expressではcookie-parserを使用することでRequestに含まれるCookieを簡単に取得できるようになります。(
req.cookies.token
の部分)
また、検証もexpress-jwtを使うことで手軽にできるようになります。
getTokenで設定した関数でトークンを取得し、secretに設定した値を使って検証するといったような形です。次にApp.tsxの方で不要になったlocalStorageを使用する部分を削除しておきます。
この部分は参考記事ではこの対応はしていませんが、
localStorageとCookieどちらが使われているかわかりにくくなるかもしれないので念のために消しておきます。また、この修正でlocalStorageからJWTを読み込まないようにしたので
画面表示時にJWTが表示されることがなくなります。
HttpOnlyなCookieを使ったのでdocument.cookie
のようなJavaScriptからは取得できないようになっています。App.tsx// 略 // Bearerで送る必要がなくなったので不要 // axios.interceptors.request.use( // config => { // const { origin } = new URL(config.url as string); // const allowedOrigins = [apiUrl]; // const token = localStorage.getItem('token'); // if (allowedOrigins.includes(origin)) { // config.headers.authorization = `Bearer ${token}`; // } // return config; // }, // error => { // return Promise.reject(error); // } // ); // 略 function App() { // localStorageにセットしなくなったので不要 // const storedJwt = localStorage.getItem('token'); // 初期値はnullにしている const [jwt, setJwt] = useState<string | null>(null); // 略 const getJwt = async () => { const { data } = await axios.get(`${apiUrl}/jwt`); // localStorageにセットする必要がないので不要 // localStorage.setItem('token', data.token); setJwt(data.token); }; // 略以上でJWTをCookieに保存してサーバーとやりとりできるようになりました。
CSRF対策
localStorageはXSSによる攻撃を受けやすいのに対して、
Cookieの場合はCSRFによる攻撃を受けやすいと言われています。なのでCookieを使ったtokenのやり取りにはCSRFへの対策とセットで行なう必要があります。
サンプルアプリケーションのアップデート
server.jsにPOSTリクエストを受け付けるエンドポイントを追加します。
server.jsapp.post('/foods', (req, res) => { foods.push({ id: foods.length + 1, description: 'new food' }); res.json({ message: 'Food created!' }); });実装は適当ですが、新しくFoodを追加するようなAPIができたイメージですね。
成功した場合はFood created!というメッセージが返ってきます。また、App.tsxの方から、POSTリクエストを送信するように修正します。
App.tsxfunction App() { // 略 const [newFoodMessage, setNewFoodMessage] = useState(null); const createFood = async () => { try { const { data } = await axios.post(`${apiUrl}/foods`); setNewFoodMessage(data.message); setFetchError(null); } catch (err) { setFetchError(err.message); } }; // 略 return ( <> // 略 <section> <button onClick={() => createFood()}> Create New Food </button> {newFoodMessage && <p>{newFoodMessage}</p>} </section> </> ); }CSRFトークンを利用する
expressではcsurfというライブラリを使うことで
手軽にCSRF対策をすることができます。
まずはライブラリを追加します。docker-compose exec front yarn add cookie-parser
/csrf-token
にCSRFトークンを取得するエンドポイントを設定します。server.jsconst csrf = require('csurf') // 略 const csrfProtection = csrf({ cookie: true }); app.use(csrfProtection); app.get('/csrf-token', (req, res) => { res.json({ csrfToken: req.csrfToken() }); });これでCSRFトークンを発行できるようになったので、
App.tsxから利用するようにします。App.tsxfunction App() { // 略 useEffect(() => { const getCsrfToken = async () => { const { data } = await axios.get(`${apiUrl}/csrf-token`); axios.defaults.headers.post['X-CSRF-Token'] = data.csrfToken; }; getCsrfToken(); }, []); // 略 }画面表示時にCSRFトークンを取得し、axiosに設定するようにしています。
これでCSRFの対策ができました。※ちなみにCSRFトークン取得時にもCookieの値が検証されるので、403エラーが出る場合はJWT取得後に画面を更新してからCreateしてみてください。今回は一画面にすべて詰め込んでいるのでこんな感じになってしまいます。
最後に
Cookieの仕組みやCORSについての理解があればフロントエンドがReactからVueになろうが
バックエンドがExpressから他のFWになろうが知識を流用できるはずです。また、localStorageでもCookieでもXSS対策がされていない場合、難易度に差はあれど盗難のリスクが発生するのは同じなので
そもそもXSS対策がされているかどうかのチェックは必須といえるでしょう。クロスサイトスクリプティング(XSS)対策としてCookieのHttpOnly属性でどこまで安全になるのか
高い保守性やUXを保持しつつ安全なアプリケーションを目指していきたいですね。
参考
React Authentication: How to Store JWT in a Cookie
クロスサイトでCookieが設定できない場合に確認すること
CORSまとめ
express.jsのcors対応
Express cors middleware
MDN Web Docs Set-Cookie
クロスサイトスクリプティング(XSS)対策としてCookieのHttpOnly属性でどこまで安全になるのか
- 投稿日:2021-01-24T08:51:32+09:00
node.jsの基本
node.jsは僕にとってはじめてお金を稼げた言語でとても特別なものなのですが、仕様を端から端まで熱心に勉強したわけではなく、壁にぶつかってはググっての繰り返しで身につけたので自分の理解をちゃんと整理するための備忘録として、また初学の人たちへ少しでも役に立てばとまとめました。
node.jsとは
node.jsはサーバーサイドのイベント駆動型JavaScript環境です。イベント駆動型というのは、プログラムの実行から何かしらが起きるまで待機して、起きた事物が規定の条件を満たしたときに指定された命令を実行するプログラミングの概念です。node.jsはJavaScriptを用いてPHPやPythonなどで書くようなサーバーサイドアプリケーションを作成することができます。
Hello World
node.jsにおける最も基礎的なHello Worldです
server.jsconst http = require('http') const port = 3000 const hostName = "127.0.0.1" const headers = { 'Content-Type': 'text/plain' } http.createServer((req, res) => { res.writeHead(200, headers) res.end('Hello World!\n') }).listen(port, hostName) console.log('Server running at http://' + hostName ')これを走らせます
node server.js
結果: Hello World!が http://127.0.0.1:3000へのgetリクエストに返信します
豆知識?
コマンドラインで走らせたコードはCtrl + Cで終了させられますnpm (Node Package Manager)
npmはnode.jsにおけるパッケージマネージャーです。Pythonにおけるpipみたいなものでしょうか(pythonはflaskを少し触ったくらいですので間違ってたらご教授お願いします)npmはnode.jsの開発環境においてモジュールの作成、共有、再利用を可能にしてくれます。この機能はnode.jsのインストールについてきます。
モジュールのインストール方法
最も簡素な方法(例:express)
npm install express
モジュールの使い方
先程のhello worldでnode.jsにビルトインされているhttpモジュールを宣言しましたが全く同じように宣言できます。
const express = require('express')express.jsはnode.jsにおける最も簡素なウェブフレームワークのひとつです。
server.jsconst express = require('express') const port = 3000 const server = express() server.get('/', (req, res) => { res.header(200).send("Hello World!") }) server.listen(port, () => { console.log('server listen on port:'+ port) })
ローカルインストールとグローバルインストール
npmでインストールするモジュールには二種類の方法があります。とても重要です。
ローカルインストール
ローカルにインストールするのはつまりそのディレクトリ内のnode_modulesフォルダにそのライブラリがインストールされるということです。
インストール方法npm install [module_name]
グローバルインストール
グローバルインストールはそのマシンの全ファイルシステムでそのモジュールを実行可能にします。先程の例に使ったexpressは自動でアプリの骨組みを作成する機能がありますので、
npm install -g express express [app_name]
で自動でウェブアプリの雛形を作ってくれます。
node.jsの仕組み
Non-blocking I/O
IOはInputとOutputのことでコンピューターを通して入力と出力の間のデータをやりとりすることです。
入力は必ずしも人間とコンピューターとのやりとりに限りません。ウェブアプリにおけるI/Oはどんどん複雑になっています。それは単なるブラウザとサーバーのやり取りに限らず、サードパーティのAPIとのやりとり、スマホなど別のデバイスを使った認証、データベースへの多重同時接続などなど。
つまるところネットワークにおけるI/Oは複雑で詳細な予測が難しいということです。
そしてI/Oの処理にはときに長い時間がかかり、データを読み込んでいる間などに処理がそこで待機しつづけてしまうことがあります。非同期メソッド
この問題の解決のためにはコールバックを行います。
asynchronous.jsconst fs = require('fs') fs.readFile('test.txt', (err, data) => { if(err){ console.log(err) return } console.log(data) }) console.log("this text should come first")コールバックであるconsole.log(data)は"this text should come first"のあとに実行されます。なぜコードの中で下にある処理が先に実行されるのか、シングルスレッドであるnode.jsでこれを可能にしているのがイベントループです。
イベントループ
node.jsの最も重要な特徴のひとつです。イベント駆動型であるnode.jsは何かが起きるまでループを繰り返して待機し続けていて、なにかすることが出てくるとすることとコールバックをキューに追加して他の一連の処理をして、次のI/Oへその処理が終了するまで待ち続けるループを行うのがイベントループです。
この特徴を最大限活かすのがnode.jsにおける効率的なコードを書くうえで大切になります。関数はイベントループをブロックしない、時間のかかる処理は細かい処理に分割するなどなど。その2へ続きます。