bcryptのハッシュ値は毎回違うのに、なぜログインできるの？【"ソルト"の役割を徹底解説】

![15855489588_6c209780a9_w.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4041306/d012bbe5-539a-4c99-9ad4-8e600e4d68c4.jpeg)
> 画像提供元：[www.bluecoat.com/](https://www.bluecoat.com)

## はじめに

ポートフォリオでログイン機能を作っていて  
「bcryptで生成されるbcryptハッシュが毎回違うのに、どうやって照合してるのだろう？」  
と疑問に思いました。

この記事では、パスワードのハッシュ化とその照合の仕組みを、**ソルトの役割**を中心にわかりやすく解説していきます。

- ハッシュって暗号化と何が違うの？
- ソルトってどこにあるの？
- なぜ“戻さずに”照合できるの？

など、同じ疑問を持った方の参考になれば嬉しいです。

---



## 「ハッシュ化」と「暗号化」の違い

ハッシュ化」と「暗号化」はどちらもセキュリティに関する用語ですが、**仕組みも使い方も全く別物**です。

| 用語     | できること                 | 戻せるか     | 例        |
|----------|----------------------------|--------------|-------------------|
| ハッシュ化 | 値を一方向に変換して照合に使う | ❌ 戻せない     | bcrypt, SHA-256  |
| 暗号化   | 第三者に読まれないように変換 | ⭕ 鍵があれば戻せる | AES, RSA         |

たとえば、**パスワードの保存**には暗号化ではなく、ハッシュ化が使われます。  
bcryptはハッシュ関数なので、**元のパスワードを復元することはできません**。

---



## なぜ毎回ハッシュ値が違うのか？→ソルトの存在があるから

:::note warn
説明する前に混乱しやすいのでハッシュ値全体（60文字）のことを**bcryptハッシュ**、ランダムに生成されるハッシュ（22文字）のことを**ハッシュ**と呼ぶことにします。
:::

同じパスワードを `bcrypt.encode("password123")` に通しても、毎回異なるbcryptハッシュが出力されます。
これは、**「ソルト（salt）」と呼ばれるランダムな文字列**を毎回くっつけてからハッシュ化しているからです。

実際に出力されたbcryptハッシュを見てみましょう：

```txt
$2a$10$XgL3rU8w/8TzYFq8BzYZpuXlfW.OJvlmDiOjclNVQ3cT3FkgQ9Hmu
```

bcryptハッシュの長さは「常に60文字」です。  
内訳は、`バージョン4 + コスト3 + ソルト22 + ハッシュ31 = 計60文字`

**また、バージョンとコストは固定の文字列のため毎回同じものが出力されます。**

```txt
$2a$  10$   XgL3rU8w/8TzYFq8BzYZpu   XlfW.OJvlmDiOjclNVQ3cT3FkgQ9Hmu
```

| パーツ                        | 文字位置       | 内容                               |
|------------------------------|----------------|------------------------------------|
| `$2a$` （固定）                       | 1〜4文字目     | アルゴリズムのバージョン（bcryptを表す） |
| `10$` （固定）                        | 5〜7文字目     | コストファクター（セキュリティ強度）     |
| `XgL3rU8w/8TzYFq8BzYZpu`      | 8〜29文字目    | ソルト（ランダムな文字列）        |
| `XlfW.OJvlmDiOjclNVQ3cT3FkgQ9Hmu` | 30文字目〜末尾 | ハッシュ（ソルトに基づいて生成されるため**毎回異なる**） |

このように、**毎回ランダムなソルトが付与されるため、同じパスワードでもbcryptハッシュが毎回異なる**という仕組みになっています。

でも、そうなると次の疑問が生まれますよね。

> 「じゃあ、どうやって照合してるの？」

この疑問については、次のセクションで `matches()` の仕組みを見ながら解説していきます。

---



## matches() はどうやって照合してるの？（実際のコードは解説後に載せます）

「同じパスワードでも毎回ハッシュ値が違うのに、どうやってログイン時に一致判定できるの？」
これは、bcryptの**matches() メソッドが再ハッシュを行っているから**です。

ここで重要なのは、

> 照合時に「ハッシュを復元」しているのではなく、「もう一度ハッシュ化して比較」している

という点です。

---



## matches() の中で行われている処理
1. データベースに保存されているbcryptハッシュから、
 ソルトとコスト情報を抽出する
1. 入力されたパスワードに、同じソルトとコストを使って再ハッシュを行う
1. その結果が、保存されたbcryptハッシュと一致するかを比較する

### イメージ図
```text
[登録時]
password123 + ソルトA（ランダム生成）
↓
bcryptハッシュ→ 保存

[ログイン時]
入力：password123
↓
保存されたbcryptハッシュからソルトAを抽出
↓
password123 + ソルトA → 再ハッシュ（bcryptハッシュ）
↓
再ハッシュ結果と保存済みハッシュを比較 → 一致したらOK！
```

✅ **分かりやすく言うと…**
ソルト（22文字）の部分が毎回ランダム生成され、
ハッシュの結果（31文字）もソルトごとに異なります。

そのため、たとえハッシュ化のロジックがすべて公開されていても、  
**bcryptハッシュから元のパスワードを逆算して復元することは非常に困難**というわけです。

> 🔒 ちなみにこの考え方は「ケルクホフスの原理」と呼ばれていて、  
> 暗号は「ロジックがすべて公開されていても安全である」という原則に基づいて設計されています。

さて、実際のコードを見ていきましょう！



## 検証用：ハッシュ値を生成するだけの簡単コード

このコードは、**実際にアプリで使う前に「bcryptの挙動を確かめる」ための
動作確認用**です。

```java
package com.ozeken.expensecalendar.util;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

public class PasswordGenerator {
    public static void main(String[] args) {
        PasswordEncoder encoder = new BCryptPasswordEncoder();
        
        String rawPassword = "password123"; // ここに生成したいパスワードを入力
        String encodedPassword = encoder.encode(rawPassword);

        System.out.println("元のパスワード: " + rawPassword);
        System.out.println("ハッシュ化されたパスワード: " + encodedPassword);
    }
}
```
### 出力結果
1回目: `$2a$10$bAvJgvuhfHIwcbRZz71hJOJCAtJ7Ol7.YQZqJKMyqOrl/E3hQNYYS`
2回目: `$2a$10$5bsWaoE1PJQEK.gsnkXPtu6MtNNKjbIRqUuyJ6HE8D4RXv7YYcHWi`
3回目: `$2a$10$NDFGi4PVtC0tw30bAC9oj.XE/AHK2nKcIch/bot36CHe7FhgkLAA6`

毎回ソルト（22文字）と、ソルトによってハッシュされた結果（31文字）が異なるので意図した動作です。



## アプリ本番用：パスワードエンコーダーの設定クラス

Spring Securityでログイン認証などを行う場合、  
このように `@Bean` でDIコンテナに格納し、エンコーダーをアプリ全体に提供するのが一般的です。

```java
package com.ozeken.expensecalendar.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/** パスワードエンコーダーの設定 */
@Configuration
public class PasswordConfig {
	
	/** BCrypt を使ったパスワードエンコーダーを提供 */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
```



## `PasswordEncoder` インターフェース

Spring Security の `PasswordEncoder` インターフェースは以下のように定義されています。
（`BCryptPasswordEncoder`は`PasswordEncoder`実装クラス）

> ※以下のコードは、Apache License 2.0 に基づいて提供されている Spring Security プロジェクトの一部コードを要約・引用したものです。  
> 出典：[Spring Security GitHub リポジトリ](https://github.com/spring-projects/spring-security)  
> ライセンス: [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0)
 ```java
 public interface PasswordEncoder {
 
     // パスワードをハッシュ化
     String encode(CharSequence rawPassword);
     // 照合処理
     boolean matches(CharSequence rawPassword, String encodedPassword);
     
 }
 ```

### 各メソッドの役割（先ほど説明しましたがもう一度説明します。）

・`encode()`  
  ⇒ パスワードを受け取り、ハッシュ化して返します。

・`matches()`  
  ⇒ パスワードと保存されたハッシュを受け取り、以下の流れで照合を行います：
  
  1. 保存されたハッシュからソルトとコスト情報を抽出
  1. 入力されたパスワードにそれらを使って再ハッシュ
  1. 再ハッシュ結果と保存されたハッシュを比較

---


## まとめ

bcryptは、**ソルト（22文字）を毎回ランダムに生成**することで、  
  同じパスワードでも異なるbcryptハッシュを出力します。

`matches()` メソッドは、**保存されたbcryptハッシュからソルトを抽出**し、  
  入力パスワードで**再ハッシュして比較**しているだけで、復元はしていません。

**ソルトが違えば結果も変わるため、たとえbcryptハッシュの仕組みが公開されていても、  
  パスワードの逆算は不可能です（＝ケルクホフスの原理）**

---

✅ bcryptは「見えても破られない」仕組みで設計されているため、  
安心してログイン機能に使うことができます！

最後まで読んでいただきありがとうございました！