基于Symfony Lexik Bundle实现手机号+OTP无密码认证方案咨询
解决方案:Symfony + Lexik JWT 支持双注册/认证方式
核心思路
Lexik JWT Bundle生成令牌仅需UserInterface实例,不需要依赖密码验证——密码验证是认证环节的逻辑,而非令牌生成的必要条件。我们可以通过以下步骤实现双认证流程:
- 扩展用户实体,支持手机号字段,允许邮箱/密码为空(适配无密码注册)
- 实现OTP生成、存储、验证逻辑
- 自定义OTP认证器,独立处理手机号+OTP的认证流程
- 配置Security,让邮箱密码认证和OTP认证共存
1. 修改用户实体
添加手机号字段,放宽邮箱/密码的非空约束(注意:邮箱密码注册时仍需校验必填):
// src/Entity/User.php use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\UserInterface; use Doctrine\ORM\Mapping\UniqueConstraint; #[ORM\Entity(repositoryClass: UserRepository::class)] #[UniqueConstraint(name: 'unique_phone', columns: ['phone'])] #[UniqueConstraint(name: 'unique_email', columns: ['email'])] class User implements UserInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] private int $id; #[ORM\Column(type: 'string', length: 20, nullable: false)] private string $phone; #[ORM\Column(type: 'string', length: 180, nullable: true)] private ?string $email = null; #[ORM\Column(type: 'string', nullable: true)] private ?string $password = null; #[ORM\Column(type: 'json')] private array $roles = []; // Getters & Setters public function getPassword(): ?string { return $this->password; } public function getUsername(): string { // 兼容Symfony Security,返回手机号或邮箱 return $this->email ?? $this->phone; } // 其他UserInterface方法(eraseCredentials、getSalt等)按需实现 }
2. OTP服务实现
用Symfony Cache存储OTP(带过期时间),避免重复使用:
// src/Service/OtpService.php namespace App\Service; use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; class OtpService { public function __construct(private AdapterInterface $cache) {} // 生成6位OTP,有效期5分钟 public function generateOtp(string $phone): string { $otp = (string)rand(100000, 999999); $cacheItem = $this->cache->getItem("otp_{$phone}"); $cacheItem->set($otp); $cacheItem->expiresAfter(300); $this->cache->save($cacheItem); return $otp; } // 验证OTP,成功后立即删除 public function validateOtp(string $phone, string $otp): bool { $cacheItem = $this->cache->getItem("otp_{$phone}"); if (!$cacheItem->isHit() || $cacheItem->get() !== $otp) { throw new AuthenticationException("无效或过期的验证码"); } $this->cache->deleteItem("otp_{$phone}"); return true; } }
3. 自定义OTP认证器
处理手机号+OTP的认证请求,验证通过后生成JWT令牌:
// src/Security/OtpAuthenticator.php namespace App\Security; use App\Entity\User; use App\Service\OtpService; use Doctrine\ORM\EntityManagerInterface; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; class OtpAuthenticator extends AbstractAuthenticator { public function __construct( private EntityManagerInterface $em, private OtpService $otpService, private JWTTokenManagerInterface $jwtManager ) {} // 匹配OTP验证接口 public function supports(Request $request): ?bool { return $request->getPathInfo() === '/api/auth/otp-verify' && $request->isMethod('POST'); } public function authenticate(Request $request): Passport { $payload = json_decode($request->getContent(), true); $phone = $payload['phone'] ?? ''; $otp = $payload['otp'] ?? ''; // 验证OTP $this->otpService->validateOtp($phone, $otp); // 自动注册新用户(如果手机号未注册) $user = $this->em->getRepository(User::class)->findOneBy(['phone' => $phone]); if (!$user) { $user = new User(); $user->setPhone($phone); $user->setRoles(['ROLE_USER']); $this->em->persist($user); $this->em->flush(); } return new SelfValidatingPassport( new UserBadge($phone, fn() => $user) ); } // 认证成功返回JWT public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { /** @var User $user */ $user = $token->getUser(); return new JsonResponse([ 'token' => $this->jwtManager->create($user) ]); } // 认证失败返回错误信息 public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { return new JsonResponse( ['error' => $exception->getMessage()], Response::HTTP_UNAUTHORIZED ); } }
4. 配置Security.yaml
让OTP认证器和邮箱密码认证共存:
# config/packages/security.yaml security: enable_authenticator_manager: true providers: app_user_provider: entity: class: App\Entity\User property: username # 对应User实体的getUsername方法 firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false api: pattern: ^/api stateless: true custom_authenticators: - App\Security\OtpAuthenticator - Lexik\Bundle\JWTAuthenticationBundle\Security\Authenticator\JWTAuthenticator # 邮箱密码登录接口 json_login: check_path: /api/auth/login username_path: email password_path: password success_handler: lexik_jwt_authentication.handler.authentication_success failure_handler: lexik_jwt_authentication.handler.authentication_failure password_hashers: App\Entity\User: 'auto'
5. 编写注册/发送OTP接口
发送OTP接口
// src/Controller/AuthController.php namespace App\Controller; use App\Service\OtpService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; class AuthController extends AbstractController { #[Route('/api/auth/send-otp', name: 'send_otp', methods: ['POST'])] public function sendOtp(Request $request, OtpService $otpService): JsonResponse { $payload = json_decode($request->getContent(), true); $phone = $payload['phone'] ?? ''; if (empty($phone)) { return new JsonResponse(['error' => '手机号不能为空'], 400); } // 生成OTP并发送(此处需替换为实际短信API调用) $otp = $otpService->generateOtp($phone); // $smsService->send($phone, "您的验证码是:{$otp}"); return new JsonResponse(['message' => '验证码已发送']); } }
邮箱密码注册接口
#[Route('/api/auth/register', name: 'register', methods: ['POST'])] public function register( Request $request, EntityManagerInterface $em, UserPasswordHasherInterface $passwordHasher, JWTTokenManagerInterface $jwtManager ): JsonResponse { $payload = json_decode($request->getContent(), true); $email = $payload['email'] ?? ''; $password = $payload['password'] ?? ''; if (empty($email) || empty($password)) { return new JsonResponse(['error' => '邮箱和密码不能为空'], 400); } $user = new User(); $user->setEmail($email); $user->setPassword($passwordHasher->hashPassword($user, $password)); $user->setRoles(['ROLE_USER']); $em->persist($user); $em->flush(); return new JsonResponse([ 'token' => $jwtManager->create($user), 'user' => ['email' => $user->getEmail()] ]); }
关键注意事项
- OTP安全:设置合理过期时间(5-10分钟),验证成功后立即删除缓存,避免重复使用
- 用户实体约束:通过UniqueConstraint确保手机号/邮箱唯一
- 短信服务:需集成第三方短信API(如阿里云、Twilio)实现OTP发送
- 权限控制:根据业务需求调整用户角色配置
内容的提问来源于stack exchange,提问作者YOUSSEF
相关产品推荐
相关产品推荐

