Laravel 11. API Token Authentication

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
client 접근을 허용하기 위해서 도메인을 가져야 합니다.
vscode 의 extenstion [ Live Server ] 를 사용하겠습니다.
Install Extension "Live Server"

Start Live Server

New window && Open New Folder
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>
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}`
}
})
* Bearer 는 Oath2.0 에서 자주 사용합니다.
* Sanctum 이나 JWT 등도 "Bearer" 를 사용합니다.
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', [AuthController::class, 'profile'])->name('profile');
});
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 출입증 자체에 정보 내장