JWT認証について簡単にまとめました。
やることはバックエンドはRailsでGemを使ってトークンの発行をします。
フロントではログイン画面を作成しAxiosを使ってRailsのAPIを利用します。
ヘッダーにはトークンの所持の有無を検証させて持ってない場合はログイン画面にリダイレクトさせます。
ログアウト機能はローカルストレージに保存したトークンを削除することで実装とします。
本格的ではありませんが認証の基本を理解してもらえたら嬉しいです。
※railsとReactの説明はしません基本的なことは理解できている前提で進めます。
そもそも認証とは
システムがユーザーの身元を確認するプロセスのことです。
これにより、ユーザーは自分がアカウントの所有者であることを証明できます。
JWTとは
JWT(JSON Web Token)は、情報を安全に伝達するためのコンパクトでURLセーフな方法を提供する、オープンスタンダード(RFC 7519)です。
JWTは3つの部分から成り立っています
- ヘッダー(Header): トークンのタイプと使用している暗号化アルゴリズムを指定します。
- ペイロード(Payload): トークンに含める情報(クレーム)を指定します。これには、ユーザーの詳細やトークンの有効期限などが含まれます。
- 署名(Signature): ヘッダーとペイロードを結合し、秘密鍵を使用して署名します。これにより、情報が改ざんされていないことを確認できます。
これら3つの部分はピリオド(.)で連結され、一つの文字列となります。
JWTのメリットとデメリット
メリット4つ挙げます。
- ステートレス性: サーバー側にセッション情報を保存する必要がない
- 自己完結型: JWTは、ペイロード部分に認証情報やその他の情報を含むことができます。これにより、システム間で状態を共有する必要がなく、複数のサービス間で使い回すことが可能です。
- 強いセキュリティ: JWTは秘密鍵を使用して署名されているため、内容の改ざんが防げます。
- クロスドメイン認証: Cookieを使用するとクロスドメインの問題が発生する可能性がありますが、JWTはその制限を受けません。
デメリット4つ挙げます。
- データ暗号化の欠如: JWTは署名を使用してデータの改ざんを防ぎますが、ペイロードの情報は基本的に暗号化されていません。機密性の高い情報は含めないように注意が必要です。
- サイズ: JWTは比較的大きいです。これは特にモバイル環境でのデータ使用量に影響を及ぼす可能性があります。
- セッション無効化: JWTはステートレスであるため、一度発行されたJWTを無効にすることが難しいです。これはログアウト機能の実装やセキュリティ上の問題に影響を及ぼす可能性があります。
- エクスパイア時間の管理: JWTの有効期限を適切に管理しないと、長期間有効なトークンが悪用されるリスクがあります。逆に短すぎるとユーザー体験が低下する可能性があります。
以上で挙げたようにメリットとデメリットを理解した上で適切な運用が求められます。今回は自己完結型である特性を活かしてReactとRails間で認証情報を使い回します。
まずはRails側から作業していきます。
使用するGem
gem 'bcrypt' # パスワード専用のハッシュ値を作成してくれる
gem 'jwt' #jsonトークンをエンコードしたりデコードしてくれる
Gemfileに記載してbundle installします。
モデルを作成
rails g model User email:string password_digest:string
class User < ApplicationRecord
has_secure_password
end
ユーザーモデルを作成して認証情報を保存していきます。
カラム名をパスワードダイジェストにすると自動で暗号化してくれます。
コントローラーを作成
ユーザーコントローラーはトークンを返すcreateアクションとログインアクションを持ちます。
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
token = create_token(@user.id)
render json: {token: token}
else
render json: {errors: @user.errors}, status: :unprocessable_entity
end
end
def login
@user = User.find_by_email(user_params[:email])
if @user && @user.authenticate(user_params[:password])
token = create_token(@user.id)
render json: {token: token}
else
render status: :unauthorized
end
end
private
def user_params
params.require(:user).permit(:email, :password)
end
end
アプリケーションコントローラーに具体的なトークン生成と認証の処理を書きました。
def authenticate
authorization_header = request.headers[:authorization]
if !authorization_header
render status: :unauthorized
else
token = authorization_header.split(" ")[1]
secret_key = Rails.application.secrets.secret_key_base[0]
decoded_token = JWT.decode(token, secret_key)
@user = User.find(decoded_token[0]["user_id"])
end
end
def create_token(user_id)
payload = {user_id: user_id}
secret_key = Rails.application.secrets.secret_key_base[0]
token = JWT.encode(payload, secret_key)
return token
end
end
React
axiosを使ってRailsのAPIにアクセスしています。
import React, { useState } from "react";
import { axios } from "axios";
export const LoginJwt = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (event) => {
event.preventDefault();
try {
const response = await axios.post('/api/login', {
user: {
email,
password
}
});
localStorage.setItem('token', response.data.token);
console.log('Login successful');
} catch (error) {
console.error('Error logging in', error);
}
};
return (
<form onSubmit={handleSubmit}>
<label>
Email:
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
</label>
<label>
Password:
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
</label>
<input type="submit" value="Login" />
</form>
);
}
ヘッダーにトークンを所持しているか確認するエフェクトを書いています。
持っていない場合は/パスにリダイレクトします。
import React, { useEffect } from "react";
import { Header } from "../organisms/layout/Header";
export const HeaderLayout = (props) => {
const { children } = props;
const token = localStorage.getItem("token");
useEffect(() => {
if (!token) {
window.location.href = "/";
}
}, [token]);
return (
<>
<Header />
{children}
</>
);
};
参考
ChatGPTにRailsとReactとjwtとbcryptのGemを使ってJWT認証したいと打ち込みました。