Другое

Как получить токен Laravel Sanctum для мобильного приложения

Полное руководство по аутентификации мобильных приложений с Laravel Sanctum. Узнайте, как правильно получать и обрабатывать токены для NativeScript-Vue и других мобильных фреймворков.

Как и когда получать токен Laravel Sanctum для мобильного приложения?

Добрый день! Использую Laravel для API и простой сайт на Vue. Настроено через Breeze и Sanctum.

Создал приложение на NativeScript-Vue. Возникли вопросы с аутентификацией:

Проблема с CSRF-токеном

GET-запросы к веб-роутам работают, но при попытке залогиниться через POST-запрос возникает ошибка 419 “page expired”.

Код запроса

javascript
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

  1. Является ли отключение VerifyCsrfToken для роута входа правильным решением?
  2. Нужно ли сначала запрашивать CSRF-токен (аналогично sanctum/csrf-cookie в SPA), сохранять его и затем прикреплять к запросам?
  3. Как правильно обрабатывать Sanctum токен, который создаётся для конкретного пользователя?
  4. Как Laravel Sanctum определяет источник запроса (сайт или мобильное приложение) и как настроить гибридную систему аутентификации?

Laravel Sanctum предоставляет несколько подходов к получению токенов для мобильных приложений. Для мобильных приложений рекомендуется создавать персональные токены доступа через API-роуты, а не использовать CSRF-токены веб-аутентификации. Мобильные приложения должны получать токены через POST-запросы к вашему API, а затем использовать эти токены для последующих запросов.

Содержание

Основные подходы к аутентификации в мобильных приложениях

Laravel Sanctum предлагает два основных подхода для аутентификации мобильных приложений:

  1. Персональные токены доступа - рекомендуется для мобильных приложений
  2. Сессийная аутентификация - используется для веб-приложений

Для мобильных приложений персонифицированные токены являются предпочтительным подходом, так как они:

  • Не требуют CSRF-токенов
  • Могут быть отозваны в любой момент
  • Имеют настраиваемые разрешения (scopes)
  • Легко управляются через API

В отличие от веб-приложений, где используются сессии и CSRF-токены, мобильные приложения работают через stateless-аутентификацию, что делает персональные токены более подходящим решением.

Важно: Мобильные приложения не должны использовать веб-роуты (/login, /register), которые ожидают CSRF-токенов. Вместо этого используйте специально созданные API-роуты для аутентификации.

Настройка CSRF для мобильных приложений

Отвечая на ваш первый вопрос - отключение VerifyCsrfToken для роута входа не является правильным решением с точки зрения безопасности. Вместо этого следует создать отдельные API-роуты для мобильных приложений.

Правильная настройка CSRF:

php
// В App/Http/Middleware/VerifyCsrfToken.php
protected $except = [
    'api/mobile/*', // Все роуты для мобильных приложений
    'api/auth/login', // Специальный API-роут для входа
];

Альтернативный подход - создание API-роутов:

php
// 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

Создание контроллера для аутентификации:

php
// 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:

php
// app/Models/User.php
namespace App\Models;

use Laravel\Sanctum\HasApiTokens;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use HasApiTokens, // Добавьте этот трейт
        // ... другие трейты
}

Обновление роутов:

php
// 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:

javascript
// В вашем мобильном приложении
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-клиента для автоматической авторизации:

javascript
// Создайте перехватчик запросов
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 определяет источник запроса через несколько механизмов:

  1. User-Agent - можно проверить в middleware
  2. Контекст роутов - API vs веб роуты
  3. Кастомные заголовки - рекомендуется для мобильных приложений

Middleware для определения источника:

php
// 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);
    }
}

Настройка роутов с разными типами аутентификации:

php
// 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 для разных типов клиентов:

php
// 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: Простой вход для мобильного приложения

javascript
// 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: Обновление токена при истечении срока действия

javascript
// Фабрика 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: Полный контроллер аутентификации для гибридной системы

php
// 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
    }
}

Заключение

  1. Не отключайте CSRF-токены для важных роутов, а создавайте отдельные API-роуты для мобильных приложений
  2. Используйте персональные токены Sanctum вместо сессий для мобильных приложений
  3. Сохраняйте токены безопасно в хранилище вашего мобильного приложения
  4. Используйте Bearer-токены для авторизации в заголовках Authorization
  5. Реализуйте механизм обновления токенов для улучшения пользовательского опыта

Для вашей ситуации с NativeScript-Vue приложением рекомендую перейти на API-роуты /api/auth/login вместо веб-роута /login. Это решит проблему с CSRF и предоставит более чистую архитектуру для мобильных приложений.

Источники

  1. Laravel Sanctum Documentation - Authentication
  2. Laravel API Authentication Best Practices
  3. NativeScript HTTP Module Documentation
  4. Laravel CSRF Protection Documentation
  5. Laravel Token Best Practices
Авторы
Проверено модерацией
Модерация