如何在Laravel中实现不要求用户登录即可验证电子邮件

31

我正在开发一个Laravel应用程序。我的应用程序正在使用Laravel内置的身份验证功能。在Laravel身份验证中,当用户注册时,会发送一封验证电子邮件。当用户验证电子邮件并点击电子邮件内部的链接时,如果用户还没有登录,则必须重新登录以确认电子邮件。

VerificationController

class VerificationController extends Controller
{
    use VerifiesEmails, RedirectsUsersBasedOnRoles;

    /**
     * Create a new controller instance.
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('signed')->only('verify');
        $this->middleware('throttle:6,1')->only('verify', 'resend');
    }

    public function redirectPath()
    {
        return $this->getRedirectTo(Auth::guard()->user());
    }
}

我尝试在这行上发表评论。

$this->middleware('auth');

但是它不起作用,而是抛出一个错误。我该如何使Laravel能够在用户未登录的情况下验证电子邮件?

但它并没有起作用,反而会出现错误。我如何让 Laravel 在用户未登录的情况下仍能够验证电子邮件?


为什么你想要这个?我可以中间人攻击那个邮件并劫持任何账号。 - Loek
RedirectsUsersBasedOnRoles 是做什么的?当你说“它不起作用”时,你是指出现了错误吗?是什么类型的错误? - Zoe Edwards
1
它会抛出一个错误,因为它期望你的用户实例。请参见:https://github.com/laravel/framework/blob/f88917adc292e7e2960e9336a0d89206b41155fe/src/Illuminate/Foundation/Auth/VerifiesEmails.php#L35 - adam
谢谢Adam。感谢Loek提出合理的理由。干杯! - Wai Yan Hein
1
@Loek,怎么回事?URL已经签名。 - AaronHS
9个回答

51

首先,像您所做的那样删除行$this->middleware('auth');

接下来,从VerifiesEmails trait中复制verify方法到您的VerificationController中,并稍作修改。该方法应如下所示:

public function verify(Request $request)
{
    $user = User::find($request->route('id'));

    if (!hash_equals((string) $request->route('hash'), sha1($user->getEmailForVerification()))) {
        throw new AuthorizationException;
    }

    if ($user->markEmailAsVerified())
        event(new Verified($user));

    return redirect($this->redirectPath())->with('verified', true);
}

这将覆盖VerifiesUsers trait中的方法并移除授权检查。

安全性(如有错误请纠正!)

尽管请求已签名并经过验证,但仍然是安全的。如果某人以某种方式获得了验证电子邮件的访问权限,则可能会验证另一个用户的电子邮件地址,但在99%的情况下,这几乎不构成风险。


1
那么我可以使用这种方法而不必担心安全问题吗? - Ishaan
这是为 Laravel 6 编辑的吗?在之前的版本中,自定义哈希比较不是必需的,对吧? - Traxo
调用未定义的方法 App\User::getEmailForVerification()。 - Sead Lab
@SeadLab 请确保在用户模型上包含MustVerifyEmail trait。除此之外,我不确定Laravel 7是否有任何更改。 - Wouter Florijn

6

以下是一个更为具有未来性的解决方案:

class VerificationController extends Controller
{

    // …

    use VerifiesEmails {
        verify as originalVerify;
    }

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth'); // DON'T REMOVE THIS
        $this->middleware('signed')->only('verify');
        $this->middleware('throttle:6,1')->only('verify', 'resend');
    }

    /**
     * Mark the authenticated user's email address as verified.
     *
     * @param Request $request
     * @return Response
     *
     * @throws AuthorizationException
     */
    public function verify(Request $request)
    {
        $request->setUserResolver(function () use ($request) {
            return User::findOrFail($request->route('id'));
        });
        return $this->originalVerify($request);
    }
}

当未经身份验证的用户点击电子邮件确认链接时,将发生以下情况:
  1. 用户将被重定向到登录界面 1
  2. 用户输入凭据并成功登录 2
  3. 用户将被重定向回电子邮件确认URL
  4. 邮件将标记为已确认

1 此时电子邮件不会被标记为已确认。

2 用户可能多次输入错误的凭据。只要输入正确的凭据,就会被重定向到预期的电子邮件确认URL。


1
然而,在 Laravel 7.x 中,VerifiesEmails trait 已被移除。 - Nina Lisitsinskaya
我用这个代码会得到 "您必须先验证您的电子邮件" 的信息。 - Gaurav

4

使未登录用户(即没有经过验证)可以进行电子邮件验证的解决方案:

更改以下文件:app/Http/Controllers/Auth/VerificationController.php:

  1. $this->middleware('auth');更改为$this->middleware('auth')->except('verify');
  2. 复制VerifiesEmails trait中的verify()方法。
  3. 编辑verify()方法以便在没有$request->user()数据的情况下正常工作。

我在VerificationController中的verify()方法如下所示:

public function verify(\Illuminate\Http\Request $request)
{
    $user = User::find($request->route('id'));

    if ($request->route('id') != $user->getKey()) {
        throw new AuthorizationException;
    }

    if ($user->markEmailAsVerified())
        event(new Verified($user));

    return redirect()->route('login')->with('verified', true);
}

签名中间件

Laravel使用名为signed的中间件来检查应用程序生成的URL的完整性。Signed会检查URL是否在创建后被修改过。尝试更改url中的id、到期时间或签名将导致错误 - 这是非常有效和有用的中间件,以保护verify()方法。

了解更多信息:https://laravel.com/docs/8.x/urls#signed-urls

(可选)

我将用户重定向到登录路由,而不是原本要访问的路由,有两个原因。1)登录后,它将尝试将用户重定向到电子邮件验证链接,导致错误;2)我想使用附加到重定向上的验证为true的闪存数据,在登录页面上显示警报,如果用户已成功验证其电子邮件地址。

我的登录页面警告示例:

@if(session()->has('verified'))
    <div class="alert alert-success">Your email address has been successfully verified.</div>
@endif

建议

如果您对如何改善此代码有任何建议,请告诉我。我很乐意编辑这个答案。


非常好结构化的、有用的回答。谢谢! :) - Tobija Fischer

3
// For Laravel 6 and Above 
use Illuminate\Auth\Events\Verified; 
use Illuminate\Http\Request; 
use App\User;

// comment auth middleware
//$this->middleware('auth');

public function verify(Request $request)
{
    $user = User::find($request->route('id'));

    if (!hash_equals((string) $request->route('hash'), sha1($user->getEmailForVerification()))) {
        throw new AuthorizationException;
    }

    if ($user->markEmailAsVerified())
        event(new Verified($user));

    return redirect($this->redirectPath())->with('verified', true);
}

1
没有用:完全与被接受答案中的代码相同,并且添加在其最后编辑之后。 - Jan Żankowski

2

请不要完全删除$this->middleware('auth'),因为那会影响重定向。如果您将其删除,则未经身份验证的用户将被重定向到“/email/verify”,而不是“/login”

所以在“VerificationController”中,$this->middleware('auth');将更改为$this->middleware('auth')->except('verify');

还需要从“VerifiesEmails”中复制“verify”函数到“VerificationController”中,并在该函数顶部添加以下两行代码:

$user = User::find($request->route('id'));

auth()->login($user);

所以你正在通过编程方式登录用户,然后执行进一步的操作。


2
以下是我的看法。验证需要用户登录才能完成验证,因此我们可以覆盖验证功能并使用链接中接收到的ID登录用户。这是安全的,因为如果Laravel无法验证URL中的签名,则不会调用验证函数,因此即使有人篡改了URL,他们也无法绕过它。
请转到您的VerificationController并在文件末尾添加以下功能。
public function verify(Request $request)
{
    if (!auth()->check()) {
        auth()->loginUsingId($request->route('id'));
    }

    if ($request->route('id') != $request->user()->getKey()) {
        throw new AuthorizationException;
    }

    if ($request->user()->hasVerifiedEmail()) {
        return redirect($this->redirectPath());
    }

    if ($request->user()->markEmailAsVerified()) {
        event(new Verified($request->user()));
    }

    return redirect($this->redirectPath())->with('verified', true);
}

注意

请确保在“config/session.php”中将same_site的值设置为“lax”。如果将其设置为“strict”,则如果您从另一个站点重定向,则不会保留会话。例如,如果您从Gmail点击验证链接,则会话cookie不会保留,因此它不会将您重定向到仪表板,但是它会在数据库中设置“email_verified_at”字段,标记验证成功。用户不会知道发生了什么,因为它会将用户重定向到登录页面。当您将其设置为“strict”时,如果您直接复制验证链接到浏览器地址栏中,则会起作用,但如果用户从Gmail Web客户端点击链接,则不会起作用,因为它使用重定向来跟踪链接。


不错的尝试。对我来说正常工作。谢谢。 - Mohammad H.
我不得不删除 $this->middleware('auth') 才能使它工作。 - pableiros

1

要使用内部的 Laravel 逻辑(而不是覆盖逻辑),我们只需创建 $request->user() 并调用 trait 的 verify 方法。当验证成功时,手动登录用户。

use VerifiesEmails {
    verify as parentVerify;
}

public function verify(Request $request)
{
    $user = User::find($request->route('id'));
    if (!$user) return abort(404);

    $request->setUserResolver(function () use($user) {
        return $user;
    });

    return $this->parentVerify($request);
}


public function verified(Request $request)
{
    Auth::login($request->user());
}

1
如果您想在未登录的情况下激活用户帐户,可以通过以下两个步骤完成:
1- 在VerificationController中删除或注释掉Auth中间件
示例如下:
public function __construct()
{
    //$this->middleware('auth');
    $this->middleware('signed')->only('verify');
    $this->middleware('throttle:6,1')->only('verify', 'resend');
}

2- 由于验证路由传递了{id},您可以只编辑verify函数,通过路由id请求查找用户,代码如下:

文件路径:*:\yourproject\vendor\laravel\framework\src\Illuminate\Foundation\Auth\VerifiesEmails.php

$user = User::findOrfail($request->route('id'));

完整的例子
public function verify(Request $request)
{
    $user = User::findOrfail($request->route('id'));

    if (! hash_equals((string) $request->route('id'), (string) $user->getKey())) {
        throw new AuthorizationException;
    }

    if (! hash_equals((string) $request->route('hash'), sha1($user->getEmailForVerification()))) {
        throw new AuthorizationException;
    }

    if ($user->hasVerifiedEmail()) {
        return redirect($this->redirectPath())->with('verified', true);
    }

    if ($user->markEmailAsVerified()) {
        event(new Verified($request->user()));
    }

    return redirect($this->redirectPath())->with('registered', true);
}

嗨,Surf。请考虑为您的代码示例添加描述。 - Eyk Rehbein

0

我更改了EmailVerificationRequest,但我知道这是错误的,不过它还是能工作的。 警告 这会影响供应商。

protected $user;
public function authorize()
{
    $this->user = \App\Models\User::find($this->route('id'));
    if ($this->user != null){
        if (! hash_equals((string) $this->route('id'),
            (string) $this->user->getKey())) {
            return false;
        }

        if (! hash_equals((string) $this->route('hash'),
            sha1($this->user->getEmailForVerification()))) {
            return false;
        }

        return true;
    }
    return false;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接