0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【React×Go】 データベースMySQL(MariaDB)、AuthとJWTを使って登録とログイン機能を実装する

Posted at

フロントエンドに React (TypeScript)、バックエンドに Go、データベースに MySQL を使って、ユーザーの メールアドレス (Email) と パスワード を登録し、そのメールアドレスでユーザーを一意に取得する認証システムを作成する方法を説明します。

要点は次の通りです

フロントエンド (React): ユーザーの Email と パスワード を登録し、ログイン機能を実装。

バックエンド (Go): ユーザー情報を MySQL に保存し、ログイン時にメールアドレスで認証。

データベース (MySQL): ユーザー情報を Email と パスワード と共に保存、Email で一意にユーザーを特定。

ディレクトリ構成

ディレクトリ構成は、下記のとおりです。

└── ReactGoApp/
    ├── backend/
    │   ├── db/
    │   │   └── db.go
    │   ├── .env
    │   ├── go.mod
    │   ├── go.sum
    │   └── main.go
    └── frontend/
        ├── node_modules
        ├── public
        └── src/
            ├── App.tsx
            ├── Login.tsx
            ├── main.tsx
            ├── Register.tsx
            ├── .gitignore
            ├── eslint.config.js
            ├── index.html
            ├── package.json
            ├── package.lock.json
            ├── README.md/
            │   ├── tsconfig.json
            │   ├── tsconfig.app.json
            │   └── tsconfig.node.json
            └── vite.config.ts

1. バックエンド (Go)

必要なパッケージは下記です。

github.com/gorilla/mux: ルーティング用。

github.com/go-sql-driver/mysql: MySQL ドライバ。

github.com/dgrijalva/jwt-go: JWT トークンの生成と検証。

github.com/rs/corsCORS (Cross-Origin Resource Sharing) は、Webブラウザがリソースを異なるドメイン(オリジン)からロードすることを制御する仕組みです。このパッケージは、GoでCORSを簡単に設定するためのライブラリです。
用途: Web APIが他のドメインからのリクエストを許可するかどうかを制御できます。
例えば、フロントエンドが http://localhost:3000 で動作している場合に、バックエンドのAPIが http://localhost:8080 で動作しているときに、CORSエラーを回避するために使用します。

使用方法: これを使うと、必要なメソッド(GET, POST など)やオリジン(ドメイン)を指定して、リクエストを許可できます。

sample.go
import "github.com/rs/cors"

// CORSミドルウェアの設定
c := cors.New(cors.Options{
    AllowedOrigins: []string{"http://localhost:3000"},  // 許可するオリジン
    AllowedMethods: []string{"GET", "POST"},            // 許可するメソッド
})

http.Handle("/", c.Handler(r))  // rはmuxなどのルーター

encoding/json:このパッケージは、JSONデータのエンコード(GoのデータをJSON形式に変換)とデコード(JSON形式のデータをGoのデータに変換)を行います。
用途: 主にWeb APIで、HTTPリクエストやレスポンスのボディにJSONを使う際に使用されます。

使用方法:json.MarshalでGoのデータ構造をJSONに変換し、json.Unmarshal でJSONをGoのデータ構造に変換します。

sample.go
import "encoding/json"

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

// デコード
var p Person
err := json.Unmarshal([]byte(`{"name":"John", "age":30}`), &p)

// エンコード
jsonData, err := json.Marshal(p)

os
os パッケージは、オペレーティングシステムとのインターフェースを提供します。ファイル操作や環境変数の取得、プロセス情報の取得など、OSに関連する操作を行います。

用途: ファイルの読み書き、環境変数の取得、プログラムの終了コードの設定などに使用します。

使用方法: 環境変数を取得する、ファイルの作成・読み込み・削除を行う際に使用します。

sample.go
import "os"

// 環境変数の取得
value := os.Getenv("MY_ENV_VAR")

// ファイルの作成
file, err := os.Create("file.txt")
defer file.Close()

time
time パッケージは、日時の操作を行うための標準ライブラリです。日付や時刻の取得、比較、加算、フォーマットなどを行います。

用途: 日時の処理、タイムゾーンの管理、時間の経過を計算する際に使用されます。

使用方法: 現在の日時の取得や、指定された日時のフォーマット・計算を行います。

sample.go
import "time"

// 現在の時刻を取得
currentTime := time.Now()

// 時間の加算
newTime := currentTime.Add(time.Hour * 24)

// 時刻をフォーマット
formattedTime := currentTime.Format("2006-01-02 15:04:05")

github.com/dgrijalva/jwt-go
このパッケージは、JWT(JSON Web Tokens)の生成と検証を行います。JWTは、主に認証・認可のために使用されるトークンです。

用途: ユーザーの認証情報をトークンとして生成し、その後リクエストごとにそのトークンが正当かどうかを検証するために使用します。

使用方法:jwt.NewWithClaimsでトークンを生成し、jwt.Parse でそのトークンを検証します。

sample.go
import "github.com/dgrijalva/jwt-go"

// トークンの生成
claims := jwt.MapClaims{
    "userID": 123,
    "exp": time.Now().Add(time.Hour * 72).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signedToken, err := token.SignedString([]byte("your_secret_key"))

// トークンの検証
token, err := jwt.Parse(signedToken, func(token *jwt.Token) (interface{}, error) {
    return []byte("your_secret_key"), nil
})

github.com/joho/godotenv
このパッケージは、.env ファイルに記述された環境変数を読み込むためのライブラリです。
特に開発環境で設定する情報(APIキーやデータベース接続情報など)を .env ファイルに保存して管理する際に便利です。

用途: .env ファイルに記述されたキーと値を、Goアプリケーションで環境変数として読み込むために使用します。

使用方法: .env ファイルを読み込むために godotenv.Load() を使います。

sample.go
import "github.com/joho/godotenv"

err := godotenv.Load()
if err != nil {
    log.Fatal("Error loading .env file")
}

// 環境変数を取得
dbPassword := os.Getenv("DB_PASSWORD")

golang.org/x/crypto/bcrypt
bcrypt は、パスワードのハッシュ化を行うライブラリです。
パスワードを平文で保存するのは非常に危険ですが、bcrypt を使うと、セキュアにハッシュ化して保存できます。

用途: ユーザーのパスワードをハッシュ化してデータベースに保存したり、入力されたパスワードと保存されたハッシュを比較する際に使用します。

使用方法: bcrypt.GenerateFromPassword でパスワードをハッシュ化し、bcrypt.CompareHashAndPassword で確認します。

sample.go
import "golang.org/x/crypto/bcrypt"

// パスワードのハッシュ化
hashedPassword, err := bcrypt.GenerateFromPassword([]byte("userPassword"), bcrypt.DefaultCost)

// パスワードの検証
err = bcrypt.CompareHashAndPassword(hashedPassword, []byte("userPassword"))
if err != nil {
    // パスワードが一致しない場合
}

github.com/gorilla/handlersgorilla/mux を使用している場合、このパッケージでクロスオリジンリソースシェアリング(CORS)の設定

go get github.com/gorilla/mux
go get github.com/dgrijalva/jwt-go
go get github.com/go-sql-driver/mysql

1.1. Go サーバー設定

以下のコードでは、ユーザーの登録 (/register)、ログイン (/login)、および保護されたリソース (/profile) を実装しています。

まずは、main.goの実装を始めます。
こちらで、/Register/Loginへのルートハンドリングを行っています。
そして、後述するデータベースアクセスファイルdb.goのモジュールを読み込みます。こうすることで、db.go内の関数をmain.goの中で使うことができます。

main.go
(前略)

func main(){
    r := mux.NewRouter()
    r.HandleFunc("/register", db.Register).Methods("POST")
    r.HandleFunc("/login", db.Login).Methods("POST")
    // 保護されたプロファイルページ
    r.Handle("/profile", db.ValidateToken(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("This is a protected profile page"))
	})))
}

(後略)

全体のmain.goのコードはこちら⇩

backend/main.go
package main

import (
	"backend/db"
	"log"
	"net/http"

	"github.com/gorilla/mux"
	"github.com/rs/cors"
)

// メイン関数でサーバーを開始
func main() {
	r := mux.NewRouter()
	r.HandleFunc("/register", db.Register).Methods("POST")
	r.HandleFunc("/login", db.Login).Methods("POST")

	// 保護されたプロファイルページ
	r.Handle("/profile", db.ValidateToken(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("This is a protected profile page"))
	})))

	db := cors.AllowAll().Handler(r)
	log.Fatal(http.ListenAndServe(":8080", db)) // log.Fatal(http.ListenAndServe(":8080", nil))
}

つぎに、MySQL(MariaDB)を使ったデータベース接続処理を実装します。

MySQL ドライバのインストール

Goプロジェクトで github.com/go-sql-driver/mysql パッケージを使用するためには、依存関係をインストールする必要があります。以下のコマンドでインストールできます。

go get -u github.com/go-sql-driver/mysql

これで、MySQLドライバがプロジェクトにインストールされ、database/sql と組み合わせて使用できるようになります。

インポートパスの確認

インポートの際に、_ "github.com/go-sql-driver/mysql" のように、MySQLドライバを _ プレフィックスでインポートすることに注意してください。
これにより、ドライバがsqlパッケージに登録され、直接使用することはなくても、ドライバが自動的に利用されます。

依存関係の確認

Go 1.11 以降、Goはモジュールシステムを使用しています。
プロジェクトでモジュールが有効になっている場合は、go.mod ファイルがプロジェクトのルートに存在しているはずです。
もしまだgo.modが作成されていない場合は、次のコマンドでモジュールを初期化できます。

go mod init your_project_name

その後、再度依存関係をインストールするために以下のコマンドを実行します。

go mod tidy

これで、必要なパッケージがすべてインストールされます。

ここまでのポイント

1._ "github.com/go-sql-driver/mysql" を db.go のインポートに追加。

2.MySQL ドライバをインストール: go get -u github.com/go-sql-driver/mysql

3.モジュールが必要な場合: go mod init go mod tidy を実行。

外部パッケージからでもアクセスできように関数と変数を設定

Go では、関数や変数の名前が『 小文字 』で始まるとその関数や変数は パッケージ外からアクセスできません。"

register loginvalidateToken はすべて小文字で始まっていますが、これらの関数を main.go からアクセスできるようにするには、関数名の最初の文字を『 大文字 』に変更する必要があります。

 関数、変数   外部パッケージからのアクセス 
 先頭文字が小文字   外部パッケージからのアクセスできない 
 先頭文字が大文字   外部パッケージからのアクセスできる 

なので、ちょっと修正しておきましょう。

backend/db/db.go
package db

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	_ "github.com/go-sql-driver/mysql"

	"github.com/dgrijalva/jwt-go"
	"github.com/joho/godotenv"
	"golang.org/x/crypto/bcrypt"
)

var db *sql.DB

// JWTのシークレットキー🌟🌟
var jwtSecret []byte // var jwtSecret = []byte("your_secret_key")

// ユーザー構造体
type User struct {
	ID       int    `json:"id"`
	UserName string `json:"userName"`
	Email    string `json:"email"`
	Password string `json:"password"`
}

// DB接続の初期化
func init() {
	// .envファイルから環境変数をロード
	if err := godotenv.Load(); err != nil {
		log.Fatal("Error loading .env file")
	}
	// JWTシークレットキーを環境変数から取得
	jwtSecret = []byte(os.Getenv("JWT_SECRET"))
	if jwtSecret == nil {
		log.Fatal("JWT_SECRET environment variable is not set")
	}

	var err error
	db, err = sql.Open("mysql", "root:pass1234@tcp(127.0.0.1:3306)/todo_app")
	if err != nil {
		log.Fatal(err)
	}
}

// パスワードをハッシュ化する関数
func hashPassword(password string) string {
	// パスワードをハッシュ化
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		log.Fatal(err)
	}
	return string(hashedPassword)
}

// ユーザー登録ハンドラー
func Register(w http.ResponseWriter, r *http.Request) {
	var user User
	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&user); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	defer r.Body.Close()

	// 実際のプロダクションでは bcrypt などを使ってハッシュ化することを推奨
	user.Password = hashPassword(user.Password)

	// ユーザーのメールアドレスがすでに存在するかをチェック
	var existingUser User
	err := db.QueryRow("SELECT id, email FROM users WHERE email = ?", user.Email).Scan(&existingUser.ID, &existingUser.Email)
	if err == nil {
		http.Error(w, "Email already exists", http.StatusConflict)
		return
	}

	// ユーザーをデータベースに保存
	_, err = db.Exec("INSERT INTO users (username, email, password) VALUES (?, ?, ?)", user.UserName, user.Email, user.Password)
	if err != nil {
		http.Error(w, "Failed to register user", http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusCreated)
}

// ユーザーログインハンドラー
func Login(w http.ResponseWriter, r *http.Request) {
	var credentials User
	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&credentials); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	defer r.Body.Close()

	var user User
	// メールアドレスでユーザーを取得
	err := db.QueryRow("SELECT id, username, email, password FROM users WHERE email = ?", credentials.Email).Scan(&user.ID, &user.UserName, &user.Password)
	if err != nil {
		http.Error(w, "Invalid credentials", http.StatusAccepted)
		return
	}
	// JWTトークンを生成
	token, err := generateJWT(user.ID)
	if err != nil {
		http.Error(w, "Failed to generate token", http.StatusInternalServerError)
		return
	}
	// トークンをレスポンスに返す
	w.Header().Set("Content-type", "application/json")
	w.Write([]byte(fmt.Sprintf(`{"token":"%s"}`, &token)))
}

// JWTトークンの生成
func generateJWT(userID int) (string, error) {
	claims := &jwt.StandardClaims{
		Subject:   fmt.Sprintf("%d", userID),
		IssuedAt:  time.Now().Unix(),
		ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(jwtSecret)
}

// JWTトークンの検証ミドルウェア
func ValidateToken(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		tokenString := r.Header.Get("Authorization")
		if tokenString == "" {
			http.Error(w, "Missing token", http.StatusUnauthorized)
			return
		}
		tokenString = tokenString[len("Bearer"):]
		token, err := jwt.ParseWithClaims(tokenString, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
			return jwtSecret, nil
		})
		if err != nil || !token.Valid {
			http.Error(w, "Invalid token", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

/*
// メイン関数でサーバーを開始
func main() {
	r := mux.NewRouter()
	r.HandleFunc("/register", register).Methods("POST")
	r.HandleFunc("/login", login).Methods("POST")

	// 保護されたプロファイルページ
	r.Handle("/profile", validateToken(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("This is a protected profile page"))
	})))
	http.Handle("/", r)
	log.Fatal(http.ListenAndServe(":8080", nil))
}
*/

/*
var DB *gorm.DB

func Connect() {
	dsn := "root:pass1234@tcp(127.0.0.1:3306)/todo_app?charset=utf8mb4&parseTime=True&loc=Local"
	database, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("DB接続に失敗しました: " + err.Error())
	}
	DB = database
}
*/

.envにJWTトークンを記載します。好きな文字列を入力しましょう。
Go プロジェクトでこの.envファイルを読み込むためには、github.com/joho/godotenv を使って環境変数をロードします。

go get github.com/joho/godotenv
backend/.env
JWT_SECRET=<好きな文字列を入力>

2. フロントエンド (React + TypeScript)

React でユーザー登録とログインのフォームを作成し、バックエンドの API にリクエストを送信します。

必要なパッケージ

npx create-react-app auth-app --template typescript
cd auth-app
npm install axios

2.1. ユーザー登録フォーム

src/Register.tsx ファイルでユーザー登録フォームを作成します。

src/Register.tsx
import React, { useState } from 'react';
import axios from 'axios';

interface User {
  username: string;
  email: string;
  password: string;
}

const Register = () => {
  const [user, setUser] = useState<User>({ username: '', email: '', password: '' });
  const [error, setError] = useState<string | null>(null);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setUser((prev) => ({ ...prev, [name]: value }));
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError(null);
    try {
      await axios.post('http://localhost:8080/register', user);
      alert('User registered successfully');
    } catch (err) {
      setError('Registration failed. Try again.');
    }
  };

  return (
    <div>
      <h2>Register</h2>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          name="username"
          value={user.username}
          onChange={handleChange}
          placeholder="Username"
        />
        <input
          type="email"
          name="email"
          value={user.email}
          onChange={handleChange}
          placeholder="Email"
        />
        <input
          type="password"
          name="password"
          value={user.password}
          onChange={handleChange}
          placeholder="Password"
        />
        <button type="submit">Register</button>
      </form>
    </div>
  );
};

export default Register;

2.2. ログインフォーム

src/Login.tsx では、ユーザーがログインするフォームを作成し、バックエンドにリクエストを送信します。

src/Login.tsx
import React, { useState } from 'react';
import axios from 'axios';

interface LoginCredentials {
  email: string;
  password: string;
}

const Login = ({ setToken }: { setToken: React.Dispatch<React.SetStateAction<string | null>> }) => {
  const [credentials, setCredentials] = useState<LoginCredentials>({ email: '', password: '' });
  const [error, setError] = useState<string | null>(null);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setCredentials((prev) => ({ ...prev, [name]: value }));
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError(null);
    try {
      const response = await axios.post('http://localhost:8080/login', credentials);
      setToken(response.data.token);
    } catch (err) {
      setError('Invalid email or password');
    }
  };

  return (
    <div>
      <h2>Login</h2>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <form onSubmit={handleSubmit}>
        <input
          type="email"
          name="email"
          value={credentials.email}
          onChange={handleChange}
          placeholder="Email"
        />
        <input
          type="password"
          name="password"
          value={credentials.password}
          onChange={handleChange}
          placeholder="Password"
        />
        <button type="submit">Login</button>
      </form>
    </div>
  );
};

export default Login;

2.3. メインアプリケーション

src/App.tsx で、ログイン後にトークンを状態として保存し、保護されたリソースを表示することができます。

src/App.tsx
import React, { useState } from 'react';
import Login from './Login';
import Register from './Register';
import axios from 'axios';

const App = () => {
  const [token, setToken] = useState<string | null>(null);

  const fetchProfile = async () => {
    try {
      const response = await axios.get('http://localhost:8080/profile', {
        headers: { Authorization: `Bearer ${token}` },
      });
      console.log(response.data);
    } catch (error) {
      console.error('Error fetching profile', error);
    }
  };

  return (
    <div>
      {!token ? (
        <>
          <Register />
          <Login setToken={setToken} />
        </>
      ) : (
        <div>
          <h1>Welcome!</h1>
          <button onClick={fetchProfile}>Fetch Profile</button>
        </div>
      )}
    </div>
  );
};

export default App;

3. MySQL データベース

データベースにテーブルを作成します。

sample.sql
CREATE DATABASE auth_db;

USE auth_db;

CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(100) NOT NULL,
  email VARCHAR(255) NOT NULL UNIQUE,
  password VARCHAR(255) NOT NULL
);

4. 実行方法

1.バックエンドの Go アプリケーションを実行します。

go run main.go

2.フロントエンドの React アプリケーションを実行します。

npm start

これで、ユーザー登録、ログイン、JWT 認証を含むシステムが動作します。ユーザーは Email を使って一意に認証され、JWT トークンを用いて保護されたリソースにアクセスできます。

アプリ作成時のトラブルシューティング

1.undefined: hashPasswordと表示された

エラーメッセージ undefined: hashPassword は、hashPassword 関数が定義されていないことを意味しています。
この関数を定義していないため、パスワードをハッシュ化しようとしたときにエラーが発生します。

修正案:

hashPassword 関数を追加しましょう。
例えば、bcrypt パッケージを使ってパスワードをハッシュ化する方法です。
bcrypt パッケージをインポートして、ハッシュ化処理を行います。

main.go
import (
	"golang.org/x/crypto/bcrypt"
)

func hashPassword(password string) string {
	// パスワードをハッシュ化
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		log.Fatal(err)
	}
	return string(hashedPassword)
}

bcrypt をインストールするためには、以下のコマンドを使ってパッケージをインストールします。

go get -u golang.org/x/crypto/bcrypt

2. cannot use tokenString[len("Bearer")] (value of type byte) as string value in assignment

tokenString[len("Bearer")] では、byte 型の1文字が返されているため、これを string 型として使おうとしてエラーが発生しています。tokenString[len("Bearer")] ではなく、tokenString[len("Bearer "):] として、文字列のスライスを取得する必要があります。

修正案:

tokenString の先頭から"Bearer "を取り除くために、以下のようにスライスを修正します。

main.go
tokenString = tokenString[len("Bearer "):]

3. token.valid undefined (type *jwt.Token has no field or method valid, but does have field Valid)

JWT トークンの検証でvalidではなく、Valid というフィールドを使う必要があります。
Goのコードではフィールド名が大文字で始まるものは公開されているため、valid Validに変更する必要があります。

修正案:

以下のように、Valid フィールドを使います。

main.go
if err != nil || !token.Valid {
	http.Error(w, "Invalid token", http.StatusUnauthorized)
	return
}

完成したコード

これで、コードが修正された状態になります。最終的な修正後のコードは次のようになります。

main.go
package db

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/dgrijalva/jwt-go"
	"github.com/gorilla/mux"
	"golang.org/x/crypto/bcrypt"
)

var db *sql.DB

// JWTのシークレットキー
var jwtSecret = []byte("your_secret_key")

// ユーザー構造体
type User struct {
	ID       int    `json:"id"`
	UserName string `json:"userName"`
	Email    string `json:"email"`
	Password string `json:"password"`
}

// DB接続の初期化
func init() {
	var err error
	db, err = sql.Open("mysql", "root:password@tcp(127.0.0.1:3306)/todo")
	if err != nil {
		log.Fatal(err)
	}
}

// パスワードをハッシュ化する関数
func hashPassword(password string) string {
	// パスワードをハッシュ化
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		log.Fatal(err)
	}
	return string(hashedPassword)
}

// ユーザー登録ハンドラー
func register(w http.ResponseWriter, r *http.Request) {
	var user User
	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&user); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	defer r.Body.Close()

	// パスワードはハッシュ化する
	user.Password = hashPassword(user.Password)

	// ユーザーのメールアドレスがすでに存在するかをチェック
	var existingUser User
	err := db.QueryRow("SELECT id, email FROM users WHERE email = ?", user.Email).Scan(&existingUser.ID, &existingUser.Email)
	if err == nil {
		http.Error(w, "Email already exists", http.StatusConflict)
		return
	}

	// ユーザーをデータベースに保存
	_, err = db.Exec("INSERT INTO users (username, email, password) VALUES (?, ?, ?)", user.UserName, user.Email, user.Password)
	if err != nil {
		http.Error(w, "Failed to register user", http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusCreated)
}

// ユーザーログインハンドラー
func login(w http.ResponseWriter, r *http.Request) {
	var credentials User
	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&credentials); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	defer r.Body.Close()

	var user User
	// メールアドレスでユーザーを取得
	err := db.QueryRow("SELECT id, username, email, password FROM users WHERE email = ?", credentials.Email).Scan(&user.ID, &user.UserName, &user.Password)
	if err != nil {
		http.Error(w, "Invalid credentials", http.StatusAccepted)
		return
	}
	// JWTトークンを生成
	token, err := generateJWT(user.ID)
	if err != nil {
		http.Error(w, "Failed to generate token", http.StatusInternalServerError)
		return
	}
	// トークンをレスポンスに返す
	w.Header().Set("Content-type", "application/json")
	w.Write([]byte(fmt.Sprintf(`{"token":"%s"}`, token)))
}

// JWTトークンの生成
func generateJWT(userID int) (string, error) {
	claims := &jwt.StandardClaims{
		Subject:   fmt.Sprintf("%d", userID),
		IssuedAt:  time.Now().Unix(),
		ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(jwtSecret)
}

// JWTトークンの検証ミドルウェア
func validateToken(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		tokenString := r.Header.Get("Authorization")
		if tokenString == "" {
			http.Error(w, "Missing token", http.StatusUnauthorized)
			return
		}
		tokenString = tokenString[len("Bearer "):] // 修正点
		token, err := jwt.ParseWithClaims(tokenString, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
			return jwtSecret, nil
		})
		if err != nil || !token.Valid { // 修正点
			http.Error(w, "Invalid token", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

// メイン関数でサーバーを開始
func main() {
	r := mux.NewRouter()
	r.HandleFunc("/register", register).Methods("POST")
	r.HandleFunc("/login", login).Methods("POST")

	// 保護されたプロファイルページ
	r.Handle("/profile", validateToken(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("This is a protected profile page"))
	})))
	http.Handle("/", r)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

サイト

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?