LoginSignup
4
2

ReactとRailsを使ったJWT認証。

Posted at

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認証したいと打ち込みました。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2