【Golang】ECサイトで使えるユーザー管理機能を実装する



## はじめに

本記事は、Golangを使用したECサイトの基本的なユーザー管理機能の実装方法について記載しています。
  
セキュリティ対策についても言及していますが、 **本記事の実装があらゆる攻撃に対して安全であることを保証するものではありません。**  

実際に運用する際は、以下の点に十分注意してください：
- **最新のセキュリティ対策を適用する**（本記事の実装が古くなる可能性があるため）
- **専門家によるコードレビューを受ける**（より高い安全性を確保するため）
- **テスト環境で十分な検証を行う**（本番環境に導入する前に脆弱性を洗い出す）

**本記事を参考にした実装によるいかなる損害・損失についても、筆者は一切の責任を負いません。**  
自己責任で実装を行い、適切なセキュリティ対策を講じてください。

<br>
本記事では、主にGORMやSQLite, bcryptを利用した実装方法を記載していきます。<br>
それでは本編へ

## 📕 対象読者
- Golangの基本文法がわかる開発者
- 基本的なユーザー管理機能を実装したい方


## 🔍 目次
1. [GORMとは？](#1-gormとは)
2. [SQLiteとは？](#2-sqliteとは)
3. [ユーザー登録の実装](#3-ユーザー登録の実装)
4. [ログイン / ログアウトの実装](#4-ログイン--ログアウトの実装)
5. [ユーザー削除の実装](#5-ユーザー削除の実装)
6. [動作確認方法](#6-動作確認方法)
7. [セキュリティの考え方と対策](#7-セキュリティの考え方と対策)
8. [SQLiteエラーの解決方法](#8-sqliteエラーの解決方法)

---

## 1. GORMとは？

**GORM** とは、Golang用のオープンソースORM（Object-Relational Mapping）ライブラリです。データベースとのやりとりを簡単に行うことができます。

GORMを使うことで、SQLを書かずに以下のような処理を簡単に実装できます。
- データベースへの接続
- テーブルの作成・マイグレーション
- データの挿入・更新・削除・検索

本記事では、GORMを使用してSQLiteデータベースと連携し、ユーザー情報を管理します。

---

## 2. SQLiteとは？

**SQLite** は、軽量で自己完結型のリレーショナルデータベース管理システム（RDBMS）です。サーバーを必要とせず、アプリケーション内のファイルとしてデータを保存できるため、小規模なアプリケーションや開発環境に適しています。

GORMはSQLiteをサポートしており、本記事では `sqlite3` を使用してデータベースを管理します。

## 3. ユーザー登録の実装


GORMを用いたデータベース処理とパスワードのハッシュ化を行います。

<details>
<summary>ユーザー登録のコードを見る</summary>

```go
package main

import (
    "fmt"
    "log"
    "net/http"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/sqlite"
    "golang.org/x/crypto/bcrypt"
)

var db *gorm.DB

type User struct {
    ID       uint   `json:"id"`
    Username string `json:"username"`
    Password string `json:"password"`
}

func init() {
    var err error
    db, err = gorm.Open("sqlite3", "./users.db")
    if err != nil {
        log.Fatal(err)
    }
    db.AutoMigrate(&User{})
}

func registerUser(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodPost {
        username := r.FormValue("username")
        password := r.FormValue("password")
        
        hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
        if err != nil {
            http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            return
        }

        user := User{Username: username, Password: string(hashedPassword)}
        db.Create(&user)

        fmt.Fprintf(w, "User registered successfully!")
    }
}

func main() {
    http.HandleFunc("/register", registerUser)
    log.Fatal(http.ListenAndServe(":8080", nil))
}
```
</details>

---

## 4. ログイン / ログアウトの実装

### ✅ ログイン処理
ユーザーのパスワードをハッシュと比較し、認証を行います。

<details>
<summary>ログインのコードを見る</summary>

```go
func loginUser(w http.ResponseWriter, r *http.Request) {
    username := r.FormValue("username")
    password := r.FormValue("password")

    var user User
    db.Where("username = ?", username).First(&user)

    if user.ID == 0 {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }

    err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
    if err != nil {
        http.Error(w, "Invalid password", http.StatusUnauthorized)
        return
    }

    fmt.Fprintf(w, "Login successful!")
}

func main() {
    http.HandleFunc("/login", loginUser)
}
```
</details>

### ✅ ログアウト処理

<details>
<summary>ログアウトのコードを見る</summary>

```go
func logoutUser(w http.ResponseWriter, r *http.Request) {
    // クッキーを削除（過去の時間に設定することで無効化）
    http.SetCookie(w, &http.Cookie{
        Name:   "session_id",
        Value:  "",
        MaxAge: -1,
        Path:   "/",
    })

    fmt.Fprintf(w, "Logged out successfully!")
}

func main() {
    http.HandleFunc("/logout", logoutUser)
}
```
</details>

---

## 5. ユーザー削除の実装

ユーザーを削除するAPIを実装します。

<details>
<summary>ユーザー削除のコードを見る</summary>

```go
func deleteUser(w http.ResponseWriter, r *http.Request) {
    username := r.FormValue("username")
    
    var user User
    db.Where("username = ?", username).First(&user)

    if user.ID == 0 {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }

    db.Delete(&user)
    fmt.Fprintf(w, "User deleted successfully!")
}

func main() {
    http.HandleFunc("/delete", deleteUser)
}
```
</details>

---

## 6. 動作確認方法

実装した内容が正しく動作するか確認するため、以下の手順を実施します。<br>
下記はusernameが`testuser`, passwordが`1234`のユーザーで動作確認する場合の例です。

### プログラムの実行
```bash
cd {上記プログラムを保存したパス}
go run main.go(main.go以外で保存した場合は該当するファイル名)
```

### ユーザー登録
```bash
curl -X POST -d "username=testuser&password=1234" http://localhost:8080/register
```

### データベースにユーザーが登録されているかの確認
testuserが登録されていることを確認します。
```bash
sqlite3 users.db "SELECT * FROM users;"
```

### ログイン
```bash
curl -X POST -d "username=testuser&password=1234" http://localhost:8080/login -c cookies.txt
```
※ -c cookies.txt をつけることで、クッキーを保存し、ログアウト時に使用できるようにしています。

### ログアウト
```bash
curl -X GET http://localhost:8080/logout -b cookies.txt
```

### ユーザー削除
```bash
curl -X POST -d "username=testuser" http://localhost:8080/delete
```

### データベースでユーザーが削除されているかの確認
testuserが削除されていることを確認します。
```bash
sqlite3 users.db "SELECT * FROM users;"
```

---

## 7. セキュリティの考え方と対策

ECサイトのユーザー管理機能を実装する際は、以下のセキュリティ対策を必ず考慮しましょう。

### 🔐 **1. パスワードの安全な管理**
- ユーザーのパスワードは**平文で保存せず**、`bcrypt` などのハッシュ化手法を用いて暗号化する。
- ハッシュ化することで、万が一データベースが流出しても**パスワードが直接漏洩するリスクを軽減**できる。

### 🚨 **2. 適切なエラーハンドリング**
- **ユーザーが存在しない場合や、パスワードが間違っている場合でも「認証エラー」として統一する。**  
  - 例：`"ユーザー名またはパスワードが間違っています"`  
  - 具体的な理由を明かさないことで、**攻撃者に情報を与えないようにする**。

### 🔒 **3. HTTPの使用（本番環境では必須）**
- ユーザーの情報が**暗号化されていないHTTP通信**で送信されると、盗聴や改ざんのリスクがある。
- 本番環境では必ず**HTTPS**を使用し、**通信内容を暗号化**すること。

## 8. SQLiteエラーの解決方法

### ❌ `no such table: users` のエラーが出る場合
#### ✅ 対処法
- `go run main.go`を実行したパスと同じパスで`sqlite3`コマンドを実行しているか確認(`pwd` で確認)
- `users.db` が存在するか確認 (`ls -l users.db`)
- `db.LogMode(true)` を追加し、SQLの実行を確認
- `go run main.go` を適切なディレクトリで実行 (`pwd` で確認)

---

## あとがき

動作確認手順も示しているので、開発環境などで一度試してみてください。
何かありましたら、コメントお願いいたします。