- ๊ธฐ์กด ์์คํ ๋ถ์
- Passkey ํตํฉ ๋ฐฉ์
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ตฌ์กฐ ๋ณ๊ฒฝ
- ์๋น์ค ๋ ์ด์ด ์ค๊ณ
- ์ธ์ฆ ํ๋ฆ ์ค๊ณ
- ๋ง์ด๊ทธ๋ ์ด์ ์ ๋ต
- ๊ตฌํ ์ฐ์ ์์
- ์ค๋ฉ ๋ฐฉ์: UUID ๊ธฐ๋ฐ ํด์ ์ค๋ฉ
- ํ
์ด๋ธ ๋ช
๋ช
๊ท์น:
users_001,users_002, ... (3์๋ฆฌ ํจ๋ฉ) - ์ค๋ ํค: UUID (v4)
- ์ค๋ ๊ฒฐ์ ์๊ณ ๋ฆฌ์ฆ: CRC32 ํด์ โ
hash % shard_count + 1 - ์ค์ ํ์ผ:
config/shard.json
// ์ฃผ์ ๋ฉ์๋
- getShardNumber($uuid): ์ค๋ ๋ฒํธ ๊ณ์ฐ
- getShardTableName($uuid): ์ค๋ ํ
์ด๋ธ๋ช
์กฐํ
- getUserByUuid($uuid): UUID๋ก ์ฌ์ฉ์ ์กฐํ (์ ์ฒด ์ค๋ ๊ฒ์)
- getUserByEmail($email): ์ด๋ฉ์ผ๋ก ์ฌ์ฉ์ ์กฐํ (์ธ๋ฑ์ค ํ
์ด๋ธ ํ์ฉ)
- createUser($data): ์ฌ์ฉ์ ์์ฑ (์๋ ์ค๋ ๊ฒฐ์ )
- updateUser($uuid, $data): ์ฌ์ฉ์ ์
๋ฐ์ดํธuser_email_index: ์ด๋ฉ์ผ โ UUID ๋งคํ (๋น ๋ฅธ ์กฐํ)user_username_index: ์ฌ์ฉ์๋ช โ UUID ๋งคํuser_uuid_index: UUID โ ์ค๋ ID ๋งคํ (์บ์)
-
๋ก๊ทธ์ธ (
Login/SubmitController)- ์ด๋ฉ์ผ/๋น๋ฐ๋ฒํธ ๊ฒ์ฆ
- ์ค๋ฉ ํ๊ฒฝ:
Shard::getUserByEmail()โ ์ค๋ ํ ์ด๋ธ์์ ์กฐํ - ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ:
Hash::check() - JWT ํ ํฐ ์์ฑ ๋๋ ์ธ์ ์์ฑ
-
ํ์๊ฐ์ (
Register/StoreController)- ์ ๋ ฅ๊ฐ ๊ฒ์ฆ
- ์ด๋ฉ์ผ ์ค๋ณต ์ฒดํฌ (์ค๋ฉ ๊ณ ๋ ค)
- UUID ์์ฑ โ ์ค๋ ๊ฒฐ์ โ ์ค๋ ํ ์ด๋ธ์ ์ฝ์
- ์ธ๋ฑ์ค ํ ์ด๋ธ ์ ๋ฐ์ดํธ
ShardedUser: ์ค๋ฉ๋ ์ฌ์ฉ์ ๋ชจ๋ธ (UUID ๊ธฐ๋ฐ)User: ๊ธฐ๋ณธ ์ฌ์ฉ์ ๋ชจ๋ธ (ID ๊ธฐ๋ฐ, ์ค๋ฉ ๋นํ์ฑํ ์ ์ฌ์ฉ)
-
์ธ๋ํค ์ ์ฝ:
webauthn_credentialsํ ์ด๋ธ์ดuser_id๋ฅผ ์ธ๋ํค๋ก ์ฌ์ฉ- ์ค๋ฉ ํ๊ฒฝ์์๋
user_id๊ฐ ์ค๋๋ณ๋ก ์ค๋ณต๋ ์ ์์ - UUID ๊ธฐ๋ฐ ๊ด๊ณ๊ฐ ํ์ํจ
- ์ค๋ฉ ํ๊ฒฝ์์๋
-
์๊ฒฉ ์ฆ๋ช ์กฐํ: Passkey ์๊ฒฉ ์ฆ๋ช ์ ์ฌ์ฉ์ UUID๋ก ์กฐํํด์ผ ํจ
- ํ์ฌ ๊ตฌ์กฐ๋
user_id๊ธฐ๋ฐ ์กฐํ๋ง ์ง์
- ํ์ฌ ๊ตฌ์กฐ๋
-
์ธ์ฆ ํ๋ฆ ํตํฉ: ๊ธฐ์กด ์ด๋ฉ์ผ/๋น๋ฐ๋ฒํธ์ Passkey๋ฅผ ๋์์ ์ง์ํด์ผ ํจ
- ๋ก๊ทธ์ธ ์ปจํธ๋กค๋ฌ์์ ๋ ๋ฐฉ์์ ๋ชจ๋ ์ฒ๋ฆฌํด์ผ ํจ
ํ์ฌ ๊ตฌ์กฐ (๋ฌธ์ ์ ):
$table->foreignId('user_id')->constrained('auth_schema.auth_users')->onDelete('cascade');์์ ๋ ๊ตฌ์กฐ (์ค๋ฉ ์ง์):
Schema::create('webauthn_credentials', function (Blueprint $table) {
$table->id();
// UUID ๊ธฐ๋ฐ ๊ด๊ณ (์ค๋ฉ ์ง์)
$table->uuid('user_uuid')->index()->comment('์ฌ์ฉ์ UUID (์ค๋ฉ ์ง์)');
// ๊ธฐ์กด user_id ์ ์ง (ํธํ์ฑ, nullable)
$table->unsignedBigInteger('user_id')->nullable()->comment('์ฌ์ฉ์ ID (ํธํ์ฑ์ฉ)');
// Passkey ์๊ฒฉ ์ฆ๋ช
ํ๋
$table->string('credential_id')->unique()->comment('WebAuthn Credential ID');
$table->text('public_key')->comment('Public Key (Base64 ์ธ์ฝ๋ฉ)');
$table->bigInteger('sign_count')->default(0)->comment('์๋ช
ํ์ (Replay ๊ณต๊ฒฉ ๋ฐฉ์ง)');
$table->string('device_name')->nullable()->comment('๋๋ฐ์ด์ค ์ด๋ฆ');
$table->string('device_type')->nullable()->comment('๋๋ฐ์ด์ค ํ์
(mobile, desktop, security_key)');
$table->timestamp('last_used_at')->nullable()->comment('๋ง์ง๋ง ์ฌ์ฉ ์ผ์');
$table->timestamps();
// ์ธ๋ฑ์ค
$table->index('user_uuid'); // UUID ๊ธฐ๋ฐ ์กฐํ ์ต์ ํ
$table->index('credential_id');
$table->index('last_used_at');
// ๋ณตํฉ ์ธ๋ฑ์ค (์ฌ์ฉ์๋ณ ์๊ฒฉ ์ฆ๋ช
์กฐํ)
$table->index(['user_uuid', 'credential_id']);
});๋ณ๊ฒฝ ์ฌํญ:
user_uuid์ปฌ๋ผ ์ถ๊ฐ (ํ์, ์ธ๋ฑ์ค)user_id์ปฌ๋ผ์ nullable๋ก ๋ณ๊ฒฝ (ํธํ์ฑ ์ ์ง)- ์ธ๋ํค ์ ์ฝ ์ ๊ฑฐ (์ค๋ฉ ํ๊ฒฝ์์๋ ์ธ๋ํค ์ฌ์ฉ ๋ถ๊ฐ)
user_uuid๊ธฐ๋ฐ ์ธ๋ฑ์ค ์ถ๊ฐ
๋ชฉ์ : ์ฌ์ฉ์ UUID๋ก ๋น ๋ฅด๊ฒ Passkey ์๊ฒฉ ์ฆ๋ช ์กฐํ
Schema::create('user_passkey_index', function (Blueprint $table) {
$table->id();
$table->uuid('user_uuid')->index()->comment('์ฌ์ฉ์ UUID');
$table->string('credential_id')->unique()->comment('Credential ID');
$table->integer('shard_id')->nullable()->comment('์ค๋ ๋ฒํธ (์บ์)');
$table->boolean('is_active')->default(true)->comment('ํ์ฑํ ์ฌ๋ถ');
$table->timestamps();
// ์ธ๋ฑ์ค
$table->index('user_uuid');
$table->index(['user_uuid', 'is_active']); // ํ์ฑ ์๊ฒฉ ์ฆ๋ช
๋ง ์กฐํ
});์ฅ์ :
- ๋น ๋ฅธ ์กฐํ ์ฑ๋ฅ
- ์ค๋ ์ ๋ณด ์บ์ฑ
๋จ์ :
- ๋ฐ์ดํฐ ์ค๋ณต
- ๋๊ธฐํ ํ์
๊ฒฐ๋ก : ์ธ๋ฑ์ค ํ
์ด๋ธ์ ์ ํ์ฌํญ. ์ด๊ธฐ ๊ตฌํ์์๋ webauthn_credentials ํ
์ด๋ธ๋ง ์ฌ์ฉํ๊ณ , ์ฑ๋ฅ ๋ฌธ์ ๋ฐ์ ์ ์ถ๊ฐ ๊ณ ๋ ค.
์ญํ : Passkey ์๊ฒฉ ์ฆ๋ช ๊ด๋ฆฌ ๋ฐ ์ธ์ฆ ์ฒ๋ฆฌ
namespace Jiny\Auth\Services;
use Jiny\Auth\Facades\Shard;
use Illuminate\Support\Facades\DB;
class PasskeyService
{
/**
* ์ฌ์ฉ์ UUID๋ก Passkey ์๊ฒฉ ์ฆ๋ช
์กฐํ
*
* ์ค๋ฉ ํ๊ฒฝ์ ๊ณ ๋ คํ์ฌ UUID ๊ธฐ๋ฐ์ผ๋ก ์กฐํํฉ๋๋ค.
*
* @param string $userUuid ์ฌ์ฉ์ UUID
* @param bool $activeOnly ํ์ฑ ์๊ฒฉ ์ฆ๋ช
๋ง ์กฐํํ ์ง ์ฌ๋ถ
* @return \Illuminate\Support\Collection
*/
public function getCredentialsByUserUuid(string $userUuid, bool $activeOnly = true)
{
$query = DB::table('webauthn_credentials')
->where('user_uuid', $userUuid);
// ํ์ฑ ์๊ฒฉ ์ฆ๋ช
๋ง ์กฐํ (ํ์์)
// if ($activeOnly) {
// $query->where('is_active', true);
// }
return $query->get();
}
/**
* Credential ID๋ก ์๊ฒฉ ์ฆ๋ช
์กฐํ
*
* @param string $credentialId Credential ID
* @return object|null
*/
public function getCredentialById(string $credentialId)
{
return DB::table('webauthn_credentials')
->where('credential_id', $credentialId)
->first();
}
/**
* Passkey ์๊ฒฉ ์ฆ๋ช
๋ฑ๋ก
*
* @param string $userUuid ์ฌ์ฉ์ UUID
* @param array $credentialData ์๊ฒฉ ์ฆ๋ช
๋ฐ์ดํฐ
* @return string Credential ID
*/
public function registerCredential(string $userUuid, array $credentialData)
{
// ์ฌ์ฉ์ ์กด์ฌ ํ์ธ (์ค๋ฉ ํ๊ฒฝ)
$user = Shard::getUserByUuid($userUuid);
if (!$user) {
throw new \Exception('์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.');
}
// ์๊ฒฉ ์ฆ๋ช
๋ฐ์ดํฐ ์ค๋น
$data = [
'user_uuid' => $userUuid,
'user_id' => $user->id ?? null, // ํธํ์ฑ (nullable)
'credential_id' => $credentialData['credential_id'],
'public_key' => $credentialData['public_key'],
'sign_count' => $credentialData['sign_count'] ?? 0,
'device_name' => $credentialData['device_name'] ?? null,
'device_type' => $credentialData['device_type'] ?? null,
'last_used_at' => null,
'created_at' => now(),
'updated_at' => now(),
];
// ์๊ฒฉ ์ฆ๋ช
์ ์ฅ
DB::table('webauthn_credentials')->insert($data);
return $credentialData['credential_id'];
}
/**
* Passkey ์๊ฒฉ ์ฆ๋ช
์
๋ฐ์ดํธ (sign_count, last_used_at)
*
* @param string $credentialId Credential ID
* @param int $signCount ์๋ช
ํ์
* @return bool
*/
public function updateCredentialUsage(string $credentialId, int $signCount)
{
return DB::table('webauthn_credentials')
->where('credential_id', $credentialId)
->update([
'sign_count' => $signCount,
'last_used_at' => now(),
'updated_at' => now(),
]) > 0;
}
/**
* Passkey ์๊ฒฉ ์ฆ๋ช
์ญ์
*
* @param string $userUuid ์ฌ์ฉ์ UUID
* @param string $credentialId Credential ID
* @return bool
*/
public function deleteCredential(string $userUuid, string $credentialId)
{
return DB::table('webauthn_credentials')
->where('user_uuid', $userUuid)
->where('credential_id', $credentialId)
->delete() > 0;
}
/**
* ์ฌ์ฉ์์ ๋ชจ๋ Passkey ์๊ฒฉ ์ฆ๋ช
์ญ์
*
* @param string $userUuid ์ฌ์ฉ์ UUID
* @return int ์ญ์ ๋ ์๊ฒฉ ์ฆ๋ช
์
*/
public function deleteAllUserCredentials(string $userUuid)
{
return DB::table('webauthn_credentials')
->where('user_uuid', $userUuid)
->delete();
}
}๊ธฐ์กด ํ๋ฆ (Login/SubmitController):
1. ์ด๋ฉ์ผ/๋น๋ฐ๋ฒํธ ์
๋ ฅ
2. Shard::getUserByEmail() โ ์ฌ์ฉ์ ์กฐํ
3. Hash::check() โ ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ
4. JWT ํ ํฐ ์์ฑ ๋๋ ์ธ์
์์ฑ
ํตํฉ๋ ํ๋ฆ (Passkey ์ถ๊ฐ):
1. ์ธ์ฆ ๋ฐฉ์ ์ ํ (์ด๋ฉ์ผ/๋น๋ฐ๋ฒํธ ๋๋ Passkey)
[์ด๋ฉ์ผ/๋น๋ฐ๋ฒํธ ๊ฒฝ๋ก]
2-1. ์ด๋ฉ์ผ/๋น๋ฐ๋ฒํธ ์
๋ ฅ
2-2. Shard::getUserByEmail() โ ์ฌ์ฉ์ ์กฐํ
2-3. Hash::check() โ ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ
2-4. JWT ํ ํฐ ์์ฑ
[Passkey ๊ฒฝ๋ก]
2-1. ์ด๋ฉ์ผ ์
๋ ฅ (๋๋ ์ฌ์ฉ์๋ช
)
2-2. Shard::getUserByEmail() โ ์ฌ์ฉ์ ์กฐํ
2-3. PasskeyService::getCredentialsByUserUuid() โ ์๊ฒฉ ์ฆ๋ช
์กฐํ
2-4. WebAuthn ์ธ์ฆ ์์ฒญ ์์ฑ (challenge)
2-5. ํด๋ผ์ด์ธํธ์์ Passkey ์ธ์ฆ ์ํ
2-6. ์๋ฒ์์ ์ธ์ฆ ์๋ต ๊ฒ์ฆ
2-7. PasskeyService::updateCredentialUsage() โ ์ฌ์ฉ ๊ธฐ๋ก ์
๋ฐ์ดํธ
2-8. JWT ํ ํฐ ์์ฑ
๋ฐฉ์ 1: ๊ธฐ์กด ์ปจํธ๋กค๋ฌ ํ์ฅ (๊ถ์ฅ)
Login/SubmitController์ Passkey ์ธ์ฆ ๋ก์ง ์ถ๊ฐauth_methodํ๋ผ๋ฏธํฐ๋ก ์ธ์ฆ ๋ฐฉ์ ๊ตฌ๋ถ
๋ฐฉ์ 2: ๋ณ๋ ์ปจํธ๋กค๋ฌ ์์ฑ
Login/PasskeyController์์ฑ- ๊ธฐ์กด ์ปจํธ๋กค๋ฌ์ ๋ถ๋ฆฌํ์ฌ ๊ด๋ฆฌ
๊ถ์ฅ ๋ฐฉ์: ๋ฐฉ์ 1 (๊ธฐ์กด ์ปจํธ๋กค๋ฌ ํ์ฅ)
- ์ฝ๋ ์ค๋ณต ์ต์ํ
- ์ธ์ฆ ๋ก์ง ํตํฉ ๊ด๋ฆฌ ์ฉ์ด
namespace Jiny\Auth\Models;
use Illuminate\Database\Eloquent\Model;
class WebAuthnCredential extends Model
{
protected $table = 'webauthn_credentials';
protected $fillable = [
'user_uuid',
'user_id',
'credential_id',
'public_key',
'sign_count',
'device_name',
'device_type',
'last_used_at',
];
protected $casts = [
'sign_count' => 'integer',
'last_used_at' => 'datetime',
];
/**
* ์ฌ์ฉ์ UUID๋ก ์๊ฒฉ ์ฆ๋ช
์กฐํ (์ค์ฝํ)
*/
public function scopeForUser($query, string $userUuid)
{
return $query->where('user_uuid', $userUuid);
}
/**
* ํ์ฑ ์๊ฒฉ ์ฆ๋ช
๋ง ์กฐํ (์ค์ฝํ)
*/
public function scopeActive($query)
{
// ํ์์ is_active ์ปฌ๋ผ ์ถ๊ฐ ํ ์ฌ์ฉ
return $query;
}
}-
webauthn_credentialsํ ์ด๋ธ ๋ง์ด๊ทธ๋ ์ด์ ์์ฑuser_uuid์ปฌ๋ผ ์ถ๊ฐuser_id์ปฌ๋ผ์ nullable๋ก ๋ณ๊ฒฝ- ์ธ๋ํค ์ ์ฝ ์ ๊ฑฐ
- ์ธ๋ฑ์ค ์ถ๊ฐ
-
๊ธฐ์กด ๋ฐ์ดํฐ ๋ง์ด๊ทธ๋ ์ด์ (์๋ ๊ฒฝ์ฐ)
user_idโuser_uuid๋งคํ- ์ค๋ฉ๋ ์ฌ์ฉ์ ํ ์ด๋ธ์์ UUID ์กฐํ
PasskeyService์์ฑ- WebAuthn ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํตํฉ (์:
web-auth/webauthn-lib)
Login/SubmitController์์ - Passkey ๋ฑ๋ก ์ปจํธ๋กค๋ฌ ์์ฑ
- Passkey ๊ด๋ฆฌ ์ปจํธ๋กค๋ฌ ์์ฑ (๋ชฉ๋ก, ์ญ์ )
- Passkey ๋ฑ๋ก UI
- Passkey ๋ก๊ทธ์ธ UI
- Passkey ๊ด๋ฆฌ UI
๊ธฐ์กด ์์คํ ๊ณผ์ ํธํ์ฑ:
user_id์ปฌ๋ผ ์ ์ง (nullable)- ๊ธฐ์กด ์ด๋ฉ์ผ/๋น๋ฐ๋ฒํธ ์ธ์ฆ ์ ์ง
- Passkey๋ ์ ํ์ ๊ธฐ๋ฅ์ผ๋ก ์ ๊ณต
๋ง์ด๊ทธ๋ ์ด์ ์คํฌ๋ฆฝํธ:
// ๊ธฐ์กด user_id ๊ธฐ๋ฐ ๋ฐ์ดํฐ๋ฅผ user_uuid๋ก ๋ง์ด๊ทธ๋ ์ด์
public function migrateUserIdToUuid()
{
// ์ค๋ฉ๋ ์ฌ์ฉ์ ํ
์ด๋ธ์์ UUID ์กฐํ
$shardConfig = $this->loadShardConfig();
if ($shardConfig['enable'] ?? false) {
$shardCount = $shardConfig['shard_count'] ?? 2;
$tablePrefix = $shardConfig['table_prefix'] ?? 'users_';
for ($i = 1; $i <= $shardCount; $i++) {
$shardNumber = str_pad($i, 3, '0', STR_PAD_LEFT);
$shardTableName = "{$tablePrefix}{$shardNumber}";
if (Schema::hasTable($shardTableName)) {
$users = DB::table($shardTableName)
->select('id', 'uuid')
->whereNotNull('uuid')
->get();
foreach ($users as $user) {
DB::table('webauthn_credentials')
->where('user_id', $user->id)
->whereNull('user_uuid')
->update(['user_uuid' => $user->uuid]);
}
}
}
}
}-
webauthn_credentialsํ ์ด๋ธ ๋ง์ด๊ทธ๋ ์ด์ (user_uuid ์ถ๊ฐ) -
PasskeyService๊ธฐ๋ณธ ๊ตฌํ - Passkey ๋ฑ๋ก API
- Passkey ๋ก๊ทธ์ธ API
-
Login/SubmitControllerPasskey ์ง์ ์ถ๊ฐ - Passkey ์๊ฒฉ ์ฆ๋ช ๊ด๋ฆฌ API (๋ชฉ๋ก, ์ญ์ )
- ์ฌ์ฉ์ ํ๋กํ์ Passkey ์ค์ UI
- Passkey ์ธ๋ฑ์ค ํ
์ด๋ธ (
user_passkey_index) - Passkey ์ฌ์ฉ ํต๊ณ
- ๋ค์ค Passkey ๊ด๋ฆฌ UI
- Passkey ๋ฐฑ์ /๋ณต๊ตฌ ๊ธฐ๋ฅ
- ์ธ๋ํค ์ ์ฝ ๋ถ๊ฐ: ์ค๋ฉ ํ๊ฒฝ์์๋ ์ธ๋ํค ์ ์ฝ์ ์ฌ์ฉํ ์ ์์
- UUID ๊ธฐ๋ฐ ๊ด๊ณ: ๋ชจ๋ ๊ด๊ณ๋ UUID ๊ธฐ๋ฐ์ผ๋ก ์ค๊ณํด์ผ ํจ
- ์ธ๋ฑ์ค ์ต์ ํ:
user_uuid๊ธฐ๋ฐ ์ธ๋ฑ์ค ํ์
- Credential ID ์ค๋ณต ๋ฐฉ์ง:
credential_id๋ ์ ์ญ์ ์ผ๋ก ๊ณ ์ ํด์ผ ํจ - Sign Count ๊ฒ์ฆ: Replay ๊ณต๊ฒฉ ๋ฐฉ์ง๋ฅผ ์ํด sign_count ๊ฒ์ฆ ํ์
- Public Key ๊ฒ์ฆ: WebAuthn ํ์ค์ ๋ฐ๋ฅธ ๊ฒ์ฆ ๋ก์ง ๊ตฌํ
- ์ธ๋ฑ์ค ์ต์ ํ:
user_uuid,credential_id์ธ๋ฑ์ค ํ์ - ์บ์ฑ: ์์ฃผ ์กฐํ๋๋ ์๊ฒฉ ์ฆ๋ช ์ ์บ์ฑ ๊ณ ๋ ค
- ๋ฐฐ์น ์ฒ๋ฆฌ: ๋๋์ ์๊ฒฉ ์ฆ๋ช ์กฐํ ์ ๋ฐฐ์น ์ฒ๋ฆฌ ๊ณ ๋ ค