PHP项目Symfony RememberMe

wen PHP项目 1

本文目录导读:

PHP项目Symfony RememberMe

  1. Remember Me 功能概述
  2. 基础配置
  3. 高级配置
  4. 多 Firewall 配置
  5. 自定义 Remember Me 行为
  6. 安全注意事项
  7. 调试 Remember Me
  8. 常见问题解决

我来为您详细讲解 Symfony 框架中的 Remember Me 功能。

Remember Me 功能概述

Remember Me(记住我)是 Symfony 的安全功能之一,允许用户在一段时间内保持登录状态,即使用户关闭了浏览器。

工作原理

  1. 用户登录时勾选"记住我"选项
  2. 系统生成一个持久的 cookie
  3. 当会话过期后,系统通过 cookie 自动重新验证用户
  4. 在指定时间内无需重新登录

基础配置

在 security.yaml 中配置

# config/packages/security.yaml
security:
    firewalls:
        main:
            # ... 其他配置
            remember_me:
                secret: '%kernel.secret%'  # 必须配置
                lifetime: 2592000          # 30天(单位:秒)
                path: /
                domain: null               # 默认当前域名
                secure: false              # HTTPS 下使用
                httponly: true
                samesite: null             # 或 'lax', 'strict', 'none'
                always_remember_me: false   # 是否始终记住登录
                remember_me_parameter: '_remember_me'  # 表单参数名
                token_provider: 'Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider'

登录表单配置

{# templates/security/login.html.twig #}
<form method="post">
    <div class="form-group">
        <input type="email" name="email" class="form-control" placeholder="Email">
    </div>
    <div class="form-group">
        <input type="password" name="password" class="form-control" placeholder="Password">
    </div>
    {# Remember Me 复选框 #}
    <div class="form-check">
        <input type="checkbox" 
               name="_remember_me" 
               id="remember_me" 
               class="form-check-input"
               checked>
        <label for="remember_me" class="form-check-label">记住我</label>
    </div>
    <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
    <button type="submit" class="btn btn-primary">登录</button>
</form>

高级配置

使用自定义 Token Provider

# config/packages/security.yaml
security:
    firewalls:
        main:
            remember_me:
                secret: '%kernel.secret%'
                token_provider: App\Security\RememberMe\CustomTokenProvider

自定义 Token Provider 实现

// src/Security/RememberMe/CustomTokenProvider.php
namespace App\Security\RememberMe;
use Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface;
use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentTokenInterface;
use Symfony\Component\Security\Core\Exception\TokenNotFoundException;
class CustomTokenProvider implements TokenProviderInterface
{
    private $connection;
    public function __construct(\Doctrine\DBAL\Connection $connection)
    {
        $this->connection = $connection;
    }
    public function loadTokenBySeries(string $series): PersistentTokenInterface
    {
        $row = $this->connection->executeQuery(
            'SELECT * FROM rememberme_token WHERE series = ?',
            [$series]
        )->fetchAssociative();
        if (!$row) {
            throw new TokenNotFoundException('No token found.');
        }
        return new PersistentToken(
            $row['class'],
            $row['username'],
            $row['series'],
            $row['value'],
            new \DateTime($row['last_used'])
        );
    }
    public function deleteTokenBySeries(string $series): void
    {
        $this->connection->delete('rememberme_token', ['series' => $series]);
    }
    public function updateToken(string $series, string $tokenValue, \DateTime $lastUsed): void
    {
        $this->connection->update(
            'rememberme_token',
            ['value' => $tokenValue, 'last_used' => $lastUsed],
            ['series' => $series]
        );
    }
    public function createToken(PersistentTokenInterface $token): void
    {
        $this->connection->insert('rememberme_token', [
            'class' => $token->getClass(),
            'username' => $token->getUsername(),
            'series' => $token->getSeries(),
            'value' => $token->getTokenValue(),
            'last_used' => $token->getLastUsed(),
        ]);
    }
}

使用 Doctrine 存储 Remember Me Token

# config/packages/security.yaml
security:
    firewalls:
        main:
            remember_me:
                secret: '%kernel.secret%'
                token_provider: 'Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider'

创建对应的数据库表:

CREATE TABLE rememberme_token (
    series VARCHAR(88) PRIMARY KEY NOT NULL,
    value VARCHAR(88) NOT NULL,
    last_used DATETIME NOT NULL,
    class VARCHAR(100) NOT NULL,
    username VARCHAR(200) NOT NULL
);

多 Firewall 配置

# config/packages/security.yaml
security:
    firewalls:
        api:
            pattern: ^/api/
            stateless: true
            # API 通常不使用 remember_me
        main:
            pattern: ^/
            form_login:
                login_path: app_login
                check_path: app_login
            remember_me:
                secret: '%kernel.secret%'
                lifetime: 2592000
                path: /
                # 不同的 firewall 可以有不同的配置
                remember_me_parameter: '_remember_me_main'

自定义 Remember Me 行为

自定义 Remember Me Handler

// src/Security/RememberMe/CustomRememberMeHandler.php
namespace App\Security\RememberMe;
use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeHandler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\User\UserInterface;
class CustomRememberMeHandler extends AbstractRememberMeHandler
{
    protected function processRememberMe(
        UserInterface $user, 
        TokenInterface $token, 
        Request $request
    ): void {
        // 自定义处理逻辑
        // 记录日志、发送通知等
        $this->createRememberMeCookie($request, $user, $token);
    }
    public function clearRememberMeCookie(Request $request): void
    {
        // 自定义清除逻辑
        parent::clearRememberMeCookie($request);
    }
}

在安全配置中使用自定义 Handler

# config/services.yaml
services:
    App\Security\RememberMe\CustomRememberMeHandler:
        arguments:
            $secret: '%kernel.secret%'
        tags:
            - { name: 'security.remember_me_handler', firewall: 'main' }

安全注意事项

安全性配置

# config/packages/security.yaml
security:
    firewalls:
        main:
            remember_me:
                secret: '%kernel.secret%'
                secure: true   # 仅通过 HTTPS 发送
                httponly: true # 防止 XSS 访问
                samesite: 'lax' # 防止 CSRF 攻击

加密强度

# config/packages/security.yaml
security:
    # 使用更强的加密算法
    encoders:
        App\Entity\User:
            algorithm: auto
            cost: 12
    firewalls:
        main:
            remember_me:
                secret: '%env(REMEMBER_ME_SECRET)%'  # 使用环境变量
                signature_hash: 'sha256'  # 使用更强的哈希算法

定期清理过期 Token

// src/Command/CleanupRememberMeTokensCommand.php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Doctrine\DBAL\Connection;
class CleanupRememberMeTokensCommand extends Command
{
    protected static $defaultName = 'app:cleanup-remember-me-tokens';
    private $connection;
    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
        parent::__construct();
    }
    protected function configure()
    {
        $this->setDescription('Clean up expired remember me tokens');
    }
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $affected = $this->connection->executeStatement(
            'DELETE FROM rememberme_token WHERE last_used < DATE_SUB(NOW(), INTERVAL 30 DAY)'
        );
        $output->writeln(sprintf('Cleaned %d expired tokens', $affected));
        return Command::SUCCESS;
    }
}

调试 Remember Me

检查 Cookie

// 在控制器中检查
public function checkRememberMeCookie(Request $request)
{
    $cookies = $request->cookies->all();
    // 查找 Remember Me cookie
    foreach ($cookies as $name => $value) {
        if (strpos($name, 'REMEMBERME') !== false) {
            // 检查 cookie
            dd($name, $value);
        }
    }
}

日志记录

# config/packages/security.yaml
security:
    firewalls:
        main:
            remember_me:
                # 添加调试日志
                always_authenticate_before_granting: true

常见问题解决

Remember Me 不工作

# 确保配置正确
security:
    firewalls:
        main:
            logout:
                # 确保 logout 时清除 remember me
                delete_cookies:
                    remember_me:
                        path: /
                        domain: null

Token 序列化问题

// 确保 User 类实现了 Serializable 接口
class User implements UserInterface, Serializable
{
    public function serialize()
    {
        return serialize([
            $this->id,
            $this->username,
            $this->email,
        ]);
    }
    public function unserialize($serialized)
    {
        list(
            $this->id,
            $this->username,
            $this->email
        ) = unserialize($serialized, ['allowed_classes' => false]);
    }
}

测试 Remember Me

// tests/Security/RememberMeTest.php
class RememberMeTest extends WebTestCase
{
    public function testRememberMe()
    {
        $client = static::createClient();
        // 登录时启用 Remember Me
        $crawler = $client->request('GET', '/login');
        $form = $crawler->selectButton('Login')->form([
            'email' => 'user@example.com',
            'password' => 'password',
            '_remember_me' => true,
        ]);
        $client->submit($form);
        // 检查是否设置了 Remember Me cookie
        $cookies = $client->getCookieJar()->all();
        $this->assertTrue(
            array_filter($cookies, function($cookie) {
                return strpos($cookie->getName(), 'REMEMBERME') !== false;
            }) !== []
        );
    }
}

这些配置和实现应该能帮助您在 Symfony 项目中成功集成 Remember Me 功能,根据具体需求,您可以选择不同的实现方式。

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