@@ -4,6 +4,7 @@ title: Cookieと認証(発展)
44
55import cookieCounterVideo from " ./cookie-counter.mp4" ;
66import cookieCounterInspectionVideo from " ./cookie-counter-inspection.mp4" ;
7+ import simpleAuthenticationVideo from " ./simple-authentication.mp4" ;
78
89多くのWebアプリケーションは、利用者が本人であることを確認するための機能を備えています。この節では、Webアプリケーションにおける「ログイン」機能を実装するための、最も基本的な技術要素について学びます。
910
@@ -36,6 +37,8 @@ Cookieを操作するための最も基本的な方法は、HTTPリクエスト
3637
3738次の例は、アクセスした回数をCookieに保存し、アクセスのたびにその値を1増やして表示するプログラムです。アクセス回数はCookieに保存されており、サーバーはその値を通してアクセスされた回数を把握しています。
3839
40+ ![ Cookieを利用したアクセスカウンタの処理の流れ] ( ./cookie-counter-flow.drawio.svg )
41+
3942Expressを用いて` Set-Cookie ` ヘッダをレスポンスに設定するには、[ ` express.Response#cookie ` メソッド] ( https://expressjs.com/ja/api.html#res.cookie ) メソッドを使用するのが一般的です。また、[ cookie-parser] ( https://www.npmjs.com/package/cookie-parser ) パッケージを使用すると、リクエストヘッダに含まれるCookieを簡単に取得できます。このパッケージは、` request.headers.cookie ` を解析し、` request.cookies ` プロパティにオブジェクト形式で格納します。
4043
4144``` javascript title="main.mjs" showLineNumbers
@@ -70,10 +73,6 @@ app.listen(3000);
7073
7174<video src = { cookieCounterVideo } controls muted />
7275
73- プログラムの流れを整理すると、次の図のようになります。ブラウザとサーバーの間でHTTPヘッダを通してCookieがやり取りされていることを確認してください。
74-
75- ![ Cookieを利用したアクセスカウンタの処理の流れ] ( ./cookie-counter-flow.drawio.svg )
76-
7776### 確認問題
7877
7978サンプルプログラムを実行し、Google Chromeの開発者ツールを用いてリクエストやレスポンスに含まれる` Set-Cookie ` ヘッダや` Cookie ` ヘッダの値を確認してみましょう。
@@ -88,12 +87,18 @@ app.listen(3000);
8887
8988## 認証情報をCookieに保存する
9089
91- 次のプログラムは、IDとパスワードによって初回の認証を行い、その結果サーバーで発行された証明書をCookieに保存して次回以降の認証を行うWebアプリケーションの例です。
90+ Cookieを使用して、実際に認証が必要なWebアプリケーションを実装してみましょう。次のプログラムは、IDとパスワードによって初回の認証を行い、その結果サーバーで発行された証明書をCookieに保存して次回以降の認証を行うWebアプリケーションの例です。ログインが必要なページで、認証が成功すると、ユーザー名が表示されます。認証に失敗した場合は、自動的にログインページに移動します。
91+
92+ <video src = { simpleAuthenticationVideo } controls muted />
9293
9394<Tabs >
9495<TabItem value = " server" label = " サーバー" >
9596
96- ``` javascript title="schema.prisma (抜粋)"
97+ このアプリケーションでは、ユーザー情報を保存するテーブル` User ` と、セッション情報を保存するテーブル` Session ` の2つを持つデータベースを使用します。` Session ` テーブルには、一意でランダムなIDである` sessionId ` と、ユーザーのIDである` userId ` が保存されます。サーバーは、クライアントが` sessionId ` を知っていれば、その` userId ` に対応するユーザーとしてクライアントを認証します。つまり、この` sessionId ` が、前述の「証明書」に相当します。
98+
99+ なお、一般的に「セッション」とは、ある処理の開始から終了までの一連の流れを指しますが、ここHTTPの文脈における「セッション」は、ユーザーがログインしてからログアウトするまでの期間を指します。
100+
101+ ``` javascript title="schema.prisma (抜粋)" showLineNumbers
97102model User {
98103 id Int @id @default (autoincrement ())
99104 username String @unique
@@ -107,45 +112,51 @@ model Session {
107112}
108113```
109114
115+ ` /login ` は、IDとパスワードを含むJSON形式のPOSTリクエストを受け取り、データベースの` User ` テーブルのデータと比較することで、認証情報が正しいかどうかを検証します。正しければ、新しいレコードを` Session ` テーブルに作成し、その` sessionId ` をCookieとしてクライアントに送信します。誤っていれば、直ちに認証失敗の<Term >ステータスコード</Term >を返して終了します。
116+
110117``` javascript title="main.mjs (POST /login の抜粋)" showLineNumbers
111118app .post (" /login" , async (request , response ) => {
112119 if (! request .body .username || ! request .body .password ) {
113- response .sendStatus (400 );
120+ response .sendStatus (400 ); // Bad Request (リクエストの形式が不正)
114121 return ;
115122 }
116123 const user = await prismaClient .user .findUnique ({
117124 where: { username: request .body .username },
118125 });
119126 if (! user || user .password !== request .body .password ) {
120- response .sendStatus (401 );
127+ response .sendStatus (401 ); // Unauthorized (認証に失敗)
121128 return ;
122129 }
123130
124131 const session = await prismaClient .session .create ({
125132 data: { userId: user .id , sessionId: crypto .randomUUID () },
126133 });
127134 response .cookie (" sessionId" , session .sessionId );
128- response .send (200 );
135+ response .send (200 ); // OK (成功)
129136});
130137```
131138
139+ ` /profile ` は、GETリクエストを受け取り、リクエストを送信したユーザーの情報をJSON形式で返します。前半部分ではCookieから` sessionId ` を取得し、その値を用いて` Session ` テーブルからレコードを検索して認証します。セッション情報が見つかれば、その` userId ` に対応するユーザー情報を` User ` テーブルから取得し、その情報をレスポンスとして返します。
140+
141+ なお、この例では認証処理と実際の処理を同じ場所に記述していますが、実際のアプリケーションでは、認証はほとんどの場所で前段階として必要になるため、共通の実装として切り出しておくことが一般的です。
142+
132143``` javascript title="main.mjs (GET /profile の抜粋)" showLineNumbers
133144app .get (" /profile" , async (request , response ) => {
134145 // 認証
135146 if (! request .cookies .sessionId ) {
136- response .sendStatus (401 );
147+ response .sendStatus (401 ); // Unauthorized (認証に失敗)
137148 return ;
138149 }
139- const session = await client .session .findUnique ({
150+ const session = await prismaClient .session .findUnique ({
140151 where: { sessionId: request .cookies .sessionId },
141152 });
142153 if (! session) {
143- response .sendStatus (401 );
154+ response .sendStatus (401 ); // Unauthorized (認証に失敗)
144155 return ;
145156 }
146157
147158 // 実際の処理
148- const user = await client .user .findUnique ({
159+ const user = await prismaClient .user .findUnique ({
149160 where: { id: session .userId },
150161 });
151162 response .json ({ username: user .username });
@@ -155,9 +166,73 @@ app.get("/profile", async (request, response) => {
155166</TabItem >
156167<TabItem value = " client" label = " クライアント" >
157168
158- WIP
169+ クライアント側のアプリケーションは、認証が必要なページ` / ` とログインページ` /login ` の2から構成されています。
170+
171+ ` / ` では、ページが読み込まれたときに` /profile ` にGETリクエストを送信し、認証が成功すればユーザー名を表示します。認証に失敗した場合は、ログインページに移動します。
172+
173+ ``` html title="public/index.html" showLineNumbers
174+ <h1 >ホーム</h1 >
175+ <p >ようこそ!<span id =" username-display" ></span >さん!</p >
176+ ```
177+
178+ 4行目では、` fetch ` 関数の第2引数の[ ` credentials ` プロパティ] ( https://developer.mozilla.org/ja/docs/Web/API/RequestInit#credentials ) に` "include" ` を指定しています。これは、リクエストにCookieを含めるためのオプションです。指定しないと、認証に必要なCookieがサーバーに送信されず、認証に失敗してしまいます。
179+
180+ 5行目では、[ ` Response#ok ` ] ( https://developer.mozilla.org/ja/docs/Web/API/Response/ok ) プロパティを用いて、レスポンスの<Term >ステータスコード</Term >が200番台であるかどうかを確認しています。200番台以外のステータスコードを受け取った場合、認証に失敗したと判断し、ログインページに移動します。
181+
182+ 6行目のログインページへの移動には、[ ` location.replace ` 関数] ( https://developer.mozilla.org/ja/docs/Web/API/Location/replace ) を使用し、現在のページを履歴に残さずに移動させています。これにより、ブラウザの「戻る」ボタンが正しく機能するようになります。逆に、履歴を残したい場合は、[ ` location.href ` プロパティ] ( https://developer.mozilla.org/ja/docs/Web/API/Location/href ) にURLを代入します。
183+
184+ ``` javascript title="public/script.js" showLineNumbers
185+ const usernameDisplay = document .getElementById (" username-display" );
186+
187+ async function initialize () {
188+ const response = await fetch (" /profile" , { credentials: " include" });
189+ if (! response .ok ) {
190+ location .replace (" /login" );
191+ return ;
192+ }
193+ const profile = await response .json ();
194+ usernameDisplay .textContent = profile .username ;
195+ }
196+
197+ initialize ();
198+ ```
199+
200+ ` /login ` では、ユーザー名とパスワードを入力するフォームと、ログインボタンを用意します。ログインボタンがクリックされると、` /login ` にPOSTリクエストを送信します。認証に成功すれば、サーバーから送信されたCookieがブラウザに保存され、` / ` に移動します。認証に失敗した場合はアラートを表示します。
201+
202+ ``` html title="public/login/index.html" showLineNumbers
203+ <p >ユーザー名: <input id =" username-input" /></p >
204+ <p >パスワード: <input id =" password-input" type =" password" /></p >
205+ <button type =" button" id =" login-button" >ログイン</button >
206+ ```
207+
208+ ``` javascript title="public/login/script.js" showLineNumbers
209+ const usernameInput = document .getElementById (" username-input" );
210+ const passwordInput = document .getElementById (" password-input" );
211+
212+ document .getElementById (" login-button" ).onclick = async () => {
213+ const response = await fetch (" /login" , {
214+ method: " POST" ,
215+ headers: { " Content-Type" : " application/json" },
216+ body: JSON .stringify ({
217+ username: usernameInput .value ,
218+ password: passwordInput .value ,
219+ }),
220+ credentials: " include" ,
221+ });
222+ if (response .ok ) {
223+ alert (" ログインに成功しました" );
224+ location .replace (" /" );
225+ } else {
226+ alert (" ログインに失敗しました" );
227+ }
228+ };
229+ ```
159230
160231</TabItem >
161232</Tabs >
162233
163234<ViewSource url = { import .meta .url } path = " _samples/simple-authentication" />
235+
236+ 全体の処理の流れは、次の図のようになります。
237+
238+ ![ Cookieを利用して認証情報を保存するWebアプリケーションの動作] ( ./simple-authentication-flow.drawio.svg )
0 commit comments