ゴール
- ログインAPI実行時にJWTを生成する。
- ローカルストレージに生成したトークンを保存し、リクエスト時には保存したトークンをヘッダー
Authorization
に自動で付与されるようにする。 - ヘッダーに付与されたトークンを元に認証チェックを行うミドルウェアを作成する。
- Axiosのインターセプターで、401エラー発生時にログイン画面に遷移するようにする。
前提
CL(Vue)側実装
ログイン画面の実装
シンプルなログイン画面を実装しておきます。
<template>
<div>
<form>
<h1 class="border-bottom my-4">Login</h1>
<div class="mb-3">
<label for="exampleInputEmail1" class="form-label">Email address</label>
<input type="email" class="form-control" v-model="email" />
</div>
<div class="mb-3">
<label for="exampleInputPassword1" class="form-label">Password</label>
<input
type="password"
class="form-control"
autocomplete
v-model="password"
/>
</div>
<button type="button" class="btn btn-primary" @click="login()">
Login
</button>
</form>
</div>
</template>
<script>
export default {
data() {
return {
email: "",
password: "",
};
},
methods: {
async login() {
try {
const requestBody = {
email: this.email,
password: this.password,
};
const { data } = await this.axios.post("/login", requestBody);
console.log("GENERATED TOKEN:", data);
localStorage.setItem("accessToken", data);
this.$router.push({
name: "homeView",
});
} catch {
alert("Authorization failed!");
}
},
},
};
</script>
Axiosインターセプター設定
API実行時に401エラーが返却された場合は、作成したログイン画面に遷移するようにしようと思います。
API実行の度に上記の処理を書くのは面倒なので、エラーレスポンスを受けた際の共通処理として、インターセプターを定義します。
インターセプターとは、リクエスト送信時/レスポンス受信時に挟み込むことができる処理です。共通する処理はインターセプターとして定義することで共通化することができます。詳しくはこちらの記事で解説されています。
リクエスト
ローカルストレージに保存されたトークン(accessToken
)を取得して、リクエストヘッダー「Authorization
」を付与する処理を書きます。
myAxios.interceptors.request.use((config) => {
const accessToken = localStorage.getItem("accessToken");
config.headers = {
...config.headers,
Authorization: `Bearer ${accessToken}`,
};
return config;
});
Authorization
ヘッダーにJWTを付与する際は、JWTの前に「Bearer
」という文字列を付与することが一般的です。これはAuthorization
ヘッダーに含めるトークンがJWTであることを明示的にすることにより、不正なトークン形式が入れられないようにするなどの効果があります。
レスポンス
エラーレスポンスを受けた際に、ステータスが401の場合はローカルストレージに保存されたトークン(accessToken
)を削除して、ログイン画面(loginView
)に遷移させる処理を書きます。
myAxios.interceptors.response.use((response) => {
return response;
}, (error) => {
if (error.response.status === 401) {
alert("Your session has expired, please login again.");
localStorage.removeItem("accessToken");
router.push({
name: "loginView"
});
}
return Promise.reject(error);
});
API(Express)側実装
今回はJWTを使用した認証を実装します。
JWTとは、JSON Web Tokenの略で、何やら情報(ペイロード)を持たせることができる暗号のような文字列のことを指します。ログインしているユーザーをペイロードで指定したトークンの生成を行い、詳しい説明はこちらの記事を参照してください。
今回は、JWTの生成や検証をすることができるnpmパッケージ「jsonwebtoken」を使用します。以下コマンドでインストールをしてください。
npm install jsonwebtoken
sign
メソッドを使用してJWTを生成することができます。
const jwt = require("jsonwebtoken");
jwt.sign(
{ userId: 1, email: "whopper@example.com" }, // ペイロード(持たせる情報)
"SECRET_KEY", // 秘密鍵(.envファイル等で定義することを推奨します)
{
expiresIn: "100000" // オプション(主に有効期限/msを設定)
}
);
verify
メソッドを使用して検証を行います。
const jwt = require("jsonwebtoken");
jwt.verify(
token, // 検証対象のJWT
"SECRET_KEY" // JWT生成時に使用する秘密鍵
);
ログインAPIを実装
実際にログインAPIを実装していきます。
まずはエンドポイントの追加です。ログインAPIなので/login
にしましょう。実行するコントローラーメソッドはAuthController.login
とします。
const AuthController = require("./controllers/AuthController.js");
router.post("/login", AuthController.login);
コントローラーを追加します。主にAuthService
クラスのlogin
メソッドを実行する処理を書きます。メソッド実行時には、引数としてリクエストボディを渡します。
const AuthService = require("../services/AuthService");
module.exports = class AuthController {
static async login(request, response, next) {
try {
const { body } = request;
const authInfo = await new AuthService().login(body);
response.json(authInfo);
} catch (e) {
next(e);
}
}
};
サービスを追加します。ここでは受け取ったリクエストボディemail
とpassword
を元に、実際にトークンを生成する処理を書きます。
const jwt = require("jsonwebtoken");
const User = require("../models/user");
module.exports = class AuthService {
async login(body) {
const { email, password } = body;
const loggedInUser = await User.findOne({
where: {
email,
password,
},
});
if (!loggedInUser) throw new Error("Authorization failed!");
try {
const token = jwt.sign(
{ userId: loggedInUser.id, email: loggedInUser.email },
"SECRET_KEY",
{ expiresIn: "600000" } // 10 minutes
);
return token;
} catch {
throw new Error("Token generation failed!");
}
}
async signUp() {}
};
では、実際にログインAPIを実行してみます。users
テーブルに任意のレコードを追加しておいてください。
ログイン画面に遷移し、メールアドレスとパスワードを入力し、「Login」ボタンを押下してください。
認証情報が誤っていない場合は、コンソールに以下のようなログが出力されます。
GENERATED TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImVtYWlsIjoidGVzdEVtYWlsQGdtYWlsLmNvbSIsImlhdCI6MTY4OTcyOTYxMSwiZXhwIjoxNjg5NzI5NjEzfQ.vliqXAR7YRpGga2PXaH3nQyFhanaVsFxEcDBx-jGXc8
ローカルストレージを確認すると、生成されたJWTが保存されていることが確認できます。
ミドルウェアを実装
リクエストヘッダー「Authorization
」で指定されたトークンが、有効な場合はAPIの実処理を実行し、無効な場合は401エラーを返すミドルウェアを作成します。
このミドルウェアを認証が必要なAPIに適用することで、API認証を適用します。
const jwt = require("jsonwebtoken");
module.exports = async function verifyAccessToken(request, response, next) {
const token = request.headers.authorization.split(" ")[1];
if (!token) response.status(401).send("Token must be provided!");
try {
const decodedToken = jwt.verify(token, "SECRET_KEY");
console.log("=============AUTH TOKEN=============");
console.debug(decodedToken);
next();
} catch {
response.status(401).send("Authorization failed!");
}
};
作成したミドルウェアを、APIに適用していきます。
router.get("/users", verifyAccessToken, UsersController.fetchAll);
該当のAPIを実行してみます。こちらのリポジトリを使用している場合は、URL/
に遷移してください。「Get users」ボタンを押下すると、APIを実行することができます。
ミドルウェアが適用される為、未ログイン時またはトークンが無効(有効期限が切れているなど)の場合は、アラートが出力され、ログイン画面に遷移します。
試しにJWTの有効期限を短くしてみることで、有効期限が切れると401エラーが出力されることも確認できます。
jwt.sign(
{ userId: 1, email: "whopper@example.com" },
"SECRET_KEY",
{
expiresIn: "2000"
}
);
これで認証機能の実装が完了しました👏 最後まで読んでいただき、ありがとうございました。