Jednoduché API ve frameworku Slim 4 – č. 10 Autentizace uživatele
- Jednoduché API ve frameworku Slim 4 – č. 1 Instalace
- Jednoduché API ve frameworku Slim 4 – č. 2 Základní CRUD
- Jednoduché API ve frameworku Slim 4 – č. 3 Struktura API a připojení k databázi
- Jednoduché API ve frameworku Slim 4 – č. 4 Testování funkcionality našeho malého API
- Jednoduché API ve frameworku Slim 4 – č. 5 Vylepšení UserControlleru
- Jednoduché API ve frameworku Slim 4 – č. 6 Přidání Model a Repositories
- Jednoduché API ve frameworku Slim 4 – č. 7 Validace dat
- Jednoduché API ve frameworku Slim 4 – č. 8 Přidáme si do datbáze produkty
- Jednoduché API ve frameworku Slim 4 – č. 9 Přidání zboží uživatelům
- Jednoduché API ve frameworku Slim 4 – č. 10 Autentizace uživatele
- Jednoduché API ve frameworku Slim 4 – č. 11 Endpointy pouze pro přihlášené uživatele
- Jednoduché API ve frameworku Slim 4 – č. 12 Testování našeho API
- Jednoduché API ve frameworku Slim 4 – č. 13 Úpravy API pro přístup z Vue aplikace
- Jednoduché API ve frameworku Slim 4 – č. 14 Úpravy API pro přístup z Vue aplikace preflight request a token v hlavičce
A je to tady kruciální část našeho API. Samozřejmě, že jako v každé aplikaci i tady budeme chtít některé funkce našeho API omezit jen pro ověřené uživatele. Tedy např. výpis produktů, které patří konkrétnímu uživateli se asi měli zobrazit pouze tomuto uživateli. Takže se do toho pustíme. Nejprve tedy každému uživateli přiřadíme heslo. Takže v databázi si vytvoříme slupec pro heslo.
1 2 |
ALTER TABLE users ADD COLUMN password VARCHAR(255) NOT NULL; |
Pro hesla budeme používat funkci v php password_hash . Takže si připravím jednoduchý php script, který mi vygeneruje hahashované heslo, které si pak vložím uživateli do databáze
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#!/usr/bin/php <?php // Kontrola, zda bylo heslo zadáno jako argument if ($argc !== 2) { echo "Použití: php " . $argv[0] . " <heslo>\n"; echo "Příklad: php " . $argv[0] . " mojeHeslo123\n"; exit(1); } // Získání hesla z argumentu $password = $argv[1]; // Generování hashe $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]); // Výpis výsledků echo "\nVýsledky pro heslo: " . $password . "\n"; echo "--------------------------------\n"; echo "Password hash: " . $hash . "\n"; echo "Délka hashe: " . strlen($hash) . " znaků\n"; // Ověření, že hash funguje if (password_verify($password, $hash)) { echo "Ověření hashe: OK\n"; } else { echo "Ověření hashe: CHYBA\n"; } echo "\nSQL příkaz pro vložení hashe:\n"; echo "UPDATE users SET password = '" . $hash . "' WHERE id = YOUR_USER_ID;\n"; ?> |
spouštím ho v příkazové řádce
1 |
php generate_hash.php TojeMojeApiHeslo |
Takže přípravné práce mám dokončeny a teď začneme s úpravou našeho API. Protože naše API bude velmi jednoduché využiji nejjednodušší možnost autentifikace pomocí hesla a tokenu v cookie. Lze určitě použít i sofistikovanější způsoby např. JWT tokeny, ale já využiji tento jednoduchý přístup. Takže nejprve si v databázi vytvoříme tabulku pro ukládání tokenů.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
-- Tabulka pro tokeny CREATE TABLE user_tokens ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, token VARCHAR(64) NOT NULL, expires_at DATETIME NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, UNIQUE KEY unique_token (token) ); -- Index pro rychlejší vyhledávání tokenů CREATE INDEX idx_token ON user_tokens(token); |
Vytvoříme si nový adresář Service a v něm servisní třídu AuthService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
<?php namespace App\Service; use App\Repositories\UserRepository; use Random\RandomException; class AuthService { private UserRepository $userRepository; private array $cookieConfig; public function __construct(UserRepository $userRepository) { $this->userRepository = $userRepository; $this->cookieConfig = [ 'path' => '/', 'domain' => 'api.local', 'secure' => false, 'httponly' => true, 'token_expiry_hours' => 24 // přidáno pro lepší správu expirace ]; } public function attemptLogin(string $email, string $password): ?array { $user = $this->userRepository->findByEmail($email); if (!$user || !password_verify($password, $user['password'])) { return null; } $tokenData = $this->createAuthToken($user['id']); return [ 'user' => [ 'id' => $user['id'], 'name' => $user['name'], 'email' => $user['email'] ], 'token' => $tokenData['token'], 'cookie' => $this->createCookieHeader('auth_token', $tokenData['token'], $tokenData['expires']) ]; } public function registerUser(array $userData): ?array { if ($this->userRepository->findByEmail($userData['email'])) { return null; } try { $this->userRepository->beginTransaction(); $hashedPassword = password_hash($userData['password'], PASSWORD_DEFAULT); $userId = $this->userRepository->create([ 'name' => $userData['name'], 'email' => $userData['email'], 'password' => $hashedPassword ]); $tokenData = $this->createAuthToken($userId); $this->userRepository->commit(); return [ 'user' => [ 'id' => $userId, 'name' => $userData['name'], 'email' => $userData['email'] ], 'token' => $tokenData['token'], 'cookie' => $this->createCookieHeader('auth_token', $tokenData['token'], $tokenData['expires']) ]; } catch (\Exception $e) { $this->userRepository->rollBack(); throw $e; } } public function validateToken(string $token): ?array { return $this->userRepository->findByToken($token); } public function logout(string $token): string { $this->userRepository->deleteToken($token); return $this->createCookieHeader('auth_token', '', 1); // Expires immediately } /** * @throws RandomException */ private function createAuthToken(int $userId): array { $token = bin2hex(random_bytes(32)); $expiresAt = date('Y-m-d H:i:s', strtotime('+24 hours')); $expires = strtotime('+24 hours'); $this->userRepository->createToken($userId, $token, $expiresAt); return [ 'token' => $token, 'expires' => $expires ]; } private function createCookieHeader(string $name, string $value, int $expires): string { $cookie = urlencode($name) . '=' . urlencode($value); $params = array_merge($this->cookieConfig, ['expires' => $expires]); $cookie .= '; Expires=' . gmdate('D, d M Y H:i:s T', $params['expires']); $cookie .= '; Path=' . $params['path']; if (!empty($params['domain'])) { $cookie .= '; Domain=' . $params['domain']; } if ($params['httponly']) { $cookie .= '; HttpOnly'; } return $cookie; } } |
do UserRepository přidáme potřebné funkce
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
public function findByEmail(string $email): ?array { $stmt = $this->db->prepare('SELECT id, name, email, password FROM users WHERE email = ?'); $stmt->execute([$email]); $user = $stmt->fetch(PDO::FETCH_ASSOC); return $user ?: null; } public function createToken(int $userId, string $token, string $expiresAt): bool { $stmt = $this->db->prepare(' INSERT INTO user_tokens (user_id, token, expires_at) VALUES (?, ?, ?) '); return $stmt->execute([$userId, $token, $expiresAt]); } public function findByToken(string $token): ?array { $stmt = $this->db->prepare(' SELECT u.* FROM users u JOIN user_tokens ut ON u.id = ut.user_id WHERE ut.token = ? AND ut.expires_at > NOW() '); $stmt->execute([$token]); $user = $stmt->fetch(PDO::FETCH_ASSOC); return $user ?: null; } public function deleteToken(string $token): bool { $stmt = $this->db->prepare('DELETE FROM user_tokens WHERE token = ?'); return $stmt->execute([$token]); } public function deleteExpiredTokens(): bool { $stmt = $this->db->prepare('DELETE FROM user_tokens WHERE expires_at < NOW()'); return $stmt->execute(); } public function beginTransaction(): void { $this->db->beginTransaction(); } public function commit(): void { $this->db->commit(); } public function rollBack(): void { $this->db->rollBack(); } |
a vytvoříme AuthController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
<?php namespace App\Controllers; use App\Service\AuthService; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; class AuthController { private AuthService $authService; public function __construct(AuthService $authService) { $this->authService = $authService; } public function login(Request $request, Response $response): Response { $data = json_decode($request->getBody()->getContents(), true); if (!isset($data['email']) || !isset($data['password'])) { return $this->jsonResponse($response, [ 'success' => false, 'message' => 'Email and password are required' ], 400); } $result = $this->authService->attemptLogin($data['email'], $data['password']); if (!$result) { return $this->jsonResponse($response, [ 'success' => false, 'message' => 'Invalid credentials' ], 401); } return $this->jsonResponse($response, [ 'success' => true, 'user' => $result['user'] ]) ->withAddedHeader('Set-Cookie', $result['cookie']); } public function register(Request $request, Response $response): Response { $data = json_decode($request->getBody()->getContents(), true); if (!isset($data['name']) || !isset($data['email']) || !isset($data['password'])) { return $this->jsonResponse($response, [ 'success' => false, 'message' => 'Name, email and password are required' ], 400); } try { $result = $this->authService->registerUser($data); if (!$result) { return $this->jsonResponse($response, [ 'success' => false, 'message' => 'Email already exists' ], 409); } return $this->jsonResponse($response, [ 'success' => true, 'message' => 'Registration successful', 'user' => $result['user'] ], 201) ->withAddedHeader('Set-Cookie', $result['cookie']); } catch (\Exception $e) { return $this->jsonResponse($response, [ 'success' => false, 'message' => 'Registration failed' ], 500); } } public function logout(Request $request, Response $response): Response { $authToken = $request->getCookieParams()['auth_token'] ?? null; if ($authToken) { $cookieHeader = $this->authService->logout($authToken); } return $this->jsonResponse($response, [ 'success' => true, 'message' => 'Logged out successfully' ]) ->withAddedHeader('Set-Cookie', $cookieHeader ?? ''); } private function jsonResponse(Response $response, array $data, int $status = 200): Response { $response->getBody()->write(json_encode($data)); return $response ->withHeader('Content-Type', 'application/json') ->withStatus($status); } } |
a samozřejmě si přidáme routy
1 2 3 |
$app->post('/api/login', [AuthController::class, 'login']); $app->post('/api/register', [AuthController::class, 'register']); $app->post('/api/logout', [AuthController::class, 'logout']); |