์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ” PHP ์†Œ์…œ ๋กœ๊ทธ์ธ๊ณผ OAuth ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐Ÿ” PHP ์†Œ์…œ ๋กœ๊ทธ์ธ๊ณผ OAuth ์™„๋ฒฝ ๊ฐ€์ด๋“œ

์นœ๊ตฌ์ฒ˜๋Ÿผ ์‰ฝ๊ฒŒ ๋ฐฐ์šฐ๋Š” ์†Œ์…œ ๋ฏธ๋””์–ด ์—ฐ๋™ ๋งˆ์Šคํ„ฐ ํด๋ž˜์Šค ๐Ÿš€

๐ŸŽฏ ์™œ ์†Œ์…œ ๋กœ๊ทธ์ธ์ด ํ•„์ˆ˜๊ฐ€ ๋˜์—ˆ์„๊นŒ?

์•ผ, ์†”์งํžˆ ๋งํ•ด์„œ ์š”์ฆ˜ ์›น์‚ฌ์ดํŠธ ๊ฐ€์ž…ํ•  ๋•Œ ๋˜ ์•„์ด๋”” ๋งŒ๋“ค๊ณ  ๋น„๋ฐ€๋ฒˆํ˜ธ ์„ค์ •ํ•˜๊ณ ... ์ง„์งœ ๊ท€์ฐฎ์ง€ ์•Š์•„? ๐Ÿ˜ฉ

๋‚˜๋งŒ ๊ทธ๋Ÿฐ ๊ฒŒ ์•„๋‹ˆ๋ผ ํ†ต๊ณ„๋ฅผ ๋ณด๋ฉด ์‚ฌ์šฉ์ž์˜ 86%๊ฐ€ ํšŒ์›๊ฐ€์ž… ๊ณผ์ •์—์„œ ์ดํƒˆํ•œ๋‹ค๊ณ  ํ•ด. ๊ทผ๋ฐ ์†Œ์…œ ๋กœ๊ทธ์ธ์„ ๋„์ž…ํ•˜๋ฉด? ์ดํƒˆ๋ฅ ์ด ๋ฌด๋ ค 30%๊นŒ์ง€ ๋–จ์–ด์ง„๋‹ค๋Š” ๊ฑฐ์•ผ!

๊ทธ๋ž˜์„œ ์š”์ฆ˜ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ๋“ค๋„ ์นด์นด์˜ค, ๊ตฌ๊ธ€, ๋„ค์ด๋ฒ„ ๋กœ๊ทธ์ธ์„ ๋‹ค ์ง€์›ํ•˜์ž–์•„? ์ด๊ฒŒ ๋ฐ”๋กœ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜(UX)์„ ์ƒ๊ฐํ•œ ๋˜‘๋˜‘ํ•œ ์„ ํƒ์ด์•ผ. ๐ŸŽจ

๐Ÿ’ก ์†Œ์…œ ๋กœ๊ทธ์ธ์˜ ํ•ต์‹ฌ ์žฅ์  ์‚ฌ์šฉ์ž ์ž…์žฅ: ํด๋ฆญ 2~3๋ฒˆ์ด๋ฉด ๊ฐ€์ž… ์™„๋ฃŒ! ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ธฐ์–ตํ•  ํ•„์š” ์—†์Œ
๊ฐœ๋ฐœ์ž ์ž…์žฅ: ํšŒ์› ์ •๋ณด ๊ด€๋ฆฌ ๋ถ€๋‹ด ๊ฐ์†Œ, ๋ณด์•ˆ ์ฑ…์ž„ ๋ถ„์‚ฐ
๋น„์ฆˆ๋‹ˆ์Šค ์ž…์žฅ: ๊ฐ€์ž…๋ฅ  ์ฆ๊ฐ€, ์‹ ๋ขฐ๋„ ํ–ฅ์ƒ, ๋งˆ์ผ€ํŒ… ๋ฐ์ดํ„ฐ ํ™•๋ณด

๐Ÿ” OAuth 2.0, ๋„๋Œ€์ฒด ๋ญ๊ธธ๋ž˜?

OAuth(Open Authorization)๋Š” ์‰ฝ๊ฒŒ ๋งํ•ด์„œ "๋‚ด ์ •๋ณด ์ข€ ์จ๋„ ๋ผ?"๋ผ๊ณ  ๋ฌผ์–ด๋ณด๋Š” ํ‘œ์ค€ํ™”๋œ ๋ฐฉ๋ฒ•์ด์•ผ. ๐Ÿค

์˜ˆ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ. ๋„ˆ๊ฐ€ ์–ด๋–ค ์•ฑ์—์„œ "๊ตฌ๊ธ€๋กœ ๋กœ๊ทธ์ธ"์„ ๋ˆ„๋ฅด๋ฉด:

1๋‹จ๊ณ„: ๊ตฌ๊ธ€์ด "์ด ์•ฑ์ด ๋„ค ์ด๋ฆ„์ด๋ž‘ ์ด๋ฉ”์ผ ๋ณด๊ณ  ์‹ถ๋Œ€. ๊ดœ์ฐฎ์•„?" ๋ฌผ์–ด๋ด„
2๋‹จ๊ณ„: ๋„ˆ๊ฐ€ "์‘ ๊ดœ์ฐฎ์•„" ํ•˜๋ฉด ๊ตฌ๊ธ€์ด ์•ฑํ•œํ…Œ ํŠน๋ณ„ํ•œ ์—ด์‡ (ํ† ํฐ)๋ฅผ ์คŒ
3๋‹จ๊ณ„: ์•ฑ์€ ๊ทธ ์—ด์‡ ๋กœ ๋„ค ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ด (๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์ ˆ๋Œ€ ์•ˆ ๋„˜์–ด๊ฐ!)

์ด๊ฒŒ ๋ฐ”๋กœ OAuth์˜ ํ•ต์‹ฌ์ด์•ผ. ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๊ณต์œ ํ•˜์ง€ ์•Š๊ณ ๋„ ๊ถŒํ•œ์„ ์œ„์ž„ํ•˜๋Š” ๊ฑฐ์ง€! ๐Ÿ”‘

OAuth 2.0 ์ธ์ฆ ํ๋ฆ„ ์‚ฌ์šฉ์ž (User) ์šฐ๋ฆฌ ์•ฑ (Client) ์†Œ์…œ ์„œ๋น„์Šค (Provider) โ‘  ๋กœ๊ทธ์ธ ์š”์ฒญ โ‘ก ์ธ์ฆ ์š”์ฒญ โ‘ข ํ† ํฐ ๋ฐœ๊ธ‰ โ‘ฃ ๋กœ๊ทธ์ธ ์™„๋ฃŒ ํ•ต์‹ฌ ํฌ์ธํŠธ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์ ˆ๋Œ€ ๊ณต์œ ๋˜์ง€ ์•Š์Œ ํ† ํฐ์œผ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ๊ถŒํ•œ ์œ„์ž„ ์‚ฌ์šฉ์ž๊ฐ€ ๊ถŒํ•œ ๋ฒ”์œ„ ์ง์ ‘ ํ™•์ธ
๐ŸŽ“ ๊ฐœ๋ฐœ์ž ๊ฟ€ํŒ OAuth 2.0์—๋Š” ์—ฌ๋Ÿฌ ์ธ์ฆ ๋ฐฉ์‹(Grant Type)์ด ์žˆ๋Š”๋ฐ, ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋Š” ์ฃผ๋กœ Authorization Code Grant๋ฅผ ์‚ฌ์šฉํ•ด. ์ด๊ฒŒ ๊ฐ€์žฅ ์•ˆ์ „ํ•˜๊ฑฐ๋“ ! ๋ชจ๋ฐ”์ผ ์•ฑ์€ PKCE(Proof Key for Code Exchange)๋ฅผ ์ถ”๊ฐ€๋กœ ์‚ฌ์šฉํ•˜๋ฉด ๋” ์ข‹๊ณ .

๐Ÿ› ๏ธ PHP๋กœ ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ตฌํ˜„ํ•˜๊ธฐ - ์ค€๋น„ ๋‹จ๊ณ„

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์ฝ”๋”ฉ ๋“ค์–ด๊ฐ€ ๋ณผ๊นŒ? ๊ฑฑ์ • ๋งˆ, ํ•˜๋‚˜์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์•Œ๋ ค์ค„๊ฒŒ! ๐Ÿ˜Š

๋จผ์ € ํ•„์š”ํ•œ ๊ฒƒ๋“ค๋ถ€ํ„ฐ ์ฑ™๊ฒจ๋ณด์ž:

1 ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ธํŒ…

PHP 7.4 ์ด์ƒ (8.0 ์ด์ƒ ์ถ”์ฒœ)
Composer (ํŒจํ‚ค์ง€ ๊ด€๋ฆฌ์ž)
SSL ์ธ์ฆ์„œ (HTTPS ํ•„์ˆ˜! ์†Œ์…œ ๋กœ๊ทธ์ธ์€ HTTP์—์„œ ์•ˆ ๋ผ)
MySQL ๋˜๋Š” MariaDB
2 ์†Œ์…œ ์„œ๋น„์Šค ์•ฑ ๋“ฑ๋ก

๊ฐ ์†Œ์…œ ํ”Œ๋žซํผ์˜ ๊ฐœ๋ฐœ์ž ์ฝ˜์†”์—์„œ ์•ฑ์„ ๋“ฑ๋กํ•ด์•ผ ํ•ด:
โ€ข ๊ตฌ๊ธ€: Google Cloud Console
โ€ข ํŽ˜์ด์Šค๋ถ: Facebook Developers
โ€ข ์นด์นด์˜ค: Kakao Developers
โ€ข ๋„ค์ด๋ฒ„: Naver Developers

์—ฌ๊ธฐ์„œ Client ID์™€ Client Secret์„ ๋ฐ›๊ฒŒ ๋  ๊ฑฐ์•ผ. ์ด๊ฑด ์ ˆ๋Œ€ ๊ณต๊ฐœํ•˜๋ฉด ์•ˆ ๋ผ! ๐Ÿ”’
3 Redirect URI ์„ค์ •

์†Œ์…œ ๋กœ๊ทธ์ธ ํ›„ ๋Œ์•„์˜ฌ ์ฃผ์†Œ๋ฅผ ์ •ํ™•ํžˆ ๋“ฑ๋กํ•ด์•ผ ํ•ด.
์˜ˆ: https://yourdomain.com/callback/google
ํ•œ ๊ธ€์ž๋ผ๋„ ํ‹€๋ฆฌ๋ฉด ์ž‘๋™ ์•ˆ ํ•˜๋‹ˆ๊นŒ ์ฃผ์˜! โš ๏ธ

์ด์ œ Composer๋กœ ํ•„์š”ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•ด๋ณด์ž. ํ„ฐ๋ฏธ๋„์„ ์—ด๊ณ :

composer require league/oauth2-client
composer require league/oauth2-google
composer require league/oauth2-facebook

์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์€ OAuth 2.0 ๊ตฌํ˜„์„ ์—„์ฒญ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ค˜. ๋ฐ”ํ€ด๋ฅผ ๋‹ค์‹œ ๋ฐœ๋ช…ํ•  ํ•„์š” ์—†์ž–์•„? ๐ŸŽก

๐Ÿ’ป ๊ตฌ๊ธ€ ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ตฌํ˜„ - ์‹ค์ „ ์ฝ”๋“œ

๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ๋ถ€ํ„ฐ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ. ์ฝ”๋“œ ๋ณด๋ฉด์„œ ํ•˜๋‚˜์”ฉ ์„ค๋ช…ํ• ๊ฒŒ! ๐Ÿ‘จโ€๐Ÿ’ป

๐Ÿ“ Step 1: ์„ค์ • ํŒŒ์ผ ๋งŒ๋“ค๊ธฐ

๋จผ์ € ๋ฏผ๊ฐํ•œ ์ •๋ณด๋“ค์„ ๋”ฐ๋กœ ๊ด€๋ฆฌํ•˜์ž. config.php ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด:

<?php
// config.php
return [
    'google' => [
        'client_id' => 'YOUR_GOOGLE_CLIENT_ID',
        'client_secret' => 'YOUR_GOOGLE_CLIENT_SECRET',
        'redirect_uri' => 'https://yourdomain.com/callback/google',
        'scopes' => ['email', 'profile']
    ],
    'facebook' => [
        'client_id' => 'YOUR_FACEBOOK_APP_ID',
        'client_secret' => 'YOUR_FACEBOOK_APP_SECRET',
        'redirect_uri' => 'https://yourdomain.com/callback/facebook',
        'scopes' => ['email', 'public_profile']
    ]
];
โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ ์ด ํŒŒ์ผ์€ ์ ˆ๋Œ€๋กœ Git์— ์ปค๋ฐ‹ํ•˜๋ฉด ์•ˆ ๋ผ! .gitignore์— ๊ผญ ์ถ”๊ฐ€ํ•˜๊ณ , ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒŒ ๋” ์ข‹์•„. ์‹ค์ œ ์„œ๋น„์Šค์—์„œ๋Š” .env ํŒŒ์ผ์ด๋‚˜ ์„œ๋ฒ„ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์ž.

๐Ÿš€ Step 2: ๋กœ๊ทธ์ธ ์‹œ์ž‘ ํŽ˜์ด์ง€

์‚ฌ์šฉ์ž๊ฐ€ "๊ตฌ๊ธ€๋กœ ๋กœ๊ทธ์ธ" ๋ฒ„ํŠผ์„ ํด๋ฆญํ–ˆ์„ ๋•Œ ์‹คํ–‰๋  ์ฝ”๋“œ์•ผ:

<?php
// login.php
session_start();
require_once 'vendor/autoload.php';

$config = require 'config.php';

use League\OAuth2\Client\Provider\Google;

// Google OAuth ํ”„๋กœ๋ฐ”์ด๋” ์ƒ์„ฑ
$provider = new Google([
    'clientId'     => $config['google']['client_id'],
    'clientSecret' => $config['google']['client_secret'],
    'redirectUri'  => $config['google']['redirect_uri'],
]);

// ์ธ์ฆ URL ์ƒ์„ฑ
$authUrl = $provider->getAuthorizationUrl([
    'scope' => $config['google']['scopes']
]);

// CSRF ๊ณต๊ฒฉ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ state ์ €์žฅ
$_SESSION['oauth2state'] = $provider->getState();

// ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
header('Location: ' . $authUrl);
exit;

์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ๊ฒŒ state์•ผ. ์ด๊ฑด CSRF(Cross-Site Request Forgery) ๊ณต๊ฒฉ์„ ๋ง‰๊ธฐ ์œ„ํ•œ ๋ณด์•ˆ ํ† ํฐ์ด์•ผ. ๋‚˜์ค‘์— ์ฝœ๋ฐฑ์—์„œ ๊ฒ€์ฆํ•  ๊ฑฐ์•ผ! ๐Ÿ›ก๏ธ

๐Ÿ”„ Step 3: ์ฝœ๋ฐฑ ์ฒ˜๋ฆฌํ•˜๊ธฐ

๊ตฌ๊ธ€์—์„œ ์ธ์ฆ ์™„๋ฃŒ ํ›„ ๋Œ์•„์˜ค๋Š” ํŽ˜์ด์ง€์•ผ. ์—ฌ๊ธฐ๊ฐ€ ํ•ต์‹ฌ์ด์•ผ!

<?php
// callback.php
session_start();
require_once 'vendor/autoload.php';

$config = require 'config.php';

use League\OAuth2\Client\Provider\Google;

$provider = new Google([
    'clientId'     => $config['google']['client_id'],
    'clientSecret' => $config['google']['client_secret'],
    'redirectUri'  => $config['google']['redirect_uri'],
]);

// ์—๋Ÿฌ ์ฒดํฌ
if (isset($_GET['error'])) {
    die('์ธ์ฆ ์‹คํŒจ: ' . htmlspecialchars($_GET['error']));
}

// State ๊ฒ€์ฆ (CSRF ๋ฐฉ์ง€)
if (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) {
    unset($_SESSION['oauth2state']);
    die('์ž˜๋ชป๋œ state ๊ฐ’์ž…๋‹ˆ๋‹ค.');
}

try {
    // Authorization code๋กœ ์•ก์„ธ์Šค ํ† ํฐ ๋ฐ›๊ธฐ
    $token = $provider->getAccessToken('authorization_code', [
        'code' => $_GET['code']
    ]);

    // ์•ก์„ธ์Šค ํ† ํฐ์œผ๋กœ ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
    $user = $provider->getResourceOwner($token);

    // ์‚ฌ์šฉ์ž ์ •๋ณด ์ถ”์ถœ
    $userData = [
        'id' => $user->getId(),
        'email' => $user->getEmail(),
        'name' => $user->getName(),
        'avatar' => $user->getAvatar(),
        'provider' => 'google',
        'access_token' => $token->getToken(),
        'refresh_token' => $token->getRefreshToken(),
        'expires' => $token->getExpires()
    ];

    // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ ๋˜๋Š” ์—…๋ฐ์ดํŠธ
    saveOrUpdateUser($userData);

    // ์„ธ์…˜์— ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ
    $_SESSION['user'] = $userData;

    // ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
    header('Location: /dashboard');
    exit;

} catch (Exception $e) {
    die('์˜ค๋ฅ˜ ๋ฐœ์ƒ: ' . $e->getMessage());
}

function saveOrUpdateUser($userData) {
    // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ
    $pdo = new PDO('mysql:host=localhost;dbname=yourdb', 'username', 'password');
    
    // ๊ธฐ์กด ์‚ฌ์šฉ์ž ํ™•์ธ
    $stmt = $pdo->prepare("
        SELECT id FROM users 
        WHERE provider = ? AND provider_id = ?
    ");
    $stmt->execute([$userData['provider'], $userData['id']]);
    
    if ($stmt->rowCount() > 0) {
        // ์—…๋ฐ์ดํŠธ
        $stmt = $pdo->prepare("
            UPDATE users SET 
                email = ?, 
                name = ?, 
                avatar = ?,
                access_token = ?,
                refresh_token = ?,
                token_expires = ?,
                updated_at = NOW()
            WHERE provider = ? AND provider_id = ?
        ");
        $stmt->execute([
            $userData['email'],
            $userData['name'],
            $userData['avatar'],
            $userData['access_token'],
            $userData['refresh_token'],
            date('Y-m-d H:i:s', $userData['expires']),
            $userData['provider'],
            $userData['id']
        ]);
    } else {
        // ์‹ ๊ทœ ์‚ฝ์ž…
        $stmt = $pdo->prepare("
            INSERT INTO users (
                provider, provider_id, email, name, avatar,
                access_token, refresh_token, token_expires, created_at
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())
        ");
        $stmt->execute([
            $userData['provider'],
            $userData['id'],
            $userData['email'],
            $userData['name'],
            $userData['avatar'],
            $userData['access_token'],
            $userData['refresh_token'],
            date('Y-m-d H:i:s', $userData['expires'])
        ]);
    }
}
๐Ÿ’ก ํ”„๋กœ ํŒ ์•ก์„ธ์Šค ํ† ํฐ์€ ๋ณดํ†ต 1์‹œ๊ฐ„ ์ •๋„ ์œ ํšจํ•ด. ๋งŒ๋ฃŒ๋˜๋ฉด refresh token์œผ๋กœ ์ƒˆ ํ† ํฐ์„ ๋ฐ›์•„์•ผ ํ•ด. ์ด ๋ถ€๋ถ„์€ ๋‚˜์ค‘์— ์ž๋™ํ™”ํ•˜๋Š” ๊ฒŒ ์ข‹์•„!

๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„

์†Œ์…œ ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ €์žฅํ•  ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. ์ผ๋ฐ˜ ํšŒ์›๊ณผ ์†Œ์…œ ํšŒ์›์„ ํ•จ๊ป˜ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ์•ผ! ๐Ÿ“Š

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    provider VARCHAR(20) NULL COMMENT '์†Œ์…œ ์ œ๊ณต์ž (google, facebook, kakao, naver)',
    provider_id VARCHAR(255) NULL COMMENT '์†Œ์…œ ์ œ๊ณต์ž์˜ ์‚ฌ์šฉ์ž ID',
    email VARCHAR(255) NOT NULL,
    name VARCHAR(100) NOT NULL,
    avatar VARCHAR(500) NULL COMMENT 'ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ URL',
    password VARCHAR(255) NULL COMMENT '์ผ๋ฐ˜ ํšŒ์›๊ฐ€์ž…์šฉ (์†Œ์…œ์€ NULL)',
    access_token TEXT NULL,
    refresh_token TEXT NULL,
    token_expires DATETIME NULL,
    email_verified BOOLEAN DEFAULT FALSE,
    status ENUM('active', 'inactive', 'suspended') DEFAULT 'active',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    last_login TIMESTAMP NULL,
    UNIQUE KEY unique_provider (provider, provider_id),
    INDEX idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ์†Œ์…œ ๋กœ๊ทธ์ธ ๋กœ๊ทธ ํ…Œ์ด๋ธ” (์„ ํƒ์‚ฌํ•ญ, ๋ถ„์„์šฉ)
CREATE TABLE social_login_logs (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    provider VARCHAR(20) NOT NULL,
    ip_address VARCHAR(45),
    user_agent TEXT,
    login_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_user_provider (user_id, provider),
    INDEX idx_login_at (login_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

์ด ๊ตฌ์กฐ์˜ ์žฅ์ ์€ ๋ญ๋ƒ๋ฉด:

1. ์ผ๋ฐ˜ ํšŒ์›๊ฐ€์ž…๊ณผ ์†Œ์…œ ๋กœ๊ทธ์ธ์„ ํ•˜๋‚˜์˜ ํ…Œ์ด๋ธ”๋กœ ๊ด€๋ฆฌ (password๊ฐ€ NULL์ด๋ฉด ์†Œ์…œ ํšŒ์›)
2. ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์—ฌ๋Ÿฌ ์†Œ์…œ ๊ณ„์ • ์—ฐ๋™ ๊ฐ€๋Šฅ (provider + provider_id ์กฐํ•ฉ์œผ๋กœ ๊ตฌ๋ถ„)
3. ํ† ํฐ ์ •๋ณด ์ €์žฅ์œผ๋กœ API ํ˜ธ์ถœ ๊ฐ€๋Šฅ
4. ๋กœ๊ทธ ํ…Œ์ด๋ธ”๋กœ ๋ณด์•ˆ ๊ฐ์‚ฌ ๋ฐ ์‚ฌ์šฉ์ž ํ–‰๋™ ๋ถ„์„ ๊ฐ€๋Šฅ ๐Ÿ“ˆ

๐ŸŽจ ์นด์นด์˜ค ๋กœ๊ทธ์ธ ๊ตฌํ˜„ํ•˜๊ธฐ

ํ•œ๊ตญ์—์„œ๋Š” ์นด์นด์˜ค ๋กœ๊ทธ์ธ์ด ์ง„์งœ ๋งŽ์ด ์“ฐ์ด์ž–์•„? ์นด์นด์˜ค๋Š” ์กฐ๊ธˆ ํŠน๋ณ„ํ•ด์„œ ๋”ฐ๋กœ ์„ค๋ช…ํ•ด์ค„๊ฒŒ! ๐ŸŸก

์นด์นด์˜ค ๊ฐœ๋ฐœ์ž ์„ค์ •

๋จผ์ € Kakao Developers์—์„œ ์•ฑ์„ ๋งŒ๋“ค๊ณ :

1. ๋‚ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ โ†’ ์•ฑ ๋งŒ๋“ค๊ธฐ
2. ํ”Œ๋žซํผ ์„ค์ • โ†’ Web ํ”Œ๋žซํผ ๋“ฑ๋ก (์‚ฌ์ดํŠธ ๋„๋ฉ”์ธ ์ž…๋ ฅ)
3. ์นด์นด์˜ค ๋กœ๊ทธ์ธ ํ™œ์„ฑํ™”
4. Redirect URI ๋“ฑ๋ก (์˜ˆ: https://yourdomain.com/callback/kakao)
5. ๋™์˜ํ•ญ๋ชฉ ์„ค์ • (๋‹‰๋„ค์ž„, ํ”„๋กœํ•„ ์ด๋ฏธ์ง€, ์ด๋ฉ”์ผ ๋“ฑ)

โš ๏ธ ์นด์นด์˜ค ํŠน์ด์‚ฌํ•ญ ์นด์นด์˜ค๋Š” ์ด๋ฉ”์ผ์ด ํ•„์ˆ˜ ๋™์˜๊ฐ€ ์•„๋‹ˆ๋ผ ์„ ํƒ ๋™์˜์•ผ. ๊ทธ๋ž˜์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฉ”์ผ ์ œ๊ณต์„ ๊ฑฐ๋ถ€ํ•  ์ˆ˜ ์žˆ์–ด. ์ด ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•ด!

์นด์นด์˜ค ๋กœ๊ทธ์ธ ์ฝ”๋“œ

<?php
// kakao_login.php
session_start();

$config = [
    'client_id' => 'YOUR_KAKAO_REST_API_KEY',
    'redirect_uri' => 'https://yourdomain.com/callback/kakao'
];

// State ์ƒ์„ฑ (CSRF ๋ฐฉ์ง€)
$state = bin2hex(random_bytes(16));
$_SESSION['kakao_state'] = $state;

// ์นด์นด์˜ค ์ธ์ฆ URL ์ƒ์„ฑ
$params = [
    'client_id' => $config['client_id'],
    'redirect_uri' => $config['redirect_uri'],
    'response_type' => 'code',
    'state' => $state
];

$authUrl = 'https://kauth.kakao.com/oauth/authorize?' . http_build_query($params);

header('Location: ' . $authUrl);
exit;
<?php
// kakao_callback.php
session_start();

$config = [
    'client_id' => 'YOUR_KAKAO_REST_API_KEY',
    'client_secret' => 'YOUR_KAKAO_CLIENT_SECRET', // ์„ ํƒ์‚ฌํ•ญ
    'redirect_uri' => 'https://yourdomain.com/callback/kakao'
];

// State ๊ฒ€์ฆ
if ($_GET['state'] !== $_SESSION['kakao_state']) {
    die('์ž˜๋ชป๋œ ์š”์ฒญ์ž…๋‹ˆ๋‹ค.');
}

// ํ† ํฐ ์š”์ฒญ
$tokenUrl = 'https://kauth.kakao.com/oauth/token';
$tokenParams = [
    'grant_type' => 'authorization_code',
    'client_id' => $config['client_id'],
    'redirect_uri' => $config['redirect_uri'],
    'code' => $_GET['code']
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $tokenUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($tokenParams));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);

$token = json_decode($response, true);

if (isset($token['error'])) {
    die('ํ† ํฐ ๋ฐœ๊ธ‰ ์‹คํŒจ: ' . $token['error_description']);
}

// ์‚ฌ์šฉ์ž ์ •๋ณด ์š”์ฒญ
$userInfoUrl = 'https://kapi.kakao.com/v2/user/me';
$headers = ['Authorization: Bearer ' . $token['access_token']];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $userInfoUrl);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);

$userInfo = json_decode($response, true);

// ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ์ถ”์ถœ
$userData = [
    'id' => $userInfo['id'],
    'email' => $userInfo['kakao_account']['email'] ?? null, // ์ด๋ฉ”์ผ ์„ ํƒ ๋™์˜
    'name' => $userInfo['kakao_account']['profile']['nickname'],
    'avatar' => $userInfo['kakao_account']['profile']['profile_image_url'],
    'provider' => 'kakao',
    'access_token' => $token['access_token'],
    'refresh_token' => $token['refresh_token'],
    'expires' => time() + $token['expires_in']
];

// ์ด๋ฉ”์ผ์ด ์—†๋Š” ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ
if (empty($userData['email'])) {
    // ์˜ต์…˜ 1: ์ด๋ฉ”์ผ ์ž…๋ ฅ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
    $_SESSION['temp_user'] = $userData;
    header('Location: /email-required');
    exit;
    
    // ์˜ต์…˜ 2: ์นด์นด์˜ค ID๋ฅผ ์ด๋ฉ”์ผ๋กœ ์‚ฌ์šฉ
    // $userData['email'] = $userData['id'] . '@kakao.user';
}

// DB ์ €์žฅ ๋ฐ ์„ธ์…˜ ์ฒ˜๋ฆฌ
saveOrUpdateUser($userData);
$_SESSION['user'] = $userData;

header('Location: /dashboard');
exit;
๐ŸŽฏ ์นด์นด์˜ค API ํ™œ์šฉ ํŒ ์นด์นด์˜ค๋Š” ๋กœ๊ทธ์ธ ์™ธ์—๋„ ๋‹ค์–‘ํ•œ API๋ฅผ ์ œ๊ณตํ•ด:
โ€ข ์นด์นด์˜คํ†ก ๋ฉ”์‹œ์ง€ ๋ณด๋‚ด๊ธฐ (๋‚˜์—๊ฒŒ ๋ณด๋‚ด๊ธฐ, ์นœ๊ตฌ์—๊ฒŒ ๋ณด๋‚ด๊ธฐ)
โ€ข ์นด์นด์˜ค์Šคํ† ๋ฆฌ ๊ณต์œ 
โ€ข ์นด์นด์˜คํŽ˜์ด ๊ฒฐ์ œ

์•ก์„ธ์Šค ํ† ํฐ๋งŒ ์žˆ์œผ๋ฉด ์ด๋Ÿฐ ๊ธฐ๋Šฅ๋“ค์„ ์‰ฝ๊ฒŒ ์—ฐ๋™ํ•  ์ˆ˜ ์žˆ์–ด! ํŠนํžˆ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๊ฑฐ๋ž˜ ์•Œ๋ฆผ์„ ์นด์นด์˜คํ†ก์œผ๋กœ ๋ณด๋‚ด๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ํ›จ์”ฌ ์ข‹์•„์ง€๊ฒ ์ง€? ๐Ÿ’ฌ

๐Ÿ”ต ๋„ค์ด๋ฒ„ ๋กœ๊ทธ์ธ ๊ตฌํ˜„ํ•˜๊ธฐ

๋„ค์ด๋ฒ„๋„ ํ•œ๊ตญ์—์„œ ๋งŽ์ด ์“ฐ์ด๋Š” ์†Œ์…œ ๋กœ๊ทธ์ธ์ด์•ผ. ๊ตฌํ˜„ ๋ฐฉ์‹์€ ๋น„์Šทํ•œ๋ฐ ์•ฝ๊ฐ„์˜ ์ฐจ์ด๊ฐ€ ์žˆ์–ด! ๐ŸŸข

<?php
// naver_login.php
session_start();

$config = [
    'client_id' => 'YOUR_NAVER_CLIENT_ID',
    'client_secret' => 'YOUR_NAVER_CLIENT_SECRET',
    'redirect_uri' => 'https://yourdomain.com/callback/naver'
];

$state = bin2hex(random_bytes(16));
$_SESSION['naver_state'] = $state;

$params = [
    'response_type' => 'code',
    'client_id' => $config['client_id'],
    'redirect_uri' => $config['redirect_uri'],
    'state' => $state
];

$authUrl = 'https://nid.naver.com/oauth2.0/authorize?' . http_build_query($params);

header('Location: ' . $authUrl);
exit;
<?php
// naver_callback.php
session_start();

$config = [
    'client_id' => 'YOUR_NAVER_CLIENT_ID',
    'client_secret' => 'YOUR_NAVER_CLIENT_SECRET',
    'redirect_uri' => 'https://yourdomain.com/callback/naver'
];

// State ๊ฒ€์ฆ
if ($_GET['state'] !== $_SESSION['naver_state']) {
    die('์ž˜๋ชป๋œ ์š”์ฒญ์ž…๋‹ˆ๋‹ค.');
}

// ํ† ํฐ ์š”์ฒญ
$tokenUrl = 'https://nid.naver.com/oauth2.0/token';
$tokenParams = [
    'grant_type' => 'authorization_code',
    'client_id' => $config['client_id'],
    'client_secret' => $config['client_secret'],
    'code' => $_GET['code'],
    'state' => $_GET['state']
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $tokenUrl . '?' . http_build_query($tokenParams));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);

$token = json_decode($response, true);

if (isset($token['error'])) {
    die('ํ† ํฐ ๋ฐœ๊ธ‰ ์‹คํŒจ: ' . $token['error_description']);
}

// ์‚ฌ์šฉ์ž ์ •๋ณด ์š”์ฒญ
$userInfoUrl = 'https://openapi.naver.com/v1/nid/me';
$headers = ['Authorization: Bearer ' . $token['access_token']];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $userInfoUrl);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response, true);

if ($result['resultcode'] !== '00') {
    die('์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ ์‹คํŒจ');
}

$userInfo = $result['response'];

$userData = [
    'id' => $userInfo['id'],
    'email' => $userInfo['email'],
    'name' => $userInfo['name'] ?? $userInfo['nickname'],
    'avatar' => $userInfo['profile_image'],
    'provider' => 'naver',
    'access_token' => $token['access_token'],
    'refresh_token' => $token['refresh_token'],
    'expires' => time() + $token['expires_in']
];

saveOrUpdateUser($userData);
$_SESSION['user'] = $userData;

header('Location: /dashboard');
exit;

๐ŸŽญ ํŽ˜์ด์Šค๋ถ ๋กœ๊ทธ์ธ ๊ตฌํ˜„ํ•˜๊ธฐ

ํŽ˜์ด์Šค๋ถ์€ ์ „ ์„ธ๊ณ„์ ์œผ๋กœ ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ์ด๋Š” ์†Œ์…œ ๋กœ๊ทธ์ธ์ด์•ผ. ํ•˜์ง€๋งŒ ์ตœ๊ทผ ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ ์ •์ฑ…์ด ๊ฐ•ํ™”๋˜๋ฉด์„œ ์กฐ๊ธˆ ๊นŒ๋‹ค๋กœ์›Œ์กŒ์–ด. ๐Ÿ˜…

<?php
// facebook_login.php
session_start();
require_once 'vendor/autoload.php';

use League\OAuth2\Client\Provider\Facebook;

$config = [
    'client_id' => 'YOUR_FACEBOOK_APP_ID',
    'client_secret' => 'YOUR_FACEBOOK_APP_SECRET',
    'redirect_uri' => 'https://yourdomain.com/callback/facebook',
    'graph_api_version' => 'v18.0' // ์ตœ์‹  ๋ฒ„์ „ ์‚ฌ์šฉ
];

$provider = new Facebook([
    'clientId' => $config['client_id'],
    'clientSecret' => $config['client_secret'],
    'redirectUri' => $config['redirect_uri'],
    'graphApiVersion' => $config['graph_api_version']
]);

$authUrl = $provider->getAuthorizationUrl([
    'scope' => ['email', 'public_profile']
]);

$_SESSION['facebook_state'] = $provider->getState();

header('Location: ' . $authUrl);
exit;
<?php
// facebook_callback.php
session_start();
require_once 'vendor/autoload.php';

use League\OAuth2\Client\Provider\Facebook;

$config = [
    'client_id' => 'YOUR_FACEBOOK_APP_ID',
    'client_secret' => 'YOUR_FACEBOOK_APP_SECRET',
    'redirect_uri' => 'https://yourdomain.com/callback/facebook',
    'graph_api_version' => 'v18.0'
];

$provider = new Facebook([
    'clientId' => $config['client_id'],
    'clientSecret' => $config['client_secret'],
    'redirectUri' => $config['redirect_uri'],
    'graphApiVersion' => $config['graph_api_version']
]);

// State ๊ฒ€์ฆ
if (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['facebook_state'])) {
    unset($_SESSION['facebook_state']);
    die('์ž˜๋ชป๋œ state ๊ฐ’์ž…๋‹ˆ๋‹ค.');
}

try {
    $token = $provider->getAccessToken('authorization_code', [
        'code' => $_GET['code']
    ]);

    // ์žฅ๊ธฐ ํ† ํฐ์œผ๋กœ ๊ตํ™˜ (์„ ํƒ์‚ฌํ•ญ, 60์ผ ์œ ํšจ)
    $longLivedToken = $provider->getLongLivedAccessToken($token);

    $user = $provider->getResourceOwner($longLivedToken);

    $userData = [
        'id' => $user->getId(),
        'email' => $user->getEmail(),
        'name' => $user->getName(),
        'avatar' => $user->getPictureUrl(),
        'provider' => 'facebook',
        'access_token' => $longLivedToken->getToken(),
        'expires' => $longLivedToken->getExpires()
    ];

    saveOrUpdateUser($userData);
    $_SESSION['user'] = $userData;

    header('Location: /dashboard');
    exit;

} catch (Exception $e) {
    die('์˜ค๋ฅ˜ ๋ฐœ์ƒ: ' . $e->getMessage());
}
โš ๏ธ ํŽ˜์ด์Šค๋ถ ์ฃผ์˜์‚ฌํ•ญ 1. ์•ฑ ๊ฒ€์ˆ˜: ์ด๋ฉ”์ผ ๊ถŒํ•œ์„ ๋ฐ›์œผ๋ ค๋ฉด ํŽ˜์ด์Šค๋ถ ์•ฑ ๊ฒ€์ˆ˜๋ฅผ ํ†ต๊ณผํ•ด์•ผ ํ•ด (๊ฐœ๋ฐœ ๋ชจ๋“œ์—์„œ๋Š” ํ…Œ์Šคํ„ฐ๋งŒ ๊ฐ€๋Šฅ)
2. ํ”„๋กœํ•„ ์‚ฌ์ง„: ๊ธฐ๋ณธ ์ œ๊ณต๋˜๋Š” ์‚ฌ์ง„ URL์€ ์ž„์‹œ URL์ด๋ผ ๋งŒ๋ฃŒ๋  ์ˆ˜ ์žˆ์–ด. ๋‹ค์šด๋กœ๋“œํ•ด์„œ ์ž์ฒด ์„œ๋ฒ„์— ์ €์žฅํ•˜๋Š” ๊ฒŒ ์ข‹์•„
3. ๋ฐ์ดํ„ฐ ์‚ญ์ œ ์ฝœ๋ฐฑ: ํŽ˜์ด์Šค๋ถ ์ •์ฑ…์ƒ ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ ์—ฐ๊ฒฐ์„ ํ•ด์ œํ•˜๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•ด์•ผ ํ•ด. ๋ฐ์ดํ„ฐ ์‚ญ์ œ ์ฝœ๋ฐฑ URL์„ ์„ค์ •ํ•ด์•ผ ํ•จ

๐Ÿ” ๋ณด์•ˆ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

์†Œ์…œ ๋กœ๊ทธ์ธ์„ ๊ตฌํ˜„ํ•  ๋•Œ ๋ณด์•ˆ์€ ์ •๋ง ์ค‘์š”ํ•ด! ํ•ดํ‚น๋‹นํ•˜๋ฉด ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ๋‹ค ํ„ธ๋ฆฌ๋‹ˆ๊นŒ. ๐Ÿ˜ฑ
์‹ค์ „์—์„œ ๊ผญ ์ง€์ผœ์•ผ ํ•  ๋ณด์•ˆ ์›์น™๋“ค์„ ์•Œ๋ ค์ค„๊ฒŒ:

1 HTTPS ํ•„์ˆ˜ ์‚ฌ์šฉ

์†Œ์…œ ๋กœ๊ทธ์ธ์€ HTTP์—์„œ ์ž‘๋™ํ•˜์ง€ ์•Š์•„. ๋ฐ˜๋“œ์‹œ SSL ์ธ์ฆ์„œ๋ฅผ ์„ค์น˜ํ•˜๊ณ  HTTPS๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด.
Let's Encrypt๋กœ ๋ฌด๋ฃŒ ์ธ์ฆ์„œ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์œผ๋‹ˆ ํ•‘๊ณ„ ๋Œ€์ง€ ๋งˆ! ๐Ÿ”’
2 State ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ CSRF ๋ฐฉ์ง€

๋ชจ๋“  OAuth ์š”์ฒญ์— state ๊ฐ’์„ ํฌํ•จ์‹œํ‚ค๊ณ  ์ฝœ๋ฐฑ์—์„œ ๊ฒ€์ฆํ•ด์•ผ ํ•ด.
์ด๊ฑธ ์•ˆ ํ•˜๋ฉด ๊ณต๊ฒฉ์ž๊ฐ€ ๋‹ค๋ฅธ ์‚ฌ๋žŒ์˜ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ์‹œํ‚ฌ ์ˆ˜ ์žˆ์–ด!
// ์ข‹์€ ์˜ˆ
$state = bin2hex(random_bytes(32)); // ์ถฉ๋ถ„ํžˆ ๊ธด ๋žœ๋ค ๊ฐ’
$_SESSION['oauth_state'] = $state;

// ๋‚˜์œ ์˜ˆ
$state = '12345'; // ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ๊ฐ’์€ ์œ„ํ—˜!
3 Client Secret ์ ˆ๋Œ€ ๋…ธ์ถœ ๊ธˆ์ง€

Client Secret์€ ์„œ๋ฒ„์—์„œ๋งŒ ์‚ฌ์šฉํ•˜๊ณ  ์ ˆ๋Œ€ ํด๋ผ์ด์–ธํŠธ(๋ธŒ๋ผ์šฐ์ €)์— ๋…ธ์ถœํ•˜๋ฉด ์•ˆ ๋ผ.
JavaScript์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜๋Š” ๊ฑด ์ž์‚ด ํ–‰์œ„์•ผ! ๐Ÿ’€
// ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ
$clientSecret = getenv('GOOGLE_CLIENT_SECRET');

// ๋˜๋Š” .env ํŒŒ์ผ ์‚ฌ์šฉ (vlucas/phpdotenv)
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$clientSecret = $_ENV['GOOGLE_CLIENT_SECRET'];
4 ํ† ํฐ ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅ

์•ก์„ธ์Šค ํ† ํฐ๊ณผ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์€ ์•”ํ˜ธํ™”ํ•ด์„œ ์ €์žฅํ•˜๋Š” ๊ฒŒ ์ข‹์•„.
ํŠนํžˆ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์€ ์žฅ๊ธฐ๊ฐ„ ์œ ํšจํ•˜๋‹ˆ๊นŒ ๋” ์กฐ์‹ฌํ•ด์•ผ ํ•ด!
function encryptToken($token) {
    $key = getenv('ENCRYPTION_KEY'); // 32๋ฐ”์ดํŠธ ํ‚ค
    $iv = random_bytes(16);
    $encrypted = openssl_encrypt($token, 'AES-256-CBC', $key, 0, $iv);
    return base64_encode($iv . $encrypted);
}

function decryptToken($encryptedToken) {
    $key = getenv('ENCRYPTION_KEY');
    $data = base64_decode($encryptedToken);
    $iv = substr($data, 0, 16);
    $encrypted = substr($data, 16);
    return openssl_decrypt($encrypted, 'AES-256-CBC', $key, 0, $iv);
}
5 ๊ถŒํ•œ ๋ฒ”์œ„ ์ตœ์†Œํ™”

ํ•„์š”ํ•œ ๊ถŒํ•œ๋งŒ ์š”์ฒญํ•ด. ์‚ฌ์šฉ์ž๋“ค์€ ๊ณผ๋„ํ•œ ๊ถŒํ•œ ์š”์ฒญ์„ ์‹ซ์–ดํ•˜๊ณ  ์‹ ๋ขฐ๋„๊ฐ€ ๋–จ์–ด์ ธ.
์ด๋ฉ”์ผ๊ณผ ํ”„๋กœํ•„๋งŒ ํ•„์š”ํ•˜๋ฉด ๊ทธ๊ฒƒ๋งŒ ์š”์ฒญํ•˜์ž!
6 ํ† ํฐ ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ

์•ก์„ธ์Šค ํ† ํฐ์€ ๋งŒ๋ฃŒ๋ผ. ๋งŒ๋ฃŒ๋œ ํ† ํฐ์œผ๋กœ API๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์—๋Ÿฌ๊ฐ€ ๋‚˜๋‹ˆ๊นŒ
๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์œผ๋กœ ์ž๋™ ๊ฐฑ์‹ ํ•˜๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ•ด์•ผ ํ•ด.
function refreshAccessToken($provider, $refreshToken) {
    try {
        $newToken = $provider->getAccessToken('refresh_token', [
            'refresh_token' => $refreshToken
        ]);
        
        // DB์— ์ƒˆ ํ† ํฐ ์ €์žฅ
        updateUserToken($newToken->getToken(), $newToken->getExpires());
        
        return $newToken;
    } catch (Exception $e) {
        // ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ๋„ ๋งŒ๋ฃŒ๋จ - ์žฌ๋กœ๊ทธ์ธ ํ•„์š”
        logout();
        header('Location: /login?expired=1');
        exit;
    }
}
7 ์„ธ์…˜ ๋ณด์•ˆ ๊ฐ•ํ™”

์„ธ์…˜ ํ•˜์ด์žฌํ‚น์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ์„ค์ •๋“ค:
// php.ini ๋˜๋Š” ์ฝ”๋“œ์—์„œ ์„ค์ •
ini_set('session.cookie_httponly', 1); // JavaScript ์ ‘๊ทผ ์ฐจ๋‹จ
ini_set('session.cookie_secure', 1);   // HTTPS์—์„œ๋งŒ ์ „์†ก
ini_set('session.cookie_samesite', 'Lax'); // CSRF ๋ฐฉ์ง€
ini_set('session.use_strict_mode', 1); // ์„ธ์…˜ ๊ณ ์ • ๊ณต๊ฒฉ ๋ฐฉ์ง€

session_start();

// ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ์„ธ์…˜ ID ์žฌ์ƒ์„ฑ
session_regenerate_id(true);
8 Rate Limiting ๊ตฌํ˜„

๋ฌด์ฐจ๋ณ„ ๋Œ€์ž… ๊ณต๊ฒฉ์„ ๋ง‰๊ธฐ ์œ„ํ•ด ๋กœ๊ทธ์ธ ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ œํ•œํ•ด์•ผ ํ•ด.
function checkRateLimit($ip) {
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379);
    
    $key = "login_attempts:$ip";
    $attempts = $redis->get($key) ?: 0;
    
    if ($attempts >= 5) {
        $ttl = $redis->ttl($key);
        die("๋„ˆ๋ฌด ๋งŽ์€ ์‹œ๋„์ž…๋‹ˆ๋‹ค. {$ttl}์ดˆ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”.");
    }
    
    $redis->incr($key);
    $redis->expire($key, 300); // 5๋ถ„
}

๐ŸŽจ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค ๋””์ž์ธ

๊ธฐ๋Šฅ๋„ ์ค‘์š”ํ•˜์ง€๋งŒ UI๋„ ์ค‘์š”ํ•ด! ์‚ฌ์šฉ์ž๊ฐ€ ์‰ฝ๊ณ  ํŽธํ•˜๊ฒŒ ๋กœ๊ทธ์ธํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๋‹ˆ๊นŒ. ๐Ÿ˜Š
์˜ˆ์˜๊ณ  ์‹ค์šฉ์ ์ธ ์†Œ์…œ ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์„ ๋งŒ๋“ค์–ด๋ณด์ž:

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>์†Œ์…œ ๋กœ๊ทธ์ธ</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        
        body {
            font-family: 'Segoe UI', sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        .login-container {
            background: white;
            padding: 40px;
            border-radius: 20px;
            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
            max-width: 400px;
            width: 90%;
        }
        
        h1 {
            text-align: center;
            color: #333;
            margin-bottom: 30px;
            font-size: 28px;
        }
        
        .social-buttons {
            display: flex;
            flex-direction: column;
            gap: 15px;
        }
        
        .social-btn {
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 15px;
            border: none;
            border-radius: 10px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            text-decoration: none;
            color: white;
        }
        
        .social-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 5px 15px rgba(0,0,0,0.2);
        }
        
        .social-btn svg {
            width: 24px;
            height: 24px;
            margin-right: 12px;
        }
        
        .btn-google {
            background: #4285f4;
        }
        
        .btn-facebook {
            background: #1877f2;
        }
        
        .btn-kakao {
            background: #fee500;
            color: #000000;
        }
        
        .btn-naver {
            background: #03c75a;
        }
        
        .divider {
            text-align: center;
            margin: 25px 0;
            position: relative;
        }
        
        .divider::before {
            content: '';
            position: absolute;
            left: 0;
            top: 50%;
            width: 100%;
            height: 1px;
            background: #e0e0e0;
        }
        
        .divider span {
            background: white;
            padding: 0 15px;
            position: relative;
            color: #999;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <div class="login-container">
        <h1>๐Ÿ” ๋กœ๊ทธ์ธ</h1>
        
        <div class="social-buttons">
            <a href="/login/google" class="social-btn btn-google">
                <svg viewBox="0 0 24 24">
                    <path fill="currentColor" d="M12.545,10.239v3.821h5.445c-0.712,2.315-2.647,3.972-5.445,3.972c-3.332,0-6.033-2.701-6.033-6.032s2.701-6.032,6.033-6.032c1.498,0,2.866,0.549,3.921,1.453l2.814-2.814C17.503,2.988,15.139,2,12.545,2C7.021,2,2.543,6.477,2.543,12s4.478,10,10.002,10c8.396,0,10.249-7.85,9.426-11.748L12.545,10.239z"/>
                </svg>
                Google๋กœ ๊ณ„์†ํ•˜๊ธฐ
            </a>
            
            <a href="/login/facebook" class="social-btn btn-facebook">
                <svg viewBox="0 0 24 24">
                    <path fill="currentColor" d="M12 2.04C6.5 2.04 2 6.53 2 12.06C2 17.06 5.66 21.21 10.44 21.96V14.96H7.9V12.06H10.44V9.85C10.44 7.34 11.93 5.96 14.22 5.96C15.31 5.96 16.45 6.15 16.45 6.15V8.62H15.19C13.95 8.62 13.56 9.39 13.56 10.18V12.06H16.34L15.89 14.96H13.56V21.96A10 10 0 0 0 22 12.06C22 6.53 17.5 2.04 12 2.04Z"/>
                </svg>
                Facebook์œผ๋กœ ๊ณ„์†ํ•˜๊ธฐ
            </a>
            
            <a href="/login/kakao" class="social-btn btn-kakao">
                <svg viewBox="0 0 24 24">
                    <path fill="currentColor" d="M12 3C6.5 3 2 6.58 2 11C2 13.95 3.67 16.58 6.39 18.09L5.26 21.94C5.17 22.21 5.3 22.5 5.56 22.63C5.66 22.68 5.77 22.7 5.88 22.7C6.03 22.7 6.18 22.65 6.29 22.55L10.77 19.03C11.17 19.07 11.58 19.09 12 19.09C17.5 19.09 22 15.51 22 11.09C22 6.67 17.5 3.09 12 3.09V3Z"/>
                </svg>
                ์นด์นด์˜ค๋กœ ๊ณ„์†ํ•˜๊ธฐ
            </a>
            
            <a href="/login/naver" class="social-btn btn-naver">
                <svg viewBox="0 0 24 24">
                    <path fill="currentColor" d="M16.273 12.845L7.376 0H0v24h7.726V11.156L16.624 24H24V0h-7.727v12.845z"/>
                </svg>
                ๋„ค์ด๋ฒ„๋กœ ๊ณ„์†ํ•˜๊ธฐ
            </a>
        </div>
        
        <div class="divider">
            <span>๋˜๋Š”</span>
        </div>
        
        <form action="/login" method="post">
            <input type="email" name="email" placeholder="์ด๋ฉ”์ผ" style="width:100%;padding:12px;margin-bottom:10px;border:1px solid #ddd;border-radius:8px;">
            <input type="password" name="password" placeholder="๋น„๋ฐ€๋ฒˆํ˜ธ" style="width:100%;padding:12px;margin-bottom:15px;border:1px solid #ddd;border-radius:8px;">
            <button type="submit" style="width:100%;padding:15px;background:#667eea;color:white;border:none;border-radius:8px;font-size:16px;font-weight:600;cursor:pointer;">์ด๋ฉ”์ผ๋กœ ๋กœ๊ทธ์ธ</button>
        </form>
    </div>
</body>
</html>
๐Ÿ’ก UX ๋””์ž์ธ ํŒ 1. ๋ฒ„ํŠผ ์ˆœ์„œ: ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” ์†Œ์…œ ๋กœ๊ทธ์ธ์„ ์œ„์— ๋ฐฐ์น˜ (ํ•œ๊ตญ์€ ์นด์นด์˜ค, ๊ธ€๋กœ๋ฒŒ์€ ๊ตฌ๊ธ€)
2. ์ƒ‰์ƒ ์ผ๊ด€์„ฑ: ๊ฐ ํ”Œ๋žซํผ์˜ ๊ณต์‹ ๋ธŒ๋žœ๋“œ ์ƒ‰์ƒ ์‚ฌ์šฉ (์‹ ๋ขฐ๋„ ํ–ฅ์ƒ)
3. ์•„์ด์ฝ˜ ํฌ๊ธฐ: 24x24px ์ •๋„๊ฐ€ ์ ๋‹น
4. ๋ชจ๋ฐ”์ผ ์ตœ์ ํ™”: ๋ฒ„ํŠผ ๋†’์ด๋Š” ์ตœ์†Œ 44px (ํ„ฐ์น˜ํ•˜๊ธฐ ํŽธํ•œ ํฌ๊ธฐ)
5. ๋กœ๋”ฉ ์ƒํƒœ: ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ํ‘œ์‹œ

๐Ÿ”„ ํ† ํฐ ๊ฐฑ์‹  ์ž๋™ํ™”

์•ก์„ธ์Šค ํ† ํฐ์€ ๋ณดํ†ต 1์‹œ๊ฐ„ ์ •๋„๋ฉด ๋งŒ๋ฃŒ๋ผ. ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๋ ค๋ฉด ํ† ํฐ์„ ์ž๋™์œผ๋กœ ๊ฐฑ์‹ ํ•ด์•ผ ํ•ด! ๐Ÿ”„

<?php
class TokenManager {
    private $pdo;
    private $providers = [];
    
    public function __construct($pdo) {
        $this->pdo = $pdo;
        $this->initProviders();
    }
    
    private function initProviders() {
        $config = require 'config.php';
        
        // ๊ตฌ๊ธ€ ํ”„๋กœ๋ฐ”์ด๋”
        $this->providers['google'] = new \League\OAuth2\Client\Provider\Google([
            'clientId' => $config['google']['client_id'],
            'clientSecret' => $config['google']['client_secret'],
            'redirectUri' => $config['google']['redirect_uri']
        ]);
        
        // ๋‹ค๋ฅธ ํ”„๋กœ๋ฐ”์ด๋”๋“ค๋„ ์ถ”๊ฐ€...
    }
    
    public function getValidToken($userId, $provider) {
        // DB์—์„œ ํ† ํฐ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
        $stmt = $this->pdo->prepare("
            SELECT access_token, refresh_token, token_expires 
            FROM users 
            WHERE id = ? AND provider = ?
        ");
        $stmt->execute([$userId, $provider]);
        $tokenData = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$tokenData) {
            throw new Exception('ํ† ํฐ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.');
        }
        
        // ํ† ํฐ์ด ์•„์ง ์œ ํšจํ•œ์ง€ ํ™•์ธ (5๋ถ„ ์—ฌ์œ )
        $expiresAt = strtotime($tokenData['token_expires']);
        $now = time();
        
        if ($expiresAt - $now > 300) {
            // ์•„์ง ์œ ํšจํ•จ
            return $tokenData['access_token'];
        }
        
        // ๋งŒ๋ฃŒ๋จ - ๊ฐฑ์‹  ํ•„์š”
        return $this->refreshToken($userId, $provider, $tokenData['refresh_token']);
    }
    
    private function refreshToken($userId, $provider, $refreshToken) {
        try {
            $oauthProvider = $this->providers[$provider];
            
            $newToken = $oauthProvider->getAccessToken('refresh_token', [
                'refresh_token' => $refreshToken
            ]);
            
            // DB ์—…๋ฐ์ดํŠธ
            $stmt = $this->pdo->prepare("
                UPDATE users SET 
                    access_token = ?,
                    token_expires = ?,
                    updated_at = NOW()
                WHERE id = ? AND provider = ?
            ");
            
            $stmt->execute([
                $newToken->getToken(),
                date('Y-m-d H:i:s', $newToken->getExpires()),
                $userId,
                $provider
            ]);
            
            return $newToken->getToken();
            
        } catch (Exception $e) {
            // ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ๋„ ๋งŒ๋ฃŒ๋จ
            error_log("ํ† ํฐ ๊ฐฑ์‹  ์‹คํŒจ: " . $e->getMessage());
            throw new Exception('๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”.');
        }
    }
    
    // API ํ˜ธ์ถœ ํ—ฌํผ ํ•จ์ˆ˜
    public function callAPI($userId, $provider, $url, $method = 'GET', $data = null) {
        $token = $this->getValidToken($userId, $provider);
        
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Authorization: Bearer ' . $token
        ]);
        
        if ($method === 'POST') {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
            curl_setopt($ch, CURLOPT_HTTPHEADER, [
                'Authorization: Bearer ' . $token,
                'Content-Type: application/json'
            ]);
        }
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($httpCode === 401) {
            // ํ† ํฐ์ด ๋ฌดํšจํ•จ - ํ•œ ๋ฒˆ ๋” ๊ฐฑ์‹  ์‹œ๋„
            $token = $this->refreshToken($userId, $provider, $this->getRefreshToken($userId, $provider));
            return $this->callAPI($userId, $provider, $url, $method, $data);
        }
        
        return json_decode($response, true);
    }
    
    private function getRefreshToken($userId, $provider) {
        $stmt = $this->pdo->prepare("
            SELECT refresh_token FROM users WHERE id = ? AND provider = ?
        ");
        $stmt->execute([$userId, $provider]);
        return $stmt->fetchColumn();
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
$pdo = new PDO('mysql:host=localhost;dbname=yourdb', 'username', 'password');
$tokenManager = new TokenManager($pdo);

// ๊ตฌ๊ธ€ ์บ˜๋ฆฐ๋” API ํ˜ธ์ถœ ์˜ˆ์‹œ
try {
    $events = $tokenManager->callAPI(
        $_SESSION['user']['id'],
        'google',
        'https://www.googleapis.com/calendar/v3/calendars/primary/events'
    );
    
    print_r($events);
} catch (Exception $e) {
    echo '์˜ค๋ฅ˜: ' . $e->getMessage();
}

์ด๋ ‡๊ฒŒ TokenManager ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด๋‘๋ฉด API ํ˜ธ์ถœํ•  ๋•Œ๋งˆ๋‹ค ํ† ํฐ ๋งŒ๋ฃŒ๋ฅผ ๊ฑฑ์ •ํ•  ํ•„์š”๊ฐ€ ์—†์–ด! ์ž๋™์œผ๋กœ ๊ฐฑ์‹ ํ•ด์ฃผ๋‹ˆ๊นŒ. ํŽธํ•˜์ง€? ๐Ÿ˜Ž

๐ŸŽฏ ์‹ค์ „ ํ™œ์šฉ ์‚ฌ๋ก€

์ด๋ก ๋งŒ ์•Œ์•„์„œ๋Š” ๋ถ€์กฑํ•˜์ง€! ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•˜๋Š”์ง€ ๋ช‡ ๊ฐ€์ง€ ์‚ฌ๋ก€๋ฅผ ๋ณด์—ฌ์ค„๊ฒŒ. ๐Ÿ’ผ

์‚ฌ๋ก€ 1: ํ”„๋กœํ•„ ์ •๋ณด ๋™๊ธฐํ™”

์‚ฌ์šฉ์ž๊ฐ€ ์†Œ์…œ ๊ณ„์ •์—์„œ ํ”„๋กœํ•„์„ ๋ณ€๊ฒฝํ•˜๋ฉด ์šฐ๋ฆฌ ์„œ๋น„์Šค์—๋„ ์ž๋™์œผ๋กœ ๋ฐ˜์˜ํ•˜๊ธฐ:

<?php
function syncUserProfile($userId, $provider) {
    global $tokenManager, $pdo;
    
    try {
        $userInfo = null;
        
        switch ($provider) {
            case 'google':
                $userInfo = $tokenManager->callAPI(
                    $userId, 
                    'google',
                    'https://www.googleapis.com/oauth2/v2/userinfo'
                );
                break;
                
            case 'kakao':
                $userInfo = $tokenManager->callAPI(
                    $userId,
                    'kakao',
                    'https://kapi.kakao.com/v2/user/me'
                );
                $userInfo = [
                    'name' => $userInfo['kakao_account']['profile']['nickname'],
                    'picture' => $userInfo['kakao_account']['profile']['profile_image_url']
                ];
                break;
        }
        
        if ($userInfo) {
            $stmt = $pdo->prepare("
                UPDATE users SET 
                    name = ?,
                    avatar = ?,
                    updated_at = NOW()
                WHERE id = ?
            ");
            $stmt->execute([
                $userInfo['name'],
                $userInfo['picture'],
                $userId
            ]);
            
            return true;
        }
    } catch (Exception $e) {
        error_log("ํ”„๋กœํ•„ ๋™๊ธฐํ™” ์‹คํŒจ: " . $e->getMessage());
        return false;
    }
}

// ํฌ๋ก ์žก์œผ๋กœ ๋งค์ผ ์‹คํ–‰
// 0 3 * * * php /path/to/sync_profiles.php

์‚ฌ๋ก€ 2: ์†Œ์…œ ๊ณต์œ  ๊ธฐ๋Šฅ

์‚ฌ์šฉ์ž๊ฐ€ ์šฐ๋ฆฌ ์„œ๋น„์Šค์˜ ์ฝ˜ํ…์ธ ๋ฅผ ์†Œ์…œ ๋ฏธ๋””์–ด์— ๊ณต์œ ํ•˜๊ธฐ:

<?php
class SocialSharer {
    private $tokenManager;
    
    public function __construct($tokenManager) {
        $this->tokenManager = $tokenManager;
    }
    
    // ํŽ˜์ด์Šค๋ถ์— ํฌ์ŠคํŠธ ๊ณต์œ 
    public function shareToFacebook($userId, $message, $link) {
        try {
            $result = $this->tokenManager->callAPI(
                $userId,
                'facebook',
                'https://graph.facebook.com/v18.0/me/feed',
                'POST',
                [
                    'message' => $message,
                    'link' => $link
                ]
            );
            
            return [
                'success' => true,
                'post_id' => $result['id']
            ];
        } catch (Exception $e) {
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }
    
    // ์นด์นด์˜คํ†ก ๋ฉ”์‹œ์ง€ ๋ณด๋‚ด๊ธฐ
    public function sendKakaoMessage($userId, $templateId, $templateArgs) {
        try {
            $result = $this->tokenManager->callAPI(
                $userId,
                'kakao',
                'https://kapi.kakao.com/v2/api/talk/memo/default/send',
                'POST',
                [
                    'template_object' => json_encode([
                        'object_type' => 'text',
                        'text' => $templateArgs['text'],
                        'link' => [
                            'web_url' => $templateArgs['url'],
                            'mobile_web_url' => $templateArgs['url']
                        ]
                    ])
                ]
            );
            
            return ['success' => true];
        } catch (Exception $e) {
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
$sharer = new SocialSharer($tokenManager);

// ์žฌ๋Šฅ๋„ท์—์„œ ๊ฑฐ๋ž˜ ์™„๋ฃŒ ์‹œ ์นด์นด์˜คํ†ก์œผ๋กœ ์•Œ๋ฆผ
$result = $sharer->sendKakaoMessage(
    $_SESSION['user']['id'],
    null,
    [
        'text' => '๐ŸŽ‰ ๊ฑฐ๋ž˜๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ์žฌ๋Šฅ๋„ท์„ ์ด์šฉํ•ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.',
        'url' => 'https://www.jaenung.net/transactions/12345'
    ]
);

์‚ฌ๋ก€ 3: ์นœ๊ตฌ ์ดˆ๋Œ€ ๊ธฐ๋Šฅ

์†Œ์…œ ์นœ๊ตฌ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์™€์„œ ์„œ๋น„์Šค์— ์ดˆ๋Œ€ํ•˜๊ธฐ:

<?php
function getFacebookFriends($userId) {
    global $tokenManager;
    
    try {
        // ์ฃผ์˜: ํŽ˜์ด์Šค๋ถ์€ ์นœ๊ตฌ ๋ชฉ๋ก ์ ‘๊ทผ์ด ์ œํ•œ์ ์ž„
        // ์•ฑ์„ ์‚ฌ์šฉํ•˜๋Š” ์นœ๊ตฌ๋งŒ ์กฐํšŒ ๊ฐ€๋Šฅ
        $friends = $tokenManager->callAPI(
            $userId,
            'facebook',
            'https://graph.facebook.com/v18.0/me/friends'
        );
        
        return $friends['data'];
    } catch (Exception $e) {
        return [];
    }
}

function inviteFriends($userId, $friendIds, $message) {
    // ์ดˆ๋Œ€ ๋งํฌ ์ƒ์„ฑ (์ถ”์ฒœ์ธ ์ฝ”๋“œ ํฌํ•จ)
    $referralCode = generateReferralCode($userId);
    $inviteLink = "https://yourdomain.com/invite?ref=$referralCode";
    
    // ์ด๋ฉ”์ผ ๋˜๋Š” ์†Œ์…œ ๋ฉ”์‹œ์ง€๋กœ ์ดˆ๋Œ€์žฅ ๋ฐœ์†ก
    foreach ($friendIds as $friendId) {
        sendInvitation($friendId, $message, $inviteLink);
    }
}

๐Ÿ› ๋ฌธ์ œ ํ•ด๊ฒฐ ๊ฐ€์ด๋“œ

๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ๊ผญ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ์ง€. ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ •๋ฆฌํ•ด๋ดค์–ด! ๐Ÿ”ง

๋ฌธ์ œ ์›์ธ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
Redirect URI mismatch ๋“ฑ๋ก๋œ URI์™€ ์‹ค์ œ URI๊ฐ€ ๋‹ค๋ฆ„ ๊ฐœ๋ฐœ์ž ์ฝ˜์†”์—์„œ ์ •ํ™•ํ•œ URI ๋“ฑ๋ก
(http/https, ํฌํŠธ, ๊ฒฝ๋กœ ๋ชจ๋‘ ์ผ์น˜ํ•ด์•ผ ํ•จ)
Invalid state parameter CSRF ํ† ํฐ ๋ถˆ์ผ์น˜ ์„ธ์…˜์ด ์ œ๋Œ€๋กœ ์œ ์ง€๋˜๋Š”์ง€ ํ™•์ธ
์ฟ ํ‚ค ์„ค์ • ํ™•์ธ (SameSite, Secure)
Token expired ์•ก์„ธ์Šค ํ† ํฐ ๋งŒ๋ฃŒ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์œผ๋กœ ๊ฐฑ์‹ 
์ž๋™ ๊ฐฑ์‹  ๋กœ์ง ๊ตฌํ˜„
Insufficient permissions ํ•„์š”ํ•œ ๊ถŒํ•œ์ด ์—†์Œ OAuth scope์— ํ•„์š”ํ•œ ๊ถŒํ•œ ์ถ”๊ฐ€
์‚ฌ์šฉ์ž์—๊ฒŒ ์žฌ์ธ์ฆ ์š”์ฒญ
SSL certificate problem HTTPS ์ธ์ฆ์„œ ์˜ค๋ฅ˜ ์œ ํšจํ•œ SSL ์ธ์ฆ์„œ ์„ค์น˜
๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false) (์šด์˜์—์„œ๋Š” ์ ˆ๋Œ€ ๊ธˆ์ง€!)
Rate limit exceeded API ํ˜ธ์ถœ ํ•œ๋„ ์ดˆ๊ณผ ์บ์‹ฑ ๊ตฌํ˜„
API ํ˜ธ์ถœ ์ตœ์†Œํ™”
๋ฐฑ์˜คํ”„ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ ์šฉ
๐Ÿ” ๋””๋ฒ„๊น… ํŒ 1. ์—๋Ÿฌ ๋กœ๊ทธ ํ™•์ธ: error_log()๋กœ ์ƒ์„ธ ์ •๋ณด ๊ธฐ๋ก
2. API ์‘๋‹ต ํ™•์ธ: var_dump($response)๋กœ ์‹ค์ œ ์‘๋‹ต ๋‚ด์šฉ ํ™•์ธ
3. ๋„คํŠธ์›Œํฌ ํƒญ ํ™œ์šฉ: ๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์—์„œ ์š”์ฒญ/์‘๋‹ต ํ™•์ธ
4. Postman ์‚ฌ์šฉ: API ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ง์ ‘ ํ…Œ์ŠคํŠธ
5. ๊ณต์‹ ๋ฌธ์„œ ์ฐธ์กฐ: ๊ฐ ํ”Œ๋žซํผ์˜ ์ตœ์‹  API ๋ฌธ์„œ ํ™•์ธ

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™”

์‚ฌ์šฉ์ž๊ฐ€ ๋งŽ์•„์ง€๋ฉด ์„ฑ๋Šฅ์ด ์ค‘์š”ํ•ด์ ธ. ์†Œ์…œ ๋กœ๊ทธ์ธ์„ ๋” ๋น ๋ฅด๊ณ  ํšจ์œจ์ ์œผ๋กœ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•๋“ค! ๐Ÿš€

1. ํ† ํฐ ์บ์‹ฑ

๋งค๋ฒˆ DB์—์„œ ํ† ํฐ์„ ๊ฐ€์ ธ์˜ค๋Š” ๊ฑด ๋น„ํšจ์œจ์ ์ด์•ผ. Redis๋‚˜ Memcached๋กœ ์บ์‹ฑํ•˜์ž:

<?php
class CachedTokenManager extends TokenManager {
    private $redis;
    
    public function __construct($pdo, $redis) {
        parent::__construct($pdo);
        $this->redis = $redis;
    }
    
    public function getValidToken($userId, $provider) {
        $cacheKey = "token:$userId:$provider";
        
        // ์บ์‹œ์—์„œ ๋จผ์ € ํ™•์ธ
        $cachedToken = $this->redis->get($cacheKey);
        if ($cachedToken) {
            $tokenData = json_decode($cachedToken, true);
            
            // ๋งŒ๋ฃŒ 5๋ถ„ ์ „๊นŒ์ง€๋Š” ์บ์‹œ ์‚ฌ์šฉ
            if ($tokenData['expires'] - time() > 300) {
                return $tokenData['access_token'];
            }
        }
        
        // ์บ์‹œ ๋ฏธ์Šค - DB์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ
        $token = parent::getValidToken($userId, $provider);
        
        // ์บ์‹œ์— ์ €์žฅ (TTL: 50๋ถ„)
        $this->redis->setex(
            $cacheKey,
            3000,
            json_encode([
                'access_token' => $token,
                'expires' => time() + 3600
            ])
        );
        
        return $token;
    }
}

2. ์—ฐ๊ฒฐ ํ’€๋ง

๋งค๋ฒˆ ์ƒˆ๋กœ์šด HTTP ์—ฐ๊ฒฐ์„ ๋งŒ๋“œ๋Š” ๋Œ€์‹  ์—ฐ๊ฒฐ์„ ์žฌ์‚ฌ์šฉ:

<?php
// Guzzle HTTP ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ (๋” ํšจ์œจ์ )
composer require guzzlehttp/guzzle

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Handler\CurlHandler;

$handler = new CurlHandler();
$stack = HandlerStack::create($handler);

$client = new Client([
    'handler' => $stack,
    'timeout' => 10,
    'connect_timeout' => 5,
    'http_errors' => false,
    'verify' => true
]);

// API ํ˜ธ์ถœ
$response = $client->get('https://www.googleapis.com/oauth2/v2/userinfo', [
    'headers' => [
        'Authorization' => 'Bearer ' . $token
    ]
]);

$userInfo = json_decode($response->getBody(), true);

3. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ

ํ”„๋กœํ•„ ๋™๊ธฐํ™” ๊ฐ™์€ ๋ฌด๊ฑฐ์šด ์ž‘์—…์€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ:

<?php
// ํ ์‹œ์Šคํ…œ ์‚ฌ์šฉ (์˜ˆ: Beanstalkd, RabbitMQ, Redis Queue)
composer require pda/pheanstalk

use Pheanstalk\Pheanstalk;

$pheanstalk = Pheanstalk::create('127.0.0.1');

// ์ž‘์—… ํ์— ์ถ”๊ฐ€
$pheanstalk
    ->useTube('social-sync')
    ->put(json_encode([
        'user_id' => $userId,
        'provider' => $provider,
        'action' => 'sync_profile'
    ]));

// ์›Œ์ปค ํ”„๋กœ์„ธ์Šค์—์„œ ์ฒ˜๋ฆฌ
// worker.php
while (true) {
    $job = $pheanstalk
        ->watch('social-sync')
        ->reserve();
    
    $data = json_decode($job->getData(), true);
    
    try {
        syncUserProfile($data['user_id'], $data['provider']);
        $pheanstalk->delete($job);
    } catch (Exception $e) {
        $pheanstalk->bury($job);
        error_log("์ž‘์—… ์‹คํŒจ: " . $e->getMessage());
    }
}

4. CDN ํ™œ์šฉ

์†Œ์…œ ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ์•„์ด์ฝ˜ ๊ฐ™์€ ์ •์  ๋ฆฌ์†Œ์Šค๋Š” CDN์—์„œ:

<!-- ์ง์ ‘ ํ˜ธ์ŠคํŒ… ๋Œ€์‹  CDN ์‚ฌ์šฉ -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">

<!-- ๋˜๋Š” SVG ์Šคํ”„๋ผ์ดํŠธ ์‚ฌ์šฉ -->
<svg class="icon">
    <use xlink:href="/icons/social-sprite.svg#google"></use>
</svg>

๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ ์•ฑ ์—ฐ๋™

์›น๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋ชจ๋ฐ”์ผ ์•ฑ์—์„œ๋„ ์†Œ์…œ ๋กœ๊ทธ์ธ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด! ์กฐ๊ธˆ ๋‹ค๋ฅธ ์ ‘๊ทผ์ด ํ•„์š”ํ•ด. ๐Ÿ“ฒ

๋„ค์ดํ‹ฐ๋ธŒ SDK vs ์›น๋ทฐ

๋„ค์ดํ‹ฐ๋ธŒ SDK ๋ฐฉ์‹ (๊ถŒ์žฅ) ์žฅ์ :
โ€ข ๋” ๋‚˜์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ (์•ฑ ๊ฐ„ ์ „ํ™˜)
โ€ข ์ƒ์ฒด ์ธ์ฆ ์ง€์› (์ง€๋ฌธ, Face ID)
โ€ข ๋” ๋น ๋ฅธ ๋กœ๊ทธ์ธ

๋‹จ์ :
โ€ข ํ”Œ๋žซํผ๋ณ„ ๊ตฌํ˜„ ํ•„์š” (iOS, Android)
โ€ข SDK ์—…๋ฐ์ดํŠธ ๊ด€๋ฆฌ ํ•„์š”
์›น๋ทฐ ๋ฐฉ์‹ ์žฅ์ :
โ€ข ์›น๊ณผ ๋™์ผํ•œ ์ฝ”๋“œ ์‚ฌ์šฉ
โ€ข ์œ ์ง€๋ณด์ˆ˜ ์‰ฌ์›€

๋‹จ์ :
โ€ข ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๋–จ์–ด์ง
โ€ข ์ผ๋ถ€ ํ”Œ๋žซํผ์—์„œ ์ œํ•œ (์˜ˆ: ํŽ˜์ด์Šค๋ถ์€ ์›น๋ทฐ ๋กœ๊ทธ์ธ ์ฐจ๋‹จ)

๋ชจ๋ฐ”์ผ API ์—”๋“œํฌ์ธํŠธ

๋ชจ๋ฐ”์ผ ์•ฑ์„ ์œ„ํ•œ RESTful API:

<?php
// api/social-login.php
header('Content-Type: application/json');

$method = $_SERVER['REQUEST_METHOD'];

if ($method === 'POST') {
    $input = json_decode(file_get_contents('php://input'), true);
    
    $provider = $input['provider']; // google, facebook, kakao, naver
    $accessToken = $input['access_token']; // ์•ฑ์—์„œ ๋ฐ›์€ ํ† ํฐ
    
    try {
        // ํ† ํฐ ๊ฒ€์ฆ ๋ฐ ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
        $userInfo = verifyAndGetUserInfo($provider, $accessToken);
        
        // DB์— ์ €์žฅ ๋˜๋Š” ์—…๋ฐ์ดํŠธ
        $user = saveOrUpdateUser($userInfo);
        
        // JWT ํ† ํฐ ์ƒ์„ฑ (์•ฑ์—์„œ ์‚ฌ์šฉํ•  ์ธ์ฆ ํ† ํฐ)
        $jwtToken = generateJWT($user['id']);
        
        echo json_encode([
            'success' => true,
            'token' => $jwtToken,
            'user' => [
                'id' => $user['id'],
                'email' => $user['email'],
                'name' => $user['name'],
                'avatar' => $user['avatar']
            ]
        ]);
    } catch (Exception $e) {
        http_response_code(400);
        echo json_encode([
            'success' => false,
            'error' => $e->getMessage()
        ]);
    }
}

function verifyAndGetUserInfo($provider, $accessToken) {
    switch ($provider) {
        case 'google':
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, 'https://www.googleapis.com/oauth2/v2/userinfo');
            curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $accessToken]);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            $response = curl_exec($ch);
            curl_close($ch);
            
            $data = json_decode($response, true);
            
            if (isset($data['error'])) {
                throw new Exception('์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค.');
            }
            
            return [
                'provider' => 'google',
                'provider_id' => $data['id'],
                'email' => $data['email'],
                'name' => $data['name'],
                'avatar' => $data['picture']
            ];
            
        // ๋‹ค๋ฅธ ํ”„๋กœ๋ฐ”์ด๋”๋“ค๋„ ๋น„์Šทํ•˜๊ฒŒ ๊ตฌํ˜„...
    }
}

function generateJWT($userId) {
    // JWT ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ
    // composer require firebase/php-jwt
    
    use Firebase\JWT\JWT;
    
    $key = getenv('JWT_SECRET');
    $payload = [
        'iss' => 'yourdomain.com',
        'aud' => 'yourdomain.com',
        'iat' => time(),
        'exp' => time() + (60 * 60 * 24 * 30), // 30์ผ
        'user_id' => $userId
    ];
    
    return JWT::encode($payload, $key, 'HS256');
}
๐Ÿ’ก ๋ชจ๋ฐ”์ผ ์•ฑ ๊ฐœ๋ฐœ ํŒ iOS: Sign in with Apple ํ•„์ˆ˜ (๋‹ค๋ฅธ ์†Œ์…œ ๋กœ๊ทธ์ธ ์ œ๊ณต ์‹œ)
Android: Google Play Services ์ตœ์‹  ๋ฒ„์ „ ์œ ์ง€
React Native: react-native-google-signin, react-native-fbsdk ์‚ฌ์šฉ
Flutter: google_sign_in, flutter_facebook_auth ํŒจํ‚ค์ง€ ์‚ฌ์šฉ

๐Ÿงช ํ…Œ์ŠคํŠธ ์ „๋žต

์†Œ์…œ ๋กœ๊ทธ์ธ์€ ์™ธ๋ถ€ ์„œ๋น„์Šค์— ์˜์กดํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ…Œ์ŠคํŠธ๊ฐ€ ๊นŒ๋‹ค๋กœ์›Œ. ํšจ๊ณผ์ ์ธ ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•์„ ์•Œ๋ ค์ค„๊ฒŒ! ๐Ÿ”ฌ

1. ์œ ๋‹› ํ…Œ์ŠคํŠธ

<?php
// tests/TokenManagerTest.php
use PHPUnit\Framework\TestCase;

class TokenManagerTest extends TestCase {
    private $tokenManager;
    private $mockPdo;
    
    protected function setUp(): void {
        // Mock PDO ์ƒ์„ฑ
        $this->mockPdo = $this->createMock(PDO::class);
        $this->tokenManager = new TokenManager($this->mockPdo);
    }
    
    public function testGetValidTokenReturnsTokenWhenNotExpired() {
        // ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„
        $mockStmt = $this->createMock(PDOStatement::class);
        $mockStmt->method('fetch')
            ->willReturn([
                'access_token' => 'valid_token',
                'refresh_token' => 'refresh_token',
                'token_expires' => date('Y-m-d H:i:s', time() + 3600)
            ]);
        
        $this->mockPdo->method('prepare')
            ->willReturn($mockStmt);
        
        // ํ…Œ์ŠคํŠธ ์‹คํ–‰
        $token = $this->tokenManager->getValidToken(1, 'google');
        
        // ๊ฒ€์ฆ
        $this->assertEquals('valid_token', $token);
    }
    
    public function testRefreshTokenWhenExpired() {
        // ๋งŒ๋ฃŒ๋œ ํ† ํฐ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ
        // ...
    }
}

// ์‹คํ–‰: vendor/bin/phpunit tests/TokenManagerTest.php

2. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

<?php
// tests/SocialLoginIntegrationTest.php
class SocialLoginIntegrationTest extends TestCase {
    public function testGoogleLoginFlow() {
        // ํ…Œ์ŠคํŠธ ๊ณ„์ •์œผ๋กœ ์‹ค์ œ OAuth ํ”Œ๋กœ์šฐ ํ…Œ์ŠคํŠธ
        $config = [
            'client_id' => getenv('TEST_GOOGLE_CLIENT_ID'),
            'client_secret' => getenv('TEST_GOOGLE_CLIENT_SECRET'),
            'redirect_uri' => 'http://localhost/test/callback'
        ];
        
        // 1. ์ธ์ฆ URL ์ƒ์„ฑ ํ…Œ์ŠคํŠธ
        $provider = new Google($config);
        $authUrl = $provider->getAuthorizationUrl();
        $this->assertStringContainsString('accounts.google.com', $authUrl);
        
        // 2. ํ† ํฐ ๊ตํ™˜ ํ…Œ์ŠคํŠธ (์‹ค์ œ code ํ•„์š”)
        // ์ฃผ์˜: ์‹ค์ œ API ํ˜ธ์ถœ์ด๋ฏ€๋กœ rate limit ๊ณ ๋ ค
    }
}

3. E2E ํ…Œ์ŠคํŠธ

// Selenium ๋˜๋Š” Cypress ์‚ฌ์šฉ
// cypress/integration/social-login.spec.js

describe('์†Œ์…œ ๋กœ๊ทธ์ธ', () => {
    it('๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ์ด ์ •์ƒ ์ž‘๋™ํ•œ๋‹ค', () => {
        cy.visit('/login');
        cy.get('.btn-google').click();
        
        // ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ ํ™•์ธ
        cy.url().should('include', 'accounts.google.com');
        
        // ํ…Œ์ŠคํŠธ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ
        cy.get('input[type="email"]').type('test@example.com');
        cy.get('#next').click();
        cy.get('input[type="password"]').type('testpassword');
        cy.get('#submit').click();
        
        // ์ฝœ๋ฐฑ ํ›„ ๋Œ€์‹œ๋ณด๋“œ๋กœ ์ด๋™ ํ™•์ธ
        cy.url().should('include', '/dashboard');
        cy.get('.user-name').should('contain', 'Test User');
    });
});
โš ๏ธ ํ…Œ์ŠคํŠธ ์ฃผ์˜์‚ฌํ•ญ 1. ํ…Œ์ŠคํŠธ ์ „์šฉ ์•ฑ ์ƒ์„ฑ: ํ”„๋กœ๋•์…˜ ์•ฑ๊ณผ ๋ถ„๋ฆฌ
2. Rate Limit ๊ณ ๋ ค: API ํ˜ธ์ถœ ํšŸ์ˆ˜ ์ œํ•œ ์ฃผ์˜
3. ๋ฏผ๊ฐ ์ •๋ณด ๋ณดํ˜ธ: ํ…Œ์ŠคํŠธ ๊ณ„์ • ์ •๋ณด๋ฅผ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ๋ง ๊ฒƒ
4. CI/CD ํ†ตํ•ฉ: GitHub Actions, Jenkins ๋“ฑ์—์„œ ์ž๋™ ํ…Œ์ŠคํŠธ

๐Ÿ“Š ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋ถ„์„

์†Œ์…œ ๋กœ๊ทธ์ธ์ด ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€, ์‚ฌ์šฉ์ž๋“ค์ด ์–ด๋–ค ๋ฐฉ์‹์„ ์„ ํ˜ธํ•˜๋Š”์ง€ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด! ๐Ÿ“ˆ

๋กœ๊ทธ์ธ ํ†ต๊ณ„ ์ˆ˜์ง‘

<?php
// ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ํ†ต๊ณ„ ๊ธฐ๋ก
function recordLoginStats($userId, $provider, $success = true) {
    global $pdo;
    
    $stmt = $pdo->prepare("
        INSERT INTO login_stats (
            user_id, provider, success, ip_address, 
            user_agent, created_at
        ) VALUES (?, ?, ?, ?, ?, NOW())
    ");
    
    $stmt->execute([
        $userId,
        $provider,
        $success ? 1 : 0,
        $_SERVER['REMOTE_ADDR'],
        $_SERVER['HTTP_USER_AGENT']
    ]);
}

// ๋Œ€์‹œ๋ณด๋“œ์—์„œ ํ†ต๊ณ„ ์กฐํšŒ
function getLoginStatistics($startDate, $endDate) {
    global $pdo;
    
    $stmt = $pdo->prepare("
        SELECT 
            provider,
            COUNT(*) as total_logins,
            SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful_logins,
            SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as failed_logins,
            ROUND(SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) as success_rate
        FROM login_stats
        WHERE created_at BETWEEN ? AND ?
        GROUP BY provider
        ORDER BY total_logins DESC
    ");
    
    $stmt->execute([$startDate, $endDate]);
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
$stats = getLoginStatistics('2024-01-01', '2024-01-31');
/*
๊ฒฐ๊ณผ:
[
    ['provider' => 'kakao', 'total_logins' => 1500, 'success_rate' => 98.5],
    ['provider' => 'google', 'total_logins' => 1200, 'success_rate' => 97.8],
    ['provider' => 'naver', 'total_logins' => 800, 'success_rate' => 96.2]
]
*/

์—๋Ÿฌ ๋ชจ๋‹ˆํ„ฐ๋ง

<?php
// Sentry ๊ฐ™์€ ์—๋Ÿฌ ํŠธ๋ž˜ํ‚น ์„œ๋น„์Šค ์‚ฌ์šฉ
composer require sentry/sentry

use Sentry\init;
use Sentry\captureException;

init([
    'dsn' => 'YOUR_SENTRY_DSN',
    'environment' => getenv('APP_ENV'),
    'traces_sample_rate' => 1.0
]);

// OAuth ์—๋Ÿฌ ์บก์ฒ˜
try {
    $token = $provider->getAccessToken('authorization_code', [
        'code' => $_GET['code']
    ]);
} catch (Exception $e) {
    // Sentry์— ์—๋Ÿฌ ์ „์†ก
    captureException($e, [
        'extra' => [
            'provider' => 'google',
            'user_id' => $_SESSION['user_id'] ?? null,
            'code' => $_GET['code'] ?? null
        ]
    ]);
    
    // ์‚ฌ์šฉ์ž์—๊ฒŒ ์นœ์ ˆํ•œ ๋ฉ”์‹œ์ง€
    die('๋กœ๊ทธ์ธ ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');
}

์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง

<?php
// ๋กœ๊ทธ์ธ ์†Œ์š” ์‹œ๊ฐ„ ์ธก์ •
function measureLoginPerformance($provider, $callback) {
    $startTime = microtime(true);
    
    try {
        $result = $callback();
        $duration = (microtime(true) - $startTime) * 1000; // ms
        
        // ์„ฑ๋Šฅ ๋กœ๊ทธ ๊ธฐ๋ก
        logPerformance($provider, $duration, true);
        
        return $result;
    } catch (Exception $e) {
        $duration = (microtime(true) - $startTime) * 1000;
        logPerformance($provider, $duration, false);
        throw $e;
    }
}

function logPerformance($provider, $duration, $success) {
    global $pdo;
    
    $stmt = $pdo->prepare("
        INSERT INTO performance_logs (
            provider, duration_ms, success, created_at
        ) VALUES (?, ?, ?, NOW())
    ");
    
    $stmt->execute([$provider, $duration, $success ? 1 : 0]);
    
    // ๋А๋ฆฐ ๋กœ๊ทธ์ธ ์•Œ๋ฆผ (3์ดˆ ์ด์ƒ)
    if ($duration > 3000) {
        error_log("Slow login detected: $provider took {$duration}ms");
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
$token = measureLoginPerformance('google', function() use ($provider) {
    return $provider->getAccessToken('authorization_code', [
        'code' => $_GET['code']
    ]);
});

๐ŸŒ ๋‹ค๊ตญ์–ด ์ง€์›

๊ธ€๋กœ๋ฒŒ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ ๋‹ค๋ฉด ๋‹ค๊ตญ์–ด ์ง€์›์€ ํ•„์ˆ˜์•ผ! ์†Œ์…œ ๋กœ๊ทธ์ธ๋„ ์‚ฌ์šฉ์ž์˜ ์–ธ์–ด์— ๋งž์ถฐ ํ‘œ์‹œํ•˜์ž. ๐Ÿ—บ๏ธ

<?php
// i18n/ko.php
return [
    'login.title' => '๋กœ๊ทธ์ธ',
    'login.google' => 'Google๋กœ ๊ณ„์†ํ•˜๊ธฐ',
    'login.facebook' => 'Facebook์œผ๋กœ ๊ณ„์†ํ•˜๊ธฐ',
    'login.kakao' => '์นด์นด์˜ค๋กœ ๊ณ„์†ํ•˜๊ธฐ',
    'login.naver' => '๋„ค์ด๋ฒ„๋กœ ๊ณ„์†ํ•˜๊ธฐ',
    'login.or' => '๋˜๋Š”',
    'login.email' => '์ด๋ฉ”์ผ๋กœ ๋กœ๊ทธ์ธ',
    'error.invalid_token' => '์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค.',
    'error.network' => '๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'
];

// i18n/en.php
return [
    'login.title' => 'Sign In',
    'login.google' => 'Continue with Google',
    'login.facebook' => 'Continue with Facebook',
    'login.kakao' => 'Continue with Kakao',
    'login.naver' => 'Continue with Naver',
    'login.or' => 'or',
    'login.email' => 'Sign in with Email',
    'error.invalid_token' => 'Invalid token.',
    'error.network' => 'Network error occurred.'
];

// ๋ฒˆ์—ญ ํ•จ์ˆ˜
function __($key, $lang = 'ko') {
    static $translations = [];
    
    if (!isset($translations[$lang])) {
        $translations[$lang] = require "i18n/$lang.php";
    }
    
    return $translations[$lang][$key] ?? $key;
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
echo __('login.google', $_SESSION['lang'] ?? 'ko');

์†Œ์…œ ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ๋„ ์‚ฌ์šฉ์ž์˜ ์–ธ์–ด ์„ค์ •์— ๋”ฐ๋ผ ํ‘œ์‹œ ์–ธ์–ด๊ฐ€ ๋ฐ”๋€Œ์–ด์•ผ ํ•ด:

<?php
// ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ ์–ธ์–ด ์„ค์ •
$authUrl = $provider->getAuthorizationUrl([
    'scope' => ['email', 'profile'],
    'hd' => 'example.com', // G Suite ๋„๋ฉ”์ธ ์ œํ•œ (์„ ํƒ)
    'hl' => $_SESSION['lang'] ?? 'ko' // ์–ธ์–ด ์„ค์ •
]);

// ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์–ธ์–ด ์„ค์ •
$params = [
    'client_id' => $config['client_id'],
    'redirect_uri' => $config['redirect_uri'],
    'response_type' => 'code',
    'lang' => $_SESSION['lang'] ?? 'ko' // ko, en
];

๐ŸŽ“ ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ์™„์ „ํ•œ ์†Œ์…œ ๋กœ๊ทธ์ธ ์‹œ์Šคํ…œ

์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋‚ด์šฉ์„ ๋ชจ๋‘ ํ•ฉ์ณ์„œ ์‹ค์ „์—์„œ ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์™„์ „ํ•œ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณด์ž! ๐Ÿ—๏ธ

ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

social-login-system/
โ”œโ”€โ”€ config/
โ”‚   โ”œโ”€โ”€ database.php
โ”‚   โ”œโ”€โ”€ oauth.php
โ”‚   โ””โ”€โ”€ app.php
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ Controllers/
โ”‚   โ”‚   โ”œโ”€โ”€ AuthController.php
โ”‚   โ”‚   โ””โ”€โ”€ SocialAuthController.php
โ”‚   โ”œโ”€โ”€ Models/
โ”‚   โ”‚   โ””โ”€โ”€ User.php
โ”‚   โ”œโ”€โ”€ Services/
โ”‚   โ”‚   โ”œโ”€โ”€ TokenManager.php
โ”‚   โ”‚   โ”œโ”€โ”€ SocialAuthService.php
โ”‚   โ”‚   โ””โ”€โ”€ UserService.php
โ”‚   โ””โ”€โ”€ Middleware/
โ”‚       โ”œโ”€โ”€ AuthMiddleware.php
โ”‚       โ””โ”€โ”€ RateLimitMiddleware.php
โ”œโ”€โ”€ public/
โ”‚   โ”œโ”€โ”€ index.php
โ”‚   โ”œโ”€โ”€ login.php
โ”‚   โ””โ”€โ”€ callback.php
โ”œโ”€โ”€ views/
โ”‚   โ”œโ”€โ”€ login.php
โ”‚   โ””โ”€โ”€ dashboard.php
โ”œโ”€โ”€ tests/
โ”‚   โ”œโ”€โ”€ Unit/
โ”‚   โ””โ”€โ”€ Integration/
โ”œโ”€โ”€ vendor/
โ”œโ”€โ”€ .env.example
โ”œโ”€โ”€ composer.json
โ””โ”€โ”€ README.md

ํ•ต์‹ฌ ํŒŒ์ผ ๊ตฌํ˜„

<?php
// src/Services/SocialAuthService.php
namespace App\Services;

use League\OAuth2\Client\Provider\Google;
use League\OAuth2\Client\Provider\Facebook;

class SocialAuthService {
    private $providers = [];
    private $config;
    
    public function __construct(array $config) {
        $this->config = $config;
        $this->initializeProviders();
    }
    
    private function initializeProviders() {
        // ๊ตฌ๊ธ€
        if (isset($this->config['google'])) {
            $this->providers['google'] = new Google([
                'clientId' => $this->config['google']['client_id'],
                'clientSecret' => $this->config['google']['client_secret'],
                'redirectUri' => $this->config['google']['redirect_uri']
            ]);
        }
        
        // ํŽ˜์ด์Šค๋ถ
        if (isset($this->config['facebook'])) {
            $this->providers['facebook'] = new Facebook([
                'clientId' => $this->config['facebook']['client_id'],
                'clientSecret' => $this->config['facebook']['client_secret'],
                'redirectUri' => $this->config['facebook']['redirect_uri'],
                'graphApiVersion' => 'v18.0'
            ]);
        }
        
        // ์นด์นด์˜ค, ๋„ค์ด๋ฒ„๋Š” ์ปค์Šคํ…€ ํ”„๋กœ๋ฐ”์ด๋” ๊ตฌํ˜„
    }
    
    public function getAuthorizationUrl($provider, $scopes = []) {
        if (!isset($this->providers[$provider])) {
            throw new \Exception("์ง€์›ํ•˜์ง€ ์•Š๋Š” ํ”„๋กœ๋ฐ”์ด๋”์ž…๋‹ˆ๋‹ค: $provider");
        }
        
        $options = [];
        if (!empty($scopes)) {
            $options['scope'] = $scopes;
        }
        
        return $this->providers[$provider]->getAuthorizationUrl($options);
    }
    
    public function getState($provider) {
        return $this->providers[$provider]->getState();
    }
    
    public function getAccessToken($provider, $code) {
        return $this->providers[$provider]->getAccessToken('authorization_code', [
            'code' => $code
        ]);
    }
    
    public function getUserInfo($provider, $token) {
        $user = $this->providers[$provider]->getResourceOwner($token);
        
        return [
            'provider' => $provider,
            'provider_id' => $user->getId(),
            'email' => $user->getEmail(),
            'name' => $user->getName(),
            'avatar' => $this->getAvatar($provider, $user),
            'access_token' => $token->getToken(),
            'refresh_token' => $token->getRefreshToken(),
            'expires' => $token->getExpires()
        ];
    }
    
    private function getAvatar($provider, $user) {
        switch ($provider) {
            case 'google':
                return $user->getAvatar();
            case 'facebook':
                return $user->getPictureUrl();
            default:
                return null;
        }
    }
}
<?php
// src/Controllers/SocialAuthController.php
namespace App\Controllers;

use App\Services\SocialAuthService;
use App\Services\UserService;

class SocialAuthController {
    private $socialAuth;
    private $userService;
    
    public function __construct(SocialAuthService $socialAuth, UserService $userService) {
        $this->socialAuth = $socialAuth;
        $this->userService = $userService;
    }
    
    public function redirectToProvider($provider) {
        try {
            $authUrl = $this->socialAuth->getAuthorizationUrl($provider);
            $_SESSION['oauth_state'] = $this->socialAuth->getState($provider);
            $_SESSION['oauth_provider'] = $provider;
            
            header('Location: ' . $authUrl);
            exit;
        } catch (\Exception $e) {
            $this->handleError($e);
        }
    }
    
    public function handleCallback() {
        // State ๊ฒ€์ฆ
        if (empty($_GET['state']) || $_GET['state'] !== $_SESSION['oauth_state']) {
            $this->handleError(new \Exception('Invalid state parameter'));
            return;
        }
        
        $provider = $_SESSION['oauth_provider'];
        
        try {
            // ํ† ํฐ ๋ฐ›๊ธฐ
            $token = $this->socialAuth->getAccessToken($provider, $_GET['code']);
            
            // ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
            $userData = $this->socialAuth->getUserInfo($provider, $token);
            
            // ์‚ฌ์šฉ์ž ์ €์žฅ ๋˜๋Š” ์—…๋ฐ์ดํŠธ
            $user = $this->userService->findOrCreateSocialUser($userData);
            
            // ์„ธ์…˜์— ์ €์žฅ
            $_SESSION['user_id'] = $user['id'];
            $_SESSION['user'] = $user;
            
            // ๋กœ๊ทธ์ธ ํ†ต๊ณ„ ๊ธฐ๋ก
            $this->userService->recordLogin($user['id'], $provider);
            
            // ๋Œ€์‹œ๋ณด๋“œ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
            header('Location: /dashboard');
            exit;
            
        } catch (\Exception $e) {
            $this->handleError($e);
        }
    }
    
    private function handleError(\Exception $e) {
        error_log('Social login error: ' . $e->getMessage());
        $_SESSION['error'] = '๋กœ๊ทธ์ธ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.';
        header('Location: /login');
        exit;
    }
}

์ด๋ ‡๊ฒŒ ๊ตฌ์กฐํ™”ํ•˜๋ฉด ์ฝ”๋“œ๊ฐ€ ๊น”๋”ํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์‰ฌ์›Œ์ ธ! ์ƒˆ๋กœ์šด ์†Œ์…œ ๋กœ๊ทธ์ธ ์ถ”๊ฐ€๋„ ๊ฐ„๋‹จํ•˜๊ณ . ๐Ÿ˜Š

๐Ÿš€ ๋ฐฐํฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

๊ฐœ๋ฐœ์ด ๋๋‚ฌ๋‹ค๊ณ  ๋ฐ”๋กœ ๋ฐฐํฌํ•˜๋ฉด ์•ˆ ๋ผ! ์‹ค์„œ๋น„์Šค ์ „์— ๊ผญ ํ™•์ธํ•ด์•ผ ํ•  ๊ฒƒ๋“ค์ด ์žˆ์–ด. โœ…

1 ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •

โœ“ Client ID/Secret์„ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌ
โœ“ .env ํŒŒ์ผ์ด .gitignore์— ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ
โœ“ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์˜ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ • ์™„๋ฃŒ
2 SSL ์ธ์ฆ์„œ

โœ“ HTTPS ์ ์šฉ ์™„๋ฃŒ
โœ“ ์ธ์ฆ์„œ ๋งŒ๋ฃŒ์ผ ํ™•์ธ
โœ“ ์ž๋™ ๊ฐฑ์‹  ์„ค์ • (Let's Encrypt)
3 Redirect URI ์—…๋ฐ์ดํŠธ

โœ“ ๋ชจ๋“  ์†Œ์…œ ํ”Œ๋žซํผ์˜ ๊ฐœ๋ฐœ์ž ์ฝ˜์†”์—์„œ ํ”„๋กœ๋•์…˜ URI ๋“ฑ๋ก
โœ“ ํ…Œ์ŠคํŠธ URI ์ œ๊ฑฐ ๋˜๋Š” ๋น„ํ™œ์„ฑํ™”
โœ“ ๋„๋ฉ”์ธ ์†Œ์œ ๊ถŒ ์ธ์ฆ ์™„๋ฃŒ
4 ๋ณด์•ˆ ์„ค์ •

โœ“ ์„ธ์…˜ ๋ณด์•ˆ ์„ค์ • ํ™œ์„ฑํ™”
โœ“ CSRF ํ† ํฐ ๊ฒ€์ฆ ํ™œ์„ฑํ™”
โœ“ Rate limiting ์„ค์ •
โœ“ SQL Injection ๋ฐฉ์–ด ํ™•์ธ
โœ“ XSS ๋ฐฉ์–ด ํ™•์ธ
5 ์—๋Ÿฌ ์ฒ˜๋ฆฌ

โœ“ ํ”„๋กœ๋•์…˜ ๋ชจ๋“œ์—์„œ ์ƒ์„ธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ˆจ๊น€
โœ“ ์—๋Ÿฌ ๋กœ๊น… ์„ค์ •
โœ“ ๋ชจ๋‹ˆํ„ฐ๋ง ๋„๊ตฌ ์—ฐ๋™ (Sentry, New Relic ๋“ฑ)
6 ์„ฑ๋Šฅ ์ตœ์ ํ™”

โœ“ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ธ๋ฑ์Šค ์„ค์ •
โœ“ ์บ์‹ฑ ํ™œ์„ฑํ™” (Redis, Memcached)
โœ“ CDN ์„ค์ •
โœ“ ์ด๋ฏธ์ง€ ์ตœ์ ํ™”
7 ๋ฐฑ์—…

โœ“ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž๋™ ๋ฐฑ์—… ์„ค์ •
โœ“ ์ฝ”๋“œ ๋ฒ„์ „ ๊ด€๋ฆฌ (Git)
โœ“ ๋ณต๊ตฌ ์ ˆ์ฐจ ๋ฌธ์„œํ™”
8 ํ…Œ์ŠคํŠธ

โœ“ ๋ชจ๋“  ์†Œ์…œ ๋กœ๊ทธ์ธ ํ”Œ๋กœ์šฐ ํ…Œ์ŠคํŠธ
โœ“ ๋‹ค์–‘ํ•œ ๋ธŒ๋ผ์šฐ์ €์—์„œ ํ…Œ์ŠคํŠธ
โœ“ ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ ํ…Œ์ŠคํŠธ
โœ“ ์—๋Ÿฌ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ
9 ๋ฒ•์  ์ค€์ˆ˜

โœ“ ๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ๋ฐฉ์นจ ์ž‘์„ฑ
โœ“ ์ด์šฉ์•ฝ๊ด€ ์ž‘์„ฑ
โœ“ ์ฟ ํ‚ค ์ •์ฑ… ๊ณต์ง€
โœ“ GDPR ์ค€์ˆ˜ (์œ ๋Ÿฝ ์‚ฌ์šฉ์ž ๋Œ€์ƒ ์‹œ)
10 ๋ฌธ์„œํ™”

โœ“ API ๋ฌธ์„œ ์ž‘์„ฑ
โœ“ ๋ฐฐํฌ ๊ฐ€์ด๋“œ ์ž‘์„ฑ
โœ“ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๊ฐ€์ด๋“œ ์ž‘์„ฑ
โœ“ ํŒ€์› ๊ต์œก ์ž๋ฃŒ ์ค€๋น„

๐Ÿ’ฌ ๋งˆ์น˜๋ฉฐ

์™€! ์—ฌ๊ธฐ๊นŒ์ง€ ์ฝ์—ˆ๋‹ค๋ฉด ์ •๋ง ๋Œ€๋‹จํ•ด! ๐Ÿ‘

์†Œ์…œ ๋กœ๊ทธ์ธ๊ณผ OAuth๋Š” ์ฒ˜์Œ์—๋Š” ๋ณต์žกํ•ด ๋ณด์ด์ง€๋งŒ, ํ•˜๋‚˜์”ฉ ์ดํ•ดํ•˜๊ณ  ๋‚˜๋ฉด ์ •๋ง ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์•ผ. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํฌ๊ฒŒ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ๊ณ , ๊ฐœ๋ฐœ์ž๋กœ์„œ๋„ ๋งŽ์€ ๊ฑธ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์–ด.

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์†Œ์…œ ๋กœ๊ทธ์ธ์ด ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ด์ž–์•„? ์ด์ œ ๋„ˆ๋„ ์ด๋Ÿฐ ์‹œ์Šคํ…œ์„ ์ง์ ‘ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๐ŸŽ‰

๊ธฐ์–ตํ•ด์•ผ ํ•  ํ•ต์‹ฌ ํฌ์ธํŠธ๋“ค:

1. ๋ณด์•ˆ์ด ์ตœ์šฐ์„ ์ด์•ผ - HTTPS, State ๊ฒ€์ฆ, ํ† ํฐ ์•”ํ˜ธํ™”๋Š” ํ•„์ˆ˜
2. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ƒ๊ฐํ•ด - ๊ฐ„๋‹จํ•˜๊ณ  ์ง๊ด€์ ์ธ UI
3. ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ์ฒ ์ €ํžˆ - ๋ฌด์—‡์ด ์ž˜๋ชป๋๋Š”์ง€ ๋ช…ํ™•ํ•˜๊ฒŒ
4. ํ…Œ์ŠคํŠธ๋ฅผ ๊ฒŒ์„๋ฆฌํ•˜์ง€ ๋งˆ - ์™ธ๋ถ€ ์„œ๋น„์Šค ์˜์กด์„ฑ์ด ํฌ๋‹ˆ๊นŒ
5. ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์„ค์ •ํ•ด - ๋ฌธ์ œ๋ฅผ ๋นจ๋ฆฌ ๋ฐœ๊ฒฌํ•˜๊ณ  ๋Œ€์‘

์ด์ œ ์ง์ ‘ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•ด๋ด! ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ์œผ๋ฉด ๊ฐ ํ”Œ๋žซํผ์˜ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜๊ณ , ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์•„.

ํ™”์ดํŒ…! ๋„ˆ๋Š” ํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ’ชโœจ

Google Facebook Kakao Naver ์†Œ์…œ ๋กœ๊ทธ์ธ ๋งˆ์Šคํ„ฐ ์™„๋ฃŒ! ์ด์ œ ๋‹น์‹ ๋„ OAuth ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค ๐ŸŽ“
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.