Как получить токен Laravel Sanctum для мобильного приложения
Полное руководство по аутентификации мобильных приложений с Laravel Sanctum. Узнайте, как правильно получать и обрабатывать токены для NativeScript-Vue и других мобильных фреймворков.
Как и когда получать токен Laravel Sanctum для мобильного приложения?
Добрый день! Использую Laravel для API и простой сайт на Vue. Настроено через Breeze и Sanctum.
Создал приложение на NativeScript-Vue. Возникли вопросы с аутентификацией:
Проблема с CSRF-токеном
GET-запросы к веб-роутам работают, но при попытке залогиниться через POST-запрос возникает ошибка 419 “page expired”.
Код запроса
login() {
Http.request({
url: "https://example.com/login",
method: 'POST',
headers: { 'Content-Type': 'application/json' },
content: JSON.stringify({
email: "admin@example.com",
password: "1111"
})
}).then(
(response) => {
console.log(response)
},
(e) => {
console.log(e)
}
)
}
Вопросы по аутентификации в мобильном приложении с Laravel Sanctum
- Является ли отключение VerifyCsrfToken для роута входа правильным решением?
- Нужно ли сначала запрашивать CSRF-токен (аналогично sanctum/csrf-cookie в SPA), сохранять его и затем прикреплять к запросам?
- Как правильно обрабатывать Sanctum токен, который создаётся для конкретного пользователя?
- Как Laravel Sanctum определяет источник запроса (сайт или мобильное приложение) и как настроить гибридную систему аутентификации?
Laravel Sanctum предоставляет несколько подходов к получению токенов для мобильных приложений. Для мобильных приложений рекомендуется создавать персональные токены доступа через API-роуты, а не использовать CSRF-токены веб-аутентификации. Мобильные приложения должны получать токены через POST-запросы к вашему API, а затем использовать эти токены для последующих запросов.
Содержание
- Основные подходы к аутентификации в мобильных приложениях
- Настройка CSRF для мобильных приложений
- Получение персональных токенов Sanctum
- Обработка токенов в мобильном приложении
- Гибридная система аутентификации
- Практические примеры реализации
- Заключение
- Источники
Основные подходы к аутентификации в мобильных приложениях
Laravel Sanctum предлагает два основных подхода для аутентификации мобильных приложений:
- Персональные токены доступа - рекомендуется для мобильных приложений
- Сессийная аутентификация - используется для веб-приложений
Для мобильных приложений персонифицированные токены являются предпочтительным подходом, так как они:
- Не требуют CSRF-токенов
- Могут быть отозваны в любой момент
- Имеют настраиваемые разрешения (scopes)
- Легко управляются через API
В отличие от веб-приложений, где используются сессии и CSRF-токены, мобильные приложения работают через stateless-аутентификацию, что делает персональные токены более подходящим решением.
Важно: Мобильные приложения не должны использовать веб-роуты (
/login,/register), которые ожидают CSRF-токенов. Вместо этого используйте специально созданные API-роуты для аутентификации.
Настройка CSRF для мобильных приложений
Отвечая на ваш первый вопрос - отключение VerifyCsrfToken для роута входа не является правильным решением с точки зрения безопасности. Вместо этого следует создать отдельные API-роуты для мобильных приложений.
Правильная настройка CSRF:
// В App/Http/Middleware/VerifyCsrfToken.php
protected $except = [
'api/mobile/*', // Все роуты для мобильных приложений
'api/auth/login', // Специальный API-роут для входа
];
Альтернативный подход - создание API-роутов:
// routes/api.php
Route::post('/auth/login', [AuthController::class, 'login'])->middleware('throttle:6,1');
Route::post('/auth/register', [AuthController::class, 'register'])->middleware('throttle:6,1');
Эти роуты не требуют CSRF-токенов, так как они находятся в группе api и используют другие механизмы защиты.
Совет: Для мобильных приложений создайте отдельную группу роутов с префиксом
api/mobile/илиapp/для лучшей организации кода.
Получение персональных токенов Sanctum
Создание контроллера для аутентификации:
// app/Http/Controllers/AuthController.php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required|string|min:8',
]);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
return response()->json(['message' => 'Invalid credentials'], 401);
}
// Создаем персональный токен для мобильного приложения
$token = $user->createToken('mobile-app', ['*'])->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token,
'token_type' => 'Bearer'
]);
}
}
Настройка модели User:
Убедитесь, что ваша модель User использует трейт HasApiTokens:
// app/Models/User.php
namespace App\Models;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use HasApiTokens, // Добавьте этот трейт
// ... другие трейты
}
Обновление роутов:
// routes/api.php
use App\Http\Controllers\AuthController;
Route::post('/auth/login', [AuthController::class, 'login']);
Route::post('/auth/register', [AuthController::class, 'register']);
// Защищенные роуты с токенами
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', function (Request $request) {
return $request->user();
});
Route::post('/logout', function (Request $request) {
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Successfully logged out']);
});
});
Обработка токенов в мобильном приложении
Обновление кода входа в NativeScript-Vue:
// В вашем мобильном приложении
login() {
Http.request({
url: "https://example.com/api/auth/login",
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
content: JSON.stringify({
email: "admin@example.com",
password: "1111"
})
}).then(
(response) => {
const result = response.content.toJSON();
// Сохраняем токен для последующих запросов
this.saveToken(result.token);
console.log('Login successful:', result);
},
(e) => {
console.error('Login error:', e);
}
)
},
// Сохранение токена
saveToken(token) {
// Используйте secure storage для токена
ApplicationSettings.setString('auth_token', token);
},
// Получение заголовка с токеном
getAuthHeader() {
const token = ApplicationSettings.getString('auth_token');
return token ? { 'Authorization': `Bearer ${token}` } : {};
},
// Пример защищенного запроса
getProtectedData() {
Http.request({
url: "https://example.com/api/user",
method: 'GET',
headers: {
...this.getAuthHeader(),
'Accept': 'application/json'
}
}).then(
(response) => {
console.log('Protected data:', response.content.toJSON());
},
(e) => {
console.error('Protected request error:', e);
}
)
}
Обновление HTTP-клиента для автоматической авторизации:
// Создайте перехватчик запросов
const httpInterceptor = {
request: function(requestOptions) {
const token = ApplicationSettings.getString('auth_token');
if (token && requestOptions.url.includes('/api/')) {
requestOptions.headers = {
...requestOptions.headers,
'Authorization': `Bearer ${token}`
};
}
return requestOptions;
}
};
// Применение перехватчика
httpModule.interceptRequest(httpInterceptor);
Гибридная система аутентификации
Определение источника запроса:
Laravel Sanctum определяет источник запроса через несколько механизмов:
- User-Agent - можно проверить в middleware
- Контекст роутов - API vs веб роуты
- Кастомные заголовки - рекомендуется для мобильных приложений
Middleware для определения источника:
// app/Http/Middleware/DetermineAuthType.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class DetermineAuthType
{
public function handle(Request $request, Closure $next)
{
// Проверяем кастомный заголовок для мобильных приложений
if ($request->header('X-Client-Type') === 'mobile') {
$request->attributes->set('auth_type', 'mobile');
} else {
$request->attributes->set('auth_type', 'web');
}
return $next($request);
}
}
Настройка роутов с разными типами аутентификации:
// routes/web.php
Route::middleware(['web', 'auth.session'])->group(function () {
Route::get('/login', [AuthController::class, 'showLoginForm']);
Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:6,1');
});
// routes/api.php
Route::group([
'middleware' => ['api', 'throttle:60,1', 'determine.auth.type']
], function () {
// Для мобильных приложений
Route::post('/auth/login', [AuthController::class, 'mobileLogin'])->middleware('client:mobile');
// Для веб-приложений
Route::post('/auth/login', [AuthController::class, 'webLogin'])->middleware('client:web');
});
Middleware для разных типов клиентов:
// app/Http/Middleware/ClientTypeMiddleware.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ClientTypeMiddleware
{
public function handle(Request $request, Closure $next, $clientType)
{
if ($request->header('X-Client-Type') !== $clientType) {
return response()->json(['message' => 'Unauthorized client type'], 403);
}
return $next($request);
}
}
Практические примеры реализации
Пример 1: Простой вход для мобильного приложения
// NativeScript-Vue компонент
export default {
data() {
return {
email: '',
password: '',
isLoading: false
}
},
methods: {
async login() {
this.isLoading = true;
try {
const response = await fetch('https://example.com/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Client-Type': 'mobile' // Наш кастомный заголовок
},
body: JSON.stringify({
email: this.email,
password: this.password
})
});
const data = await response.json();
if (response.ok) {
ApplicationSettings.setString('auth_token', data.token);
this.$navigateTo('Home');
} else {
alert(data.message || 'Login failed');
}
} catch (error) {
console.error('Login error:', error);
alert('Network error occurred');
} finally {
this.isLoading = false;
}
}
}
}
Пример 2: Обновление токена при истечении срока действия
// Фабрика HTTP-запросов с автоматическим обновлением токена
const apiClient = {
async request(options) {
try {
const response = await fetch(options.url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${ApplicationSettings.getString('auth_token')}`
}
});
if (response.status === 401) {
// Токен истек, пытаемся обновить
await this.refreshToken();
// Повторяем оригинальный запрос
return this.request(options);
}
return response;
} catch (error) {
console.error('API request error:', error);
throw error;
}
},
async refreshToken() {
// Логика обновления токена
// или перенаправление на экран входа
ApplicationSettings.remove('auth_token');
throw new Error('Token expired');
}
};
Пример 3: Полный контроллер аутентификации для гибридной системы
// app/Http/Controllers/AuthController.php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class AuthController extends Controller
{
public function mobileLogin(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'password' => 'required|string|min:8',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
return response()->json(['message' => 'Invalid credentials'], 401);
}
// Проверяем, что у пользователя нет активных мобильных токенов
$user->tokens()->where('name', 'mobile-app')->delete();
$token = $user->createToken('mobile-app', ['*'])->plainTextToken;
return response()->json([
'user' => $user->only(['id', 'name', 'email']),
'token' => $token,
'token_type' => 'Bearer'
]);
}
public function webLogin(Request $request)
{
// Логика для веб-аутентификации с CSRF
}
}
Заключение
- Не отключайте CSRF-токены для важных роутов, а создавайте отдельные API-роуты для мобильных приложений
- Используйте персональные токены Sanctum вместо сессий для мобильных приложений
- Сохраняйте токены безопасно в хранилище вашего мобильного приложения
- Используйте Bearer-токены для авторизации в заголовках
Authorization - Реализуйте механизм обновления токенов для улучшения пользовательского опыта
Для вашей ситуации с NativeScript-Vue приложением рекомендую перейти на API-роуты /api/auth/login вместо веб-роута /login. Это решит проблему с CSRF и предоставит более чистую архитектуру для мобильных приложений.