Hokma School :: Course 2 Laravel

Laravel 11. API Token Authentication

테디아저씨 2026. 5. 15. 21:04

 

 

 

WEB 브라우저 로그인


→ Session 저장
→ 로그인 유지

 

모바일앱 / 외부API

Flutter
React Native
외부 서비스


→ Token 인증

 

 

Session Token
브라우저 중심 앱/API 중심
쿠키 사용 Token 사용
서버 상태 저장 Stateless 가능

 

 

 

 

Laravel Sanctum 

Laravel 공식 API 인증 패키지
Laravel Sanctum

 

install package

composer require laravel/sanctum

 

migration to my project

php artisan vendor:publish --tag=sanctum-migrations

// old version
// php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

 

php artisan migrate

 

 

====================================================================================

Client

login.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Bootstrap demo</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
  </head>
  <body>
    <div class="container mt-5 col-lg-3 col-md-4 col-sm-12 mx-auto">
        <h1 class="text-center mb-4">Login</h1>
        <form method="post" id="loginForm" action="" autocomplete="off">
            <div class="mb-3">
                <label for="email" class="form-label">Email</label>
                <input type="email" class="form-control" id="email" name="email" aria-describedby="emailHelp">
            </div>
            <div class="mb-3">
                <label for="password" class="form-label">Password</label>
                <input type="password" class="form-control" id="password" name="password">
            </div>
            <div id="response_message"></div>
            <div class="mt-3">  
                <button type="button" onclick="login()" class="btn btn-primary">Submit</button>
            </div>
        </form>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>

    <script>
        function login() {
            var box = document.getElementById('response_message');
            box.textContent = '';

            fetch('http://127.0.0.1:8000/api/login', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Accept': 'application/json',
                },
                body: JSON.stringify({
                    email: document.getElementById('email').value,
                    password: document.getElementById('password').value,
                }),
            })
            .then(function (response) {
                console.log(response);
                return response.json().then(function (data) {
                    console.log(data);
                    if( response.ok ) {
                        box.className = 'mt-3 text-success';
                        box.textContent = '요청 성공';
                        localStorage.setItem('token', data.token);
                        localStorage.setItem('userName', data.user.name);
                        location.href = '/dashboard.html';
                    } else {
                        box.className = 'mt-3 text-danger';
                        if (data.errors) {
                            Object.values(data.errors).flat().forEach(function (message) {
                                box.textContent += message + '\n';
                            });
                        } else {
                            box.textContent = data.message || '요청 실패';
                        }
                    }
                });
            })
            .catch(function () {
                box.className = 'mt-3 text-danger';
                box.textContent = '요청 실패2';
            });
        }
    </script>

  </body>
</html>

 

server start

 

 

 

http://127.0.0.1:5500/login.html

 

====================================================================================

 

 

 

 

 


routes/api.php

use App\Http\Controllers\Api\AuthController;

Route::post('/login', [AuthController::class, 'login'])->name('login');

 

 

 

User Model

use Laravel\Sanctum\HasApiTokens;


#[Fillable(['name', 'email', 'password'])]
#[Hidden(['password', 'remember_token'])]


class User extends Authenticatable
{
    use HasApiTokens;
}

 

 

make controller for login

php artisan make:controller Api/AuthController

 

 

app/Http/Controllers/Api/AuthController.php

 

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;

class AuthController extends Controller
{

    public function login(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'email' => 'required|email',
            'password' => 'required',
        ],[
            'email.required' => 'email required!',
            'email.email' => 'email format is not valid!',
            'password.required' => 'password required!',
        ]);

        if ($validator->fails()) {
            return response()->json([
                'message' => 'validation failed!',
                'errors' => $validator->errors(),
            ], 422);
        }

        // 로그인 검사
        if (!Auth::attempt([
            'email' => $request->email,
            'password' => $request->password
        ])) {

            return response()->json([
                'message' => '로그인 실패'
            ], 401);
        }

        // 로그인 성공 사용자
        $user = Auth::user();

        // 토큰 생성
        $token = $user->createToken('api-token')->plainTextToken;

        // 응답
        return response()->json([
            'token' => $token,
            'user' => $user
        ]);
    }
}

 

 

config/cors.php  

<?php

return [
    'paths' => ['api/*', 'sanctum/csrf-cookie'],
    'allowed_methods' => ['*'],
    'allowed_origins' => [
        'http://127.0.0.1:8001',
    ],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => true,
];

 

항목역할

paths CORS 적용 URL
allowed_methods 허용 HTTP Method
allowed_origins 허용 프론트 주소
allowed_origins_patterns 정규식 허용
allowed_headers 허용 요청 헤더
exposed_headers JS 에 노출할 응답 헤더
max_age OPTIONS 캐시 시간
supports_credentials 쿠키 인증 허용 여부

 

response

{
  "token": "1|asdasdasd..."
}

 

 

LocalStorage

1. 브라우저에 데이터를 저장하는 웹 저장소(Web Storage) 기능입니다.

2. 페이지를 새로고침하거나 브라우저를 껐다 켜도 데이터가 유지됩니다.

3. 숫자, 객체도 문자열로 변환하여 저장하고, 꺼내서 변환해야 한다.

 

// save
localStorage.setItem('{key}', '{value}');

// load
const keyName = localStorage.getItem('{key}');

// delete
localStorage.removeItem('{key}');

// delete all  :: 도메인별로 구분된다. ( 127.0.0.1:8000, 127.0.0.1:8001 서로 읽거나 쓰지 못함.)
localStorage.clear();



// Object


const user = {
  name: 'teddy',
  age: 20
};

localStorage.setItem('user', JSON.stringify(user));

const user = JSON.parse(localStorage.getItem('user'));

 

로컬스토리지에 토큰저장

save token to local storage ( javascript )

localStorage.setItem('token', data.token);
localStorage.setItem('userName', data.user.name);

 

Client Server

dashboard.html

 

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Bootstrap demo</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
  </head>
  <body>
    <div class="container mt-5 col-lg-3 col-md-4 col-sm-12 mx-auto">
        <h1 class="text-center mb-4">Dashboard</h1>
        <p class="text-center mb-4">Welcome, <span id="userName"></span></p>
        <button type="button" onclick="logout()" class="btn btn-primary">Logout</button>
    </div>
    <script>

        var token = localStorage.getItem('token');

        if( token ) {
            var userName = localStorage.getItem('userName');
            document.getElementById('userName').textContent = userName;
        } else {
            location.href = '/login.html';
        }
        
        function logout() {
            localStorage.removeItem('token');
            localStorage.removeItem('userName');
            location.href = '/login.html';
        }

    </script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
  </body>
</html>

 

http://127.0.0.1:5500/login.html

 

 

로컬스토리지 사용하기

use local storage token

const token = localStorage.getItem('token');
const userName = localStorage.getItem('userName');

 

 

fetch('/api/profile', {
 headers:{
   Authorization:`Bearer ${token}`
 }
})

 

 

delete Token

<script>
    function logout() {
        localStorage.removeItem('token');
        localStorage.removeItem('userName');
        location.href = '/login';
    }
</script>

 

 

Token 으로 사용자 확인

 

dashboard.html

<button type="button" onclick="getProfile()" class="btn btn-primary" data-bs-toggle="offcanvas" data-bs-target="#staticBackdrop" aria-controls="staticBackdrop">MyProfile</button>





<div class="offcanvas offcanvas-start" data-bs-backdrop="static" tabindex="-1" id="staticBackdrop" aria-labelledby="staticBackdropLabel">
<div class="offcanvas-header">
  <h5 class="offcanvas-title" id="staticBackdropLabel">My Profile</h5>
  <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
  <div>
        <p>Name: <span id="profileName"></span></p>
        <p>Email: <span id="profileEmail"></span></p>
        <p><span id="profilePhoto"></span></p>
  </div>
</div>
</div>


<script>
// add script
function getProfile() {
    fetch('http://127.0.0.1:8000/api/profile', {
        method: 'GET',
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'Authorization': 'Bearer ' + token,
        },
    })
    .then(function (response) {
        console.log(response);
        return response.json().then(function (data) {
            document.getElementById('profileName').textContent = data.name;
            document.getElementById('profileEmail').textContent = data.email;
            document.getElementById('profilePhoto').innerHTML = '<img src="' + data.photo + '" width="150" alt="Profile Photo">';
            console.log(data);
        });
    })
    .catch(function (error) {
        console.log(error);
    });
}

</script>

 

app/Http/Controllers/Api/AuthController.php

public function profile(Request $request)
{

    $user = Auth::user();

    $profile = [
        'id' => $user->id,
        'name' => $user->name,
        'email' => $user->email,
        'photo' => $user->photo,
    ];

    if( isset($profile['photo']) && $profile['photo'] != null){
        $profile['photo'] = url('storage/' . $profile['photo']);
    } else {
        $profile['photo'] = null;
    }

    return response()->json($profile);
}

 

routes/api.php

// 비교확인할것.

Route::get('/profile', [AuthController::class, 'profile'])->name('profile');



// auto Make Auth
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/profile', function (Request $request) {
        return $request->user();
    });
});

 

 

 

 

or

 

서버에서도 revoke 가능

 

 

 

보호된 API

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/profile', function (Request $request) {
        return $request->user();
    });
});

 

 

JWT ( Other Token Package )

  • 여러 서버 연동
  • 마이크로서비스
  • 외부 파트너 API
  • SSO 구조

 

 

Sanctum

토큰 → DB 조회 필요

 

JWT

토큰 자체에 정보 포함
DB 조회 없이 검증 가능

 

비유하자면

Sanctum 출입증 번호

JWT 출입증 자체에 정보 내장

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
</head>
<body>
<div class="container mt-5 col-lg-3 col-md-4 col-sm-12 mx-auto">
<h1 class="text-center mb-4">Login</h1>
<form method="post" id="loginForm" action="" autocomplete="off">
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" aria-describedby="emailHelp">
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<div id="response_message"></div>
<div class="mt-3">
<button type="button" onclick="login()" class="btn btn-primary">Submit</button>
</div>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>

<script>
function login() {
var box = document.getElementById('response_message');
box.textContent = '';

fetch('http://127.0.0.1:8000/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
email: document.getElementById('email').value,
password: document.getElementById('password').value,
}),
})
.then(function (response) {
console.log(response);
return response.json().then(function (data) {
console.log(data);
if( response.ok ) {
box.className = 'mt-3 text-success';
box.textContent = '요청 성공';
//localStorage.setItem('token', data.token);
//localStorage.setItem('userName', data.user.name);
//location.href = '/dashboard.html';
} else {
box.className = 'mt-3 text-danger';
if (data.errors) {
Object.values(data.errors).flat().forEach(function (message) {
box.textContent += message + '\n';
});
} else {
box.textContent = data.message || '요청 실패';
}
}
});
})
.catch(function () {
box.className = 'mt-3 text-danger';
box.textContent = '요청 실패2';
});
}
</script>

</body>
</html>