Bcryptを用いてパスワードをハッシュ化する (Node.js)
ログイン機能のあるアプリケーションを開発する場合、(大体の場合は)ユーザーにパスワードを設定してもらうことになります。当然ですがそれらのパスワードを平文でデータベースに保存したりするのはセキュリティ的にご法度で、ハッシュ化して保存をする必要があります。
この記事では、bcrypt というパッケージを用いて、Node.jsでパスワードをハッシュ化する方法・そしてそれを用いてパスワードをチェックする方法を紹介します。ちなみに、bcryptはハッシュ化アルゴリズムの名前でもありパッケージの名前でもあります。以下混同して使用しています。
準備
まずはbcryptをインストールします。TypeScriptの型定義ファイルももちろんあります。
$ npm install bcrypt --save $ npm install @types/bcrypt --save-dev # TypeScriptを利用している場合
ちなみに執筆時では bcrypt@3.0.5
が入りました。
ハッシュ化
まずは平文のパスワードをハッシュ化します。会員登録時やパスワード変更時に行う処理ですね。
以下のように、パスワードを「ラウンド数」とともに bcrypt.hash
関数に渡します。
ラウンド数を大きくすればするほど安全なハッシュを生成することができますが、計算に要する時間が長くなります(ラウンド数が1増えると時間は2倍ほどになるそうです)。こちらによると、ラウンド数10でのハッシュ化には2.0GHzのCPUで0.1秒ほどかかるようです。
ここではラウンド数を10に設定しています。通常はこれくらいで(もう少し小さくても?)大丈夫そうです。
const bcrypt = require('bcrypt'); const saltRounds = 10; bcrypt.hash(password, rounds) .then((hashedPassword) => { // ... });
チェック
平文のパスワード(password
)とハッシュ化されたパスワード(hashedPassword
)を比較し、その平文のパスワードが正しいかどうかをチェックします。ログイン時の処理ですね。正しければisCorrectPassword
がtrue
に、そうでなければfalse
になります。
bcrypt.compare(password, hashedPassword) .then((isCorrectPassword) => { // ... });
おまけ
- bcryptの計算の概要について大雑把に説明します。まずランダムでソルトを作成しています。そして、平文パスワードとソルトを組み合わせてハッシュ化し、そのハッシュとソルトを組み合わせてハッシュ化し、そのハッシュとソルトを組み合わせてハッシュ化し...というのを何回も繰り返しているようです。その回数を調整しているのがラウンド数というわけです。
- 上の例での
hashedPassword
には、ラウンド数やソルトの情報も含まれています。compare
関数は「ラウンド数やソルトの情報を抜き出し、それをもとに平文パスワードをソルトともに指定回数ハッシュ化し、比較する」ということをやってます。 - 上の例の通り、bcryptは、コスト(ラウンド数)を設定できるようになっています。これにより、コンピューターの計算能力が伸びて攻撃側の能力が上がったとしても(ラウンド数を上げることで)同じアルゴリズムを利用し続けることができるというメリットがあります。
bcrypt
には、hashSync
関数やcompareSync
関数などの同期的な関数も存在します。しかし、簡単なスクリプト等以外には使用はするべきではありません。上にも書きましたが、これらの関数は計算量の大きい処理をするので、ある程度時間がかかります。その間イベントループがブロックされてしまい、他の処理が(ほとんど)できなくなります。サーバーアプリケーションの場合はリクエストに対する処理ができなくなってしまいます。ちなみに、hash
関数やcompare
関数を呼び出した際はスレッドプールを利用して(メインスレッド外で)処理をするので、イベントループのブロックはおきません。そして、複数のCPUコアを有効活用できます。
Cのプログラムをx86-64のマシン上でPowerPC向けにコンパイルする
x86-64のマシン + Ubuntu 16.04で、PowerPC 64bit向けにC言語のプログラムをコンパイルする方法を紹介します。俗に言うクロスコンパイルっていうやつです。
以下のように、必要なものをaptで入れます。binutilsにはアセンブラやローダが入っています。
$ sudo apt install gcc-powerpc-linux-gnu binutils-powerpc-linux-gnu
これで環境は整ったので、実際に動かしてみます。まず、以下のファイルをa.cとして保存します。
int fact(int n) { if (n == 1) return 1; return n * fact(n - 1); }
以下のようなコマンドでコンパイルします。わかりやすさのために最適化オプションを無効にし、アセンブリを出力しています。
$ powerpc64-linux-gnu-gcc -S a.c -O0
以下のような(見慣れた?)PowerPCのアセンブリを得ることができました。
.file "a.c" .machine power7 .section ".toc","aw" .section ".text" .align 2 .globl fact .section ".opd","aw" .align 3 fact: .quad .L.fact,.TOC.@tocbase,0 .previous .type fact, @function .L.fact: mflr 0 std 0,16(1) std 31,-8(1) stdu 1,-128(1) mr 31,1 mr 9,3 stw 9,176(31) lwz 9,176(31) cmpwi 7,9,1 bne 7,.L2 li 9,1 b .L3 .L2: lwz 9,176(31) addi 9,9,-1 extsw 9,9 mr 3,9 bl fact mr 9,3 mr 10,9 lwz 9,176(31) mullw 9,9,10 extsw 9,9 .L3: mr 3,9 addi 1,31,128 ld 0,16(1) mtlr 0 ld 31,-8(1) blr .long 0 .byte 0,0,0,1,128,1,0,1 .size fact,.-.L.fact .ident "GCC: (Ubuntu/IBM 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609"
とても簡単で最高ですね。
ちなみに、今回入ったアプリケーションたちのバージョンはこのような感じでした。
$ powerpc64-linux-gnu-gcc --version powerpc64-linux-gnu-gcc (Ubuntu/IBM 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609 ...
$ powerpc64-linux-gnu-as --version GNU assembler (GNU Binutils for Ubuntu) 2.26.1 ...
$ powerpc64-linux-gnu-ld --version GNU ld (GNU Binutils for Ubuntu) 2.26.1 ...
URLから画像のサイズを取得する(Node.js)
サーバーサイドアプリケーションを書いていると、「あるURLでアクセスできる画像の(縦横の)サイズを知りたい」という場面がまれにあると思います。Nodejsにおいて、そういった場合にどうしたらよいかを紹介します。
準備
image-size を利用します。また、ここでは axios を用いますが、node-fetch など他のライブラリでも同様にできると思います。標準ライブラリの https.request
や http.request
を用いることもできますが、いろいろとめんどくさいと思います(そもそもhttpかhttpsかで使うライブラリを分けなくてはいけない?)。
ちなみに執筆時点でのバージョンは axios@0.18.0
, image-size@0.7.3
です。
$ npm i image-size axios
実装
以下のような関数を用意します。axios.get
のオプションにおいて responseType: 'arraybuffer'
と指定してあげないと、 res.data
が文字列になったりしてうまくいきません(ContentTypeなどをもとにうまくやってほしいとも思ってしまいますが...)。
この関数に画像のURLを投げれば、width
プロパティ(number)と height
プロパティ(number)をもったオブジェクト(のプロミス)を返してくれます。大体のメジャーなフォーマットに対応してくれています。
const imageSize = require('image-size'); const axios = require('axios'); function fetchSize(imgUrl) { return axios.get(imgUrl, { responseType: 'arraybuffer', }) .then(res => imageSize(res.data)) .then(i => ({ width: i.width, height: i.height, })); }
TypeScriptの場合
簡単ですが、TypeScriptだと以下のようになります。
import imageSize from 'image-size'; import axios from 'axios'; function fetchSize(imgUrl: string): Promise<{ height: number; width: number; }> { return axios.get(imgUrl, { responseType: 'arraybuffer', }) .then(res => imageSize(res.data)) .then(i => ({ width: i.width, height: i.height, })); }
NginxとDockerでリバースプロキシサーバーを作る
Dockerコンテナ内でnginxを動かし、リバースプロキシサーバーを作る簡単な例を紹介します。
プロダクションで利用する際はnginxの設定をもっとしっかりしないといけませんが、この記事ではそのあたりは端折ります。
コード
Dockerfile
nginx.confを /etc/ginx
に追加するだけのものすごく簡単なものです。イメージについては2019年4月9日現在の最新バージョンである nginx:1.15.10
を使用しています。
FROM nginx:1.15.10 COPY nginx.conf /etc/nginx/
nginx.conf
nginxの設定ファイルです。/proxy/
からはじまるパスへのリクエストを http://example.com に振り分けるようにします。それ以外は /usr/share/nginx/html
(ドキュメントルート)から配信するようにしています。
また、ログに $upstream_addr
を追加するようにしています。これでプロキシ先のアドレスがログに含まれるようになります。
ちなみに、nginxの公式イメージでは、 /var/log/nginx/access.log
は /dev/stdout
(標準出力)のエイリアスになっています。(そのイメージをもとに作っている)今回のイメージでも同様です。標準出力に吐き出されたログは docker logs
コマンドなどでみることができます。
user nginx; worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '"$request" $status $upstream_addr'; access_log /var/log/nginx/access.log main; server { location / { root /usr/share/nginx/html; } location /proxy/ { proxy_pass http://example.com/; } } }
起動
イメージを作成してコンテナを実行します。-p 3000:80
で、ホストのポート3000でコンテナのポート80にアクセスできるようにしています。
$ docker run -d -p 3000:80 $(docker build .)
確認
ブラウザなどで http://localhost:3000/
にアクセスすると Welcome to nginx!
のページが、 http://localhost:3000/proxy/
にアクセスすると Example Domain
のページが見れ、正しく動いていることがわかります。
また、以下のようにコンテナのログを確認してみます。
$ docker logs <コンテナid> "GET / HTTP/1.1" 200 - "GET /proxy/ HTTP/1.1" 200 93.184.216.34:80
うまくいっていることがわかります。(example.comのIPアドレスは変わるかもしれません。)
初期化されたMySQLのDockerコンテナを用意する
サーバーサイドアプリケーションの開発環境などにおいて、「初期化された(データベースやテーブルが作成された)MySQLのDockerコンテナを用意したい」といった場面があると思います。そういった際にどうすればよいか、簡単な例を通して説明します。
コード
init.sql
実行したいSQL文たちをファイルとして保存します。
CREATE DATABASE db1; USE db1; CREATE TABLE table1(id INT); INSERT INTO table1 VALUES(1);
Dockerfile
SQL文が書かれたファイルを /docker-entrypoint-initdb.d/
ディレクトリ下にもってきます。MySQLのDockerオフィシャルイメージにおいて、このディレクトリ下に置かれたファイル内に記載されたSQL文がデータベース起動時に実行されるようになっています。
ここでは、2019/4/8現在最新の mysql:8.0.15
イメージを用いています。
FROM mysql:8.0.15 COPY init.sql /docker-entrypoint-initdb.d/init.sql
実行
Dockerファイルとinit.sqlが存在しているディレクトリ内で以下のコマンドを実行します。
$(docker build . -q)
はDockerファイルをもとにイメージを作成し、イメージのIDを返しています。そしてそのイメージをもとにコンテナを立ち上げています。簡単のために、 -e MYSQL_ALLOW_EMPTY_PASSWORD=1
によってパスワードを無効化しています。
$ docker run -d -e MYSQL_ALLOW_EMPTY_PASSWORD=1 $(docker build . -q)
確認
データベース内にアクセスして確認してみます。
$ docker exec -ti <コンテナ名> mysql mysql> select * from db1.table1; +------+ | id | +------+ | 1 | +------+ 1 row in set (0.00 sec)
うまく初期化され、データが入っていることがわかります。
複数ファイルに分割したい場合
今回はSQL文が書かれたファイルを1つ用意しましたが、わかりやすさのためにこれを複数に分けたい場合もあると思います(テーブルの作成とダミーデータの挿入を分けるなど)。そういった際には、init001.sql
, init002.sql
... といったようにファイルを分割し、それらを /docker-entrypoint-initdb.d/
以下に持ってくれば大丈夫です。辞書順で実行してくれます。
NginxのリバースプロキシでのDNS名前解決における落とし穴
問題
Nginxのリバースプロキシでは、プロキシ先として(IPアドレスの他に)ホスト名を指定することができます。 その際、設定ファイルのlocationコンテキストは以下のようになると思います。
location /hoge/ { proxy_pass http://example.com/; }
しかし、これには大きな落とし穴があります。DNSの名前解決がnginxの起動時にしか行われず、TTLは無視され、初回起動時に解決されたIPアドレスがずっと使われてしまうのです。 もちろん、ホスト名に対応するIPアドレスに変更があったときにエラーになってしまいます。
自分の場合では、プロキシ先としてAWSのELBのホスト名を指定しており、(ELBのアドレスは固定ではないので)この問題が原因で502エラーを吐き出してしまっていました。
解決策?
nginxにはresolverというディレクティブがあります(公式ドキュメント)。By default, nginx caches answers using the TTL value of a response
とあるので、TTLが尊重されるみたいです。うまく行きそうな気がします。
試しにresolverにはGoogle Public DNSの8.8.8.8
を利用してみます。
location /hoge/ { resolver 8.8.8.8; proxy_pass http://example.com/; }
しかし、これでもうまくいきません。もはやバグですね。
解決策
このようにして、一旦ホスト名を変数に格納して使うと、うまくいくようです。
location /hoge/ { set $target example.com; resolver 8.8.8.8; proxy_pass http://$target/; }
なんだかなあって感じです。
inversifyJSを用いてTypeScriptでDIをする
この記事では、InversifyJS を用いて TypeScript で DI をするサンプルを紹介し、また、(DIに慣れていない方向けに)DI をすると何が嬉しいのか、というのを説明します。
サンプル
「ユーザーの登録」「登録したユーザーの情報を取得」の 2 つのシンプルな機能を持ったプログラムを書きます。 また、ユーザーが登録されたときにログされるようにします。
準備
必要な依存をインストールします。
$ npm i inversify reflect-metadata typescript ts-node
tsconfig.json
を以下の要領で作成します。
types": ["reflect-metadata"]
, "experimentalDecorators": true
, "emitDecoratorMetaData": true
というのが InversifyJS のために必要です。
{ "compilerOptions": { "target": "es5", "lib": ["es6", "dom"], "types": ["reflect-metadata"], "module": "commonjs", "moduleResolution": "node", "experimentalDecorators": true, "emitDecoratorMetadata": true } }
コード
src/User.ts
User クラスを定義します。
プロパティ2つとtoString()
関数のみのシンプルなクラスです。
class User { constructor(public id: number, public name: string) {} toString(): string { return `User${JSON.stringify({ id: this.id, name: this.name })}`; } }
src/UserRepository.ts
User クラスに対応付けられるデータの永続化に用いるためのインターフェイスを定義します。 「ユーザーの作成」と「ユーザーの取得」の 2 つの操作を定義しています。
import User from "./User"; export default interface UserRepository { find(id: number): User; create(name: string): User; }
src/UserRepositoryImpl.ts
UserRepository の実装を用意します。 通常のアプリケーションであればここでデータベースなどを利用した実装をするかと思いますが、今回はシンプルにインメモリのオブジェクトを用いています。
後に DI で他オブジェクトに注入できるよう、inversify の@injectable()
デコレータをつけています。
import { injectable } from "inversify"; import User from "./User"; import UserRepository from "./UserRepository"; @injectable() export default class UserRepositoryImpl implements UserRepository { private store: { [key: string]: User | undefined } = {}; private nextUserId: number = 0; constructor() {} find(id: number): User { const user = this.store[id]; if (!user) { throw new Error("user not found"); } return user; } create(name: string): User { const id = this.nextUserId++; const user = new User(id, name); this.store[id] = user; return user; } }
src/Logger.ts
こちらはロガー用のシンプルなインターフェイスです。
export default interface Logger { log(message: string): void; }
src/LoggerImpl.ts
ロガーの実装です。実際は winston などを使ったりするとは思いますが、ここでは console に流すだけにしています。
こちらも@injectable()
デコレータをつけています。
import { injectable } from "inversify"; import Logger from "./Logger"; @injectable() export default class LoggerImpl implements Logger { log(message: string): void { console.log(message); } }
src/App.ts
メインのクラスを定義します。
createNewUser(name: string)
により、(UserRepository インターフェイスを実装したインスタンスによって)ユーザーが作成され、また新しいユーザーが作成されたことが(Logger インターフェイスを実装したインスタンスを用いて)ログされるようにしています。
findUser(id: number)
により、特定の id に対応するユーザーのデータを取得できるようにしています。
@inject()
デコレータによって、userRepository
にUserRepository
のインスタンスが、logger
にLogger
のインスタンスが注入されるようになっています。
import { injectable, inject } from "inversify"; import UserRepository from "./UserRepository"; import Logger from "./Logger"; @injectable() export default class App { private readonly userRepository: UserRepository; private readonly logger: Logger; constructor( @inject("UserRepository") userRepository: UserRepository, @inject("Logger") logger: Logger ) { this.userRepository = userRepository; this.logger = logger; } createNewUser(name: string) { const user = this.userRepository.create(name); this.logger.log(`new user: ${user}`); return user; } findUser(id: number) { const user = this.userRepository.find(id); return user; } }
src/inversify.config.ts
DI コンテナを作成し、使用するオブジェクトをバインドします。
import { Container } from "inversify"; import App from "./App"; import Logger from "./Logger"; import LoggerImpl from "./LoggerImpl"; import UserRepository from "./UserRepository"; import UserRepositoryImpl from "./UserRepositoryImpl"; const container = new Container(); container.bind<Logger>("Logger").to(LoggerImpl); container .bind<UserRepository>("UserRepository") .to(UserRepositoryImpl) .inSingletonScope(); container.bind<App>("App").to(App); export default container;
src/index.ts
実際に実行させてみるコードです。
import "reflect-metadata"; import container from "./inversify.config"; import App from "./App"; container.get<App>("App").createNewUser("Taro"); container.get<App>("App").createNewUser("Jiro"); console.log(container.get<App>("App").findUser(1)));
実行
実際に実行させてみると、以下のようになり、うまくいっていることがわかります。
$ ./node_modules/.bin/ts-node srd/index.ts new user: User{"id":0,"name":"Taro"} new user: User{"id":1,"name":"Jiro"} User { id: 1, name: 'Jiro' }
inversifyJS、直感的でわかりやすいですね。
補足
DIコンテナへのオブジェクトの登録において、簡単のために文字列を用いましたが、Symbolを利用することが推奨されています。
何が嬉しいのか
オブジェクト指向的なプログラミングをする際、インターフェイス等を用いて「実装は抽象に依存させる」こと、そして「できるだけ抽象への依存を増やす」ことが大切です。 そうすることでオブジェクト同士が疎結合になり、テストもしやすく、各オブジェクトの実装の変更が簡単な設計になります。
例えば、今回のAppクラスはLoggerとUserRepositoryという抽象(インターフェイス)に依存をしていました。 そして、LoggerImplという実装はLoggerという抽象に依存し、UserRepositoryImplという実装はUserRepositoryという抽象に依存していました。
このことにより、AppクラスはLoggerの実装やUserRepositoryの実装と切り離されていました(疎結合)。
実際、UserRepositoryに関して、(今回や、テスト時のために)単純なオブジェクトを用いた実装を用意しても、MySQLを用いた実装を用意しても、Appクラスのコードは全く変わらないことがわかると思います。
このように、インターフェイスを用いて依存の方向を制御するとソフトウェアの構成がいい感じにはなるのですが、オブジェクトの作成・その受け渡しが結構面倒になります。
今回のケースだったら、
const logger = new LoggerImpl(); const userRepository = new UserRepository(); const app = new App(userRepository, logger);
程度で済んだりもしますが、オブジェクトの数が増え、依存性も複雑になってくるとかなりしんどくなってくるはずです。
それの面倒を見てくれるのが、inversifyJSのようなDIフレームワーク、DIコンテナというわけです。