使用修改过的 MustVerifyEmail 特征的 Laravel API 无法获取请求用户

Posted

技术标签:

【中文标题】使用修改过的 MustVerifyEmail 特征的 Laravel API 无法获取请求用户【英文标题】:Laravel API using modified MustVerifyEmail trait cannot pick up request user 【发布时间】:2020-04-27 00:37:20 【问题描述】:

我使用 Laravel 6.x 作为带有外部(不同域)Vue 前端的后端,并且没有注册用户功能。我注册用户的方式是使用 Maatwebsite/Laravel-Excel 包导入批量用户——效果很好。

因此,当创建每个用户时,通过向每个用户发送电子邮件验证链接来创建工作,当他们第一次登录时,他们需要更改密码,同时他们的电子邮件被标记为已验证 - 这也应该有效很好。

问题在于,对于已经创建的用户工厂,他们的 email_verified_at 字段已填写 - 以及新导入的用户 - 我无法登录,因为自定义 EnsureEmailApiIsVerified 中间件无权访问 $request->user()。我发现我可以指定 'api' 的身份验证守卫,例如 $request->user('api') ,然后它可以获取用户,但只有他们的 Bearer 令牌(使用 Laravel Passport)随请求一起发送.

这确实很有意义,因为如果没有诸如令牌之类的标识符,系统将如何知道请求用户是谁。但是,用户模型上的标准“实现 MustVerifyEmail”和网络路由上的后续标准“EnsureEmailIsVerified”中间件如何获取 $request->user()?

有理由认为(标准的和我的自定义的)中间件都应该可以访问 $request->user() 或者两者都不应该。

现在我不得不修改并将相当多的框架控制器带入我的 App\Http 目录,但我几乎逐字复制了它们,只是更改了一些东西以确保它适用于我的 API 路由 - 因为将默认保护设置为config/auth.php 中的 'api' 而不是 'web' 并没有像我想象的那样在我的控制器中使用它作为默认值。

以下是我遵循的步骤:

    创建了一个自定义中间件并将其附加到 App\Http\Kernel.php 中的整个 'api' 中间件组
/**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class
        ],

        'api' => [
            'throttle:60,1',
            'bindings',
            'verifiedapi',

        ],
    ];

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'role' => \App\Http\Middleware\HasRole::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        'verifiedapi' => \App\Http\Middleware\EnsureApiEmailIsVerified::class,
    ];

然后这里是自定义的“EnsureApiEmailIsVerified”中间件:

<?php

namespace App\Http\Middleware;

use Closure;
use App\Http\Controllers\API\Auth\MustVerifyApiEmail;

class EnsureApiEmailIsVerified

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    
        if (! $request->user('api') ||
            ($request->user('api') instanceof MustVerifyApiEmail &&
            ! $request->user('api')->hasVerifiedEmail())) 
            return abort(403, 'Your email has not yet been verified.');
        

        return $next($request);
    

您将看到它引用了我的自定义“MustVerifyApiEmail”的一个实例,它是在用户模型上使用的特征,唯一偏离标准特征的地方是公共函数“sendApiEmailVerificationNotification”:

<?php

namespace App\Http\Controllers\API\Auth;

use App\Notifications\VerifyApiEmail;

trait MustVerifyApiEmail

    /**
     * Determine if the user has verified their email address.
     *
     * @return bool
     */
    public function hasVerifiedEmail()
    
        return ! is_null($this->email_verified_at);
    

    /**
     * Mark the given user's email as verified.
     *
     * @return bool
     */
    public function markEmailAsVerified()
    
        return $this->forceFill([
            'email_verified_at' => $this->freshTimestamp(),
        ])->save();
    

    /**
     * Send the email verification notification.
     *
     * @return void
     */
    public function sendApiEmailVerificationNotification()
    
        $this->notify(new VerifyApiEmail);
    

    /**
     * Get the email address that should be used for verification.
     *
     * @return string
     */
    public function getEmailForVerification()
    
        return $this->email;
    

这个新的 'sendApiEmailVerificationNotification()' 通过自定义的 VerifyApiEmail 通知通知 $request-user('api'),如下所示:

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Config;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;

class VerifyApiEmail implements ShouldQueue

    use Queueable;

    /**
     * The callback that should be used to build the mail message.
     *
     * @var \Closure|null
     */
    public static $toMailCallback;

    /**
     * Create a new notification instance.
     *
     * @return void
     */
    public function __construct()
    
        //
    

    /**
     * Get the notification's delivery channels.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function via($notifiable)
    
        return ['mail'];
    

    /**
     * Build the mail representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return \Illuminate\Notifications\Messages\MailMessage
     */
    public function toMail($notifiable)
    
        $verificationUrl = $this->verificationUrl($notifiable);

        if (static::$toMailCallback) 
            return call_user_func(static::$toMailCallback, $notifiable, $verificationUrl);
        

        return (new MailMessage)
            ->subject(Lang::get('Verify Email Address'))
            ->line(Lang::get('Please click the button below to verify your email address.'))
            ->action(Lang::get('Verify Email Address'), $verificationUrl)
            ->line(Lang::get('If you did not create an account, no further action is required.'));
    

    /**
     * Get the verification URL for the given notifiable.
     *
     * @param  mixed  $notifiable
     * @return string
     */
    protected function verificationUrl($notifiable)
    
        return URL::temporarySignedRoute(
            'verification.api.verify',
            Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
            [
                'id' => $notifiable->getKey(),
                'hash' => sha1($notifiable->getEmailForVerification()),
            ]
        );
    

    /**
     * Set a callback that should be used when building the notification mail message.
     *
     * @param  \Closure  $callback
     * @return void
     */
    public static function toMailUsing($callback)
    
        static::$toMailCallback = $callback;
    

新的api路由如下:

Route::namespace('API\Auth')->group(function () 
    Route::post('login', 'PassportController@login');
    Route::post('refresh', 'PassportController@refresh');
    Route::post('logout', 'PassportController@logout');
    Route::get('email/verify/id/hash', 'VerificationApiController@verify')->name('verification.api.verify');
    Route::get('email/resend', 'VerificationApiController@resend')->name('api.verification.resend');
);

而VerificationApiController如下:

<?php

namespace App\Http\Controllers\API\Auth;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Auth\Access\AuthorizationException;

class VerificationApiController extends Controller


    /**
     * Show the email verification notice.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function show(Request $request)
    
        //
    

    /**
     * Mark the authenticated user's email address as verified.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function verify(Request $request)
    
        if (! hash_equals((string) $request->route('id'), (string) $request->user('api')->getKey())) 
            throw new AuthorizationException;
        

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

        if ($request->user('api')->hasVerifiedEmail()) 
            return response()->json(['error' => 'Email already verified'], 422);
        

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

        return response()->json(['success' => 'Email verified!']);
    

    /**
     * Resend the email verification notification.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function resend(Request $request)
    
        $request->user('api')->sendApiEmailVerificationNotification();

        return response()->json(['success' => 'Email verification has been resent!']);
    

我还注意到,在用户模型上,它将 User 扩展为 Authenticatable,然后使用标准 MustVerifyEmail' - 所以我也将其从框架中取出,并将用法更改为新的 MustVerifyApiEmail - 如下所示:

<?php

namespace App\Http\Controllers\API\Auth;

use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use App\Http\Controllers\API\Auth\MustVerifyApiEmail;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

class User extends Model implements
    AuthenticatableContract,
    AuthorizableContract,
    CanResetPasswordContract

    use Authenticatable, Authorizable, CanResetPassword, MustVerifyApiEmail;

我的用户模型在顶部看起来像这样:

<?php

namespace App;

use Illuminate\Support\Str;
use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use App\Http\Controllers\API\Auth\User as Authenticatable;
use App\Http\Controllers\API\Auth\MustVerifyApiEmailInterface;

class User extends Authenticatable implements MustVerifyApiEmailInterface

    use HasApiTokens, Notifiable;
...

正如您所看到的,这是相当多的自定义 - 但理论上它应该都能正常工作,而且我没有收到任何可以使用的错误。以下是我得到的错误:

当我使用已验证或未验证电子邮件的用户登录时,我收到用户的电子邮件尚未验证的错误 - 但这只是因为它没有接收 $request->user('api' )。当我在返回转储 $request->user('api') 的请求之前尝试在中间件本身中引发错误时,它给了我 null

所以我的问题是,使用 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class 的标准中间件 - 这如何获取 $request->user()?

我是否遗漏了什么,或者我是否以错误的方式解决了这个问题?似乎当用户登录时它没有登录然后运行中间件 - 所以没有 $request->user('api') - 也许是因为我使用的是 Passport,但我认为应该发生的情况是中间件需要在对用户进行身份验证后运行,然后才能访问 $request->user('api')

非常感谢任何指导!

【问题讨论】:

【参考方案1】:

希望你早就解决了这个问题,但由于我之前遇到了同样的问题,我想我会分享我的解决方案。我想在登录 api 时使用 verify,但意识到这将在用户登录或验证之前调用,这意味着请求中没有用户。

就我而言,我对自定义 EnsureEmailsVerified 进行了更改,如下所示:

<?php

namespace App\Http\Middleware;

use App\User;
use Closure;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class EnsureApiEmailIsVerified

    /**
     * Handle an incoming request.
     *
     * @param  Request  $request
     * @param  \Closure  $next
     * @return JsonResponse
     */
    public function handle($request, Closure $next)
    
        $user = $request->user() ?? User::where('email', $request->get('username'))->first();
        if (! $user ||
            ($user instanceof MustVerifyEmail &&
                ! $user->hasVerifiedEmail())) 
            return  response()->json(['error' => [
                'message' => __('errors.email_not_verified'),
                'status_code'  => 401,
            ]], 401);
        

        return $next($request);
    

同样,我创建了一个自定义 VerifiesEmails 特征并更改了验证方法以根据电子邮件链接中的 id 查找用户,因为我知道这将用于未登录的新用户:

public function verify(Request $request)

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


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

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

    if ($user->hasVerifiedEmail()) 
        return new Response('', 204);
    

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

    if ($response = $this->verified($request)) 
        return $response;
    

    return new Response('', 204);

【讨论】:

以上是关于使用修改过的 MustVerifyEmail 特征的 Laravel API 无法获取请求用户的主要内容,如果未能解决你的问题,请参考以下文章

BluetoothGatt 显示 10,000 个相同的服务特征

Android 逆向修改运行中的 Android 进程的内存数据 ( 使用 IDA 分析要修改的内存特征 | 根据内存特征搜索修改点 | 修改进程内存 )

final关键字细节

sh 使用Sublime Text打开上次提交的所有修改过的文件

在 Linux shell 脚本中检索最近修改过的子目录?

如何使用android [关闭]获取RealmObject中的最后一个修改过的RealmObject或字段