20210124のNode.jsに関する記事は7件です。

フレームワーク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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

元ネタ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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_profile
eval "$(nodenv init -)"

ここでターミナルを閉じて、新たにターミナルを立ち上げます。

node-buildプラグインのインストール

以下のコマンドを順に実行して、node-buildプラグインをインストールします。

ターミナル
mkdir -p "$(nodenv root)"/plugins
git clone https://github.com/nodenv/node-build.git "$(nodenv root)"/plugins/node-build

Node.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.4

npmのバージョンの確認

以下のコマンドで、現在のnpmのバージョンを確認してみます。

ターミナル
npm -v

npmのバージョンは6.14.10でした。Node.jsのリリース一覧を確認すると、Node.js 14.15.4に対応するnpmのバージョンと一致していました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

フロントサーバ、バックサーバの分離された構成でミニマムなサービスを作る。

はじめに

本記事はモノリシックでない分離されたアーキテクチャ(マイクロサービス)について理解を深めるため、
ミニマムなアプリを作ってみたときのまとめ記事です。
※当方初心者のため、間違いありましたら是非ともご指摘お願いいたします。

著者について

以下著者のスペック

  • エンジニア一年目
  • WebアプリといえばMVCしか知らない
  • マイクロサービス?なにそれおいしいの

背景

基本情報などの勉強中、よくこんな図が出てきて混乱していました。

image.png

  • Webアプリのアーキテクチャ?、MVCしかしらんけど?
  • アプリのサーバって一つだけじゃないの?
  • でもReactとかは独立してサーバーが立っているぽい、、
  • どうやってバックエンドと連携させるんやろ、、?

このような疑問を持った私は、「わかんないなら触ってみればいいじゃん!」と意気込み、
フロントエンドとバックエンドの機能をサーバごとに切り離したミニマムなアプリを作ろうと決めました。

開発

概要

画面を担当するフロントサーバとロジックを担当するバックサーバの二つのサーバを立てて、
インターネットのニュースを検索できるアプリを作る。

環境

  • Node.js v14.15.1

    • サーバサイドのJavaScript
    • こちらの記事が非常にわかりやすくおすすめ
    • 今回は超簡単なロジックを実装するのみに使う
  • Express v4.17.1

    • Node.jsで動く軽量なWebフレームワーク
    • Webサーバを立てるのも非常に簡単
    • 今回はAPIのルーティング等に使う
  • Yarn v1.22.4

    • npmと互換性があるパッケージ管理システム
    • 一度インストールしたパッケージをキャッシュするためインストールが高速

目標

画像のような簡単なニュース検索アプリを作る。
検索条件を入れ、「Search」ボタンを押下すれば、検索条件にヒットするニュースを検索する。
image.png

構成

本アプリの構成を以下画像に示します。
image.png

重要であるのはフロントサーバとバックサーバを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.js
const 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.js
    const 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

※コードレビュー歓迎

終わりに

業務でも趣味でもモノリシックなアーキテクチャしか触ったことがなく、
ツイッター上でマイクロサービスなどの用語を理解するのに時間がかかりました。
※現在は完全に理解した程度

技術理解のために実際に触れてみるのはやはり強いですね、、、
本記事がどなたかのお役に立てれば幸いです。
※間違いありましたら是非ご指摘お願いいたします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 -v

v11.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制限で動かせなかったものです。
こんな小さなボードでこれだけのことができることに驚愕です。
ラズパイゼロ最高。オンプレミス最高。

参考記事

  1. https://www.taneyats.com/entry/install-nodejs-on-raspberrypi-zero
  2. https://github.com/Automattic/node-canvas/wiki/Installation%3A-Ubuntu-and-other-Debian-based-systems
  3. https://girliemac.com/blog/2016/06/13/kittycam-update-with-raspberrypi3/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでの認証時にJWTをCookieに設定する方法

SPAでの認証といえばJWTを使うことが多いと思いますが、
localStorageに保存するとセキュリティリスクが高いとかで、
CookieにHttpOnlyな値として保存するのが良いとしばしば言われることもあります。
今回はReact × ExpressでJWTをCookieに保存する具体的な方法を紹介します。

(そもそもJWTを使うべきかとか、localStorageを使うことのリスクなどについては要件次第なのであまり言及しません)

調査にあたっては以下の記事を参考にしました。
React Authentication: How to Store JWT in a Cookie

記事の方法そのままでは自分の環境では上手くいかなかったので、ハマりポイントも含めて手順を解説します。

最終的に出来上がったもの

動作環境

以下のDockerイメージを使用して挙動を確認しました。
node:15.5.1-alpine3.12

準備編

まずはlocalStorageにJWTを保存して動くサンプルアプリケーションを用意します。
上記の参考記事を見てもらっても良いですが、
こちらで用意した以下のリポジトリを見てもらっても良いです。
本記事ではこちらに準じて進めます。

Reactの部分だけTypeScriptを使用 + Dockerを使った構成
https://github.com/Kanatani28/jwt-how-to-use

(ちなみに自前でプロジェクトを作成したい場合はcreate-react-appでプロジェクトを作成して、各種ライブラリをインストールしてください。)

ソースコードは以下のようになっています。

App.tsx
import 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.js
const 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ボタンを押すと、今度は正常に通信できるはずです。

  • JWTなしで通信
    スクリーンショット 2021-01-21 22.37.22.png

  • JWTありで通信
    スクリーンショット 2021-01-21 22.37.33.png

localStorageを確認してみる

Chromeの開発者ツール > Applicationを開くとlocalStorageに取得したtokenが設定されているのが確認できます。

スクリーンショット 2021-01-21 23.38.45.png

localStorageに保存されているので、当然JavaScriptで取得することができます。

localStorage.getItem("token")

この状態があまりよろしくないので修正していきます。

修正編

JWTをCookieに保存する

まず最初にserver.jsのJWTを発行する部分を修正していきます。

そもそもCookieの仕組みって?

図にすると以下のようになります。
(知ってるよって人はスキップしてください)

スクリーンショット 2021-01-22 17.19.04.png

サーバーからのレスポンスヘッダーにSet-Cookieという値が設定されていた場合、
クライアントのCookieにその値がセットされます。
以降そのサーバーとの通信ではセットされたCookieの値が付与されることになります。
フルスタックなフレームワークだとこういった仕組みを提供しているものが多いです。

Set-Cookieヘッダーを付与するようにする

Cookieをセットするためには、サーバーのレスポンスにSet-Cookieヘッダーを含める必要があります。
Cookieを使うため、JWT取得時に以下のようにSet-Cookieヘッダーを含めてレスポンスを返すようにします。

server.js
app.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:3000localhost:3001
クロスオリジンでアプリケーションを起動しています。
クロスオリジンでCookieを使用する場合、いくつか設定が必要になります。

server.jsのcorsを設定している部分を以下のように修正します。

server.js
app.use(cors({
  credentials: true,
  origin: "http://localhost:3000"
}));

これでlocalhost:3000で起動しているアプリケーションともCookieをやり取りすることができるようになります。

また、App.tsxの方にも以下を追記します。

App.tsx
axios.defaults.withCredentials = true;

今回はサーバーとの通信にaxiosを使用していますが、
axiosはデフォルトではCookieを使う設定になっていないので、
上記のようにwithCredentialsをtrueにすることで通信時にCookieを送信できるようになります。

ここまで設定できたら再度アプリケーションを動かしてみましょう。
Get JWTボタンを押すとJWTが取得でき、開発者ツールで確認すると
Cookieにtokenが設定できているはずです。

スクリーンショット 2021-01-24 17.47.38.png

Cookieに設定されたtokenを検証するようにする

server.jsでApp.tsxからのリクエスト時にCookieに設定されたtokenを検証する処理を追記・修正します。

まずは新しくcookie-parserというライブラリを追加します。

docker-compose exec front yarn add cookie-parser

次にserver.jsを以下のように修正します。

server.js
const 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.js
app.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.tsx
function 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.js
const 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.tsx
function 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してみてください。今回は一画面にすべて詰め込んでいるのでこんな感じになってしまいます。

スクリーンショット 2021-01-24 17.26.40.png

最後に

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属性でどこまで安全になるのか

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

node.jsの基本

node.jsは僕にとってはじめてお金を稼げた言語でとても特別なものなのですが、仕様を端から端まで熱心に勉強したわけではなく、壁にぶつかってはググっての繰り返しで身につけたので自分の理解をちゃんと整理するための備忘録として、また初学の人たちへ少しでも役に立てばとまとめました。

node.jsとは

node.jsはサーバーサイドのイベント駆動型JavaScript環境です。イベント駆動型というのは、プログラムの実行から何かしらが起きるまで待機して、起きた事物が規定の条件を満たしたときに指定された命令を実行するプログラミングの概念です。node.jsはJavaScriptを用いてPHPやPythonなどで書くようなサーバーサイドアプリケーションを作成することができます。

Hello World

node.jsにおける最も基礎的なHello Worldです

server.js
const 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.js
const 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 に存在するモジュールはnpmのウェブサイトにて検索できますが "npm search [module_name]" でも検索できます。

ローカルインストールとグローバルインストール

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.js
const 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へ続きます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む