Skip to content

Commit 90a6d13

Browse files
committed
2つめのセクション執筆完了
1 parent abd567a commit 90a6d13

File tree

8 files changed

+679
-164
lines changed

8 files changed

+679
-164
lines changed

docs/3-web-servers/07-fetch-api-post/http-message.drawio.svg

Lines changed: 2 additions & 2 deletions
Loading

docs/4-advanced/01-cookie/_samples/simple-authentication/main.mjs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,35 @@ app.use(express.static("./public"));
1111

1212
app.post("/login", async (request, response) => {
1313
if (!request.body.username || !request.body.password) {
14-
response.sendStatus(400);
14+
response.sendStatus(400); // Bad Request (リクエストの形式が不正)
1515
return;
1616
}
1717
const user = await prismaClient.user.findUnique({
1818
where: { username: request.body.username },
1919
});
2020
if (!user || user.password !== request.body.password) {
21-
response.sendStatus(401);
21+
response.sendStatus(401); // Unauthorized (認証に失敗)
2222
return;
2323
}
2424

2525
const session = await prismaClient.session.create({
2626
data: { userId: user.id, sessionId: crypto.randomUUID() },
2727
});
2828
response.cookie("sessionId", session.sessionId);
29-
response.send(200);
29+
response.send(200); // OK (成功)
3030
});
3131

3232
app.get("/profile", async (request, response) => {
3333
// 認証
3434
if (!request.cookies.sessionId) {
35-
response.sendStatus(401);
35+
response.sendStatus(401); // Unauthorized (認証に失敗)
3636
return;
3737
}
3838
const session = await prismaClient.session.findUnique({
3939
where: { sessionId: request.cookies.sessionId },
4040
});
4141
if (!session) {
42-
response.sendStatus(401);
42+
response.sendStatus(401); // Unauthorized (認証に失敗)
4343
return;
4444
}
4545

docs/4-advanced/01-cookie/_samples/simple-authentication/public/login/script.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ document.getElementById("login-button").onclick = async () => {
1313
});
1414
if (response.ok) {
1515
alert("ログインに成功しました");
16-
window.location.href = "/";
16+
location.replace("/");
1717
} else {
1818
alert("ログインに失敗しました");
1919
}

docs/4-advanced/01-cookie/_samples/simple-authentication/public/script.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const usernameDisplay = document.getElementById("username-display");
33
async function initialize() {
44
const response = await fetch("/profile", { credentials: "include" });
55
if (!response.ok) {
6-
window.location.href = "/login";
6+
location.replace("/login");
77
return;
88
}
99
const profile = await response.json();

docs/4-advanced/01-cookie/cookie-counter-flow.drawio.svg

Lines changed: 141 additions & 141 deletions
Loading

docs/4-advanced/01-cookie/index.mdx

Lines changed: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ title: Cookieと認証(発展)
44

55
import cookieCounterVideo from "./cookie-counter.mp4";
66
import 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+
3942
Expressを用いて`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
97102
model 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
111118
app.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
133144
app.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

Comments
 (0)