PHP项目Symfony remember_me与cookie

wen PHP项目 2

Symfony Remember Me 功能与 Cookie 详解

基本配置

security.yaml 配置

# config/packages/security.yaml
security:
    firewalls:
        main:
            remember_me:
                secret: '%kernel.secret%'
                lifetime: 604800 # 7天,单位秒
                path: /
                domain: ~ # 默认为当前域
                secure: false # 是否仅HTTPS
                httponly: true
                samesite: lax
                always_remember_me: false
                remember_me_parameter: _remember_me # 表单中的字段名

登录表单实现

{# templates/security/login.html.twig #}
<form method="post">
    <input type="email" name="_username" value="{{ last_username }}">
    <input type="password" name="_password">
    {# Remember me checkbox #}
    <label>
        <input type="checkbox" name="_remember_me" checked>
        记住我
    </label>
    <button type="submit">登录</button>
</form>

自定义 Remember Me 服务

// src/Security/RememberMeService.php
namespace App\Security;
use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken;
use Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface;
class RememberMeService implements TokenProviderInterface
{
    private $connection;
    public function __construct($connection)
    {
        $this->connection = $connection;
    }
    public function loadTokenBySeries(string $series): PersistentToken
    {
        // 从数据库加载token
        $stmt = $this->connection->prepare(
            'SELECT * FROM remember_me_tokens WHERE series = ?'
        );
        $stmt->execute([$series]);
        if (!$data = $stmt->fetch()) {
            throw new \Symfony\Component\Security\Core\Exception\TokenNotFoundException();
        }
        return new PersistentToken(
            $data['class'],
            $data['username'],
            $data['series'],
            $data['value'],
            new \DateTime($data['last_used'])
        );
    }
    public function createToken(\Symfony\Component\Security\Core\Authentication\RememberMe\PersistentTokenInterface $token): void
    {
        // 创建新的remember me token
        $stmt = $this->connection->prepare(
            'INSERT INTO remember_me_tokens (class, username, series, value, last_used) VALUES (?, ?, ?, ?, ?)'
        );
        $stmt->execute([
            $token->getClass(),
            $token->getUsername(),
            $token->getSeries(),
            $token->getValue(),
            $token->getLastUsed()->format('Y-m-d H:i:s')
        ]);
    }
    public function deleteTokenBySeries(string $series): void
    {
        // 删除token
        $stmt = $this->connection->prepare(
            'DELETE FROM remember_me_tokens WHERE series = ?'
        );
        $stmt->execute([$series]);
    }
    public function updateToken(string $series, string $tokenValue, \DateTime $lastUsed): void
    {
        // 更新token
        $stmt = $this->connection->prepare(
            'UPDATE remember_me_tokens SET value = ?, last_used = ? WHERE series = ?'
        );
        $stmt->execute([$tokenValue, $lastUsed->format('Y-m-d H:i:s'), $series]);
    }
}

Cookie 管理

自定义 Cookie 配置

// src/EventListener/RememberMeCookieListener.php
namespace App\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\Security\Http\Event\LogoutEvent;
class RememberMeCookieListener implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            LoginSuccessEvent::class => 'onLoginSuccess',
            LogoutEvent::class => 'onLogout',
        ];
    }
    public function onLoginSuccess(LoginSuccessEvent $event): void
    {
        // 登录成功后可以添加额外的cookie处理
        $request = $event->getRequest();
        $response = $event->getResponse();
        // 设置自定义cookie
        $this->setCustomCookie($response);
    }
    public function onLogout(LogoutEvent $event): void
    {
        // 退出时清除所有相关cookie
        $response = $event->getResponse();
        $this->clearRememberMeCookie($response);
    }
    private function setCustomCookie($response): void
    {
        $cookie = new \Symfony\Component\HttpFoundation\Cookie(
            'custom_remember_me',
            'some_value',
            time() + 3600 * 24 * 30, // 30天
            '/',
            null,
            false, // secure
            true,  // httponly
            false,
            'lax'
        );
        $response->headers->setCookie($cookie);
    }
    private function clearRememberMeCookie($response): void
    {
        $response->headers->clearCookie('REMEMBERME');
    }
}

数据库表结构

-- 持久化token 表
CREATE TABLE remember_me_tokens (
    id INT AUTO_INCREMENT PRIMARY KEY,
    class VARCHAR(255) NOT NULL,
    username VARCHAR(255) NOT NULL,
    series VARCHAR(88) NOT NULL UNIQUE,
    value VARCHAR(88) NOT NULL,
    last_used DATETIME NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_username (username),
    INDEX idx_series (series)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

安全检查与验证

// src/Security/RememberMeChecker.php
namespace App\Security;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices;
class RememberMeChecker extends AbstractRememberMeServices
{
    protected function processAutoLoginCookie(array $cookieParts, Request $request): ?UserInterface
    {
        // 验证remember me cookie
        if (count($cookieParts) !== 3) {
            throw new AuthenticationException('Invalid cookie format');
        }
        [$series, $tokenValue, $username] = $cookieParts;
        // 验证用户是否存在
        $user = $this->userProvider->loadUserByIdentifier($username);
        if (!$user) {
            throw new AuthenticationException('User not found');
        }
        // 验证token
        if (!$this->validateToken($series, $tokenValue)) {
            throw new AuthenticationException('Invalid token');
        }
        return $user;
    }
    private function validateToken(string $series, string $tokenValue): bool
    {
        // 实现token验证逻辑
        return true; // 简化示例
    }
    public function loginSuccess(Request $request, Response $response, TokenInterface $token): void
    {
        // 登录成功后的处理
    }
    public function logout(Request $request, Response $response, TokenInterface $token): void
    {
        // 退出时的处理
    }
}

最佳实践

  1. 安全配置

    PHP项目Symfony remember_me与cookie

    # 安全建议配置
    remember_me:
     secret: '%kernel.secret%'
     name: REMEMBERME
     lifetime: 2592000 # 30天
     path: /
     domain: ~
     secure: true # 生产环境建议true
     httponly: true
     samesite: strict
     persistent: true # 使用数据库存储
  2. 清理过期token

    // src/Command/CleanRememberMeTokensCommand.php
    namespace App\Command;

use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface;

class CleanRememberMeTokensCommand extends Command { protected static $defaultName = 'app:clean-remember-me-tokens';

protected function configure(): void
{
    $this->setDescription('Clean expired remember me tokens');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
    // 清理过期token
    $this->entityManager
        ->createQuery('DELETE FROM App\Entity\RememberMeToken t WHERE t.lastUsed < :date')
        ->setParameter('date', new \DateTime('-30 days'))
        ->execute();
    $output->writeln('Expired tokens cleaned');
    return Command::SUCCESS;
}

3. **定期清理cron job**:
```bash
# crontab
0 3 * * * php /path/to/project/bin/console app:clean-remember-me-tokens

这样配置的Remember Me功能可以安全地让用户保持登录状态,同时提供了完善的cookie管理和安全验证机制。

抱歉,评论功能暂时关闭!