【问题标题】:Migrating old md5 passwords to bcrypt with Laravel 5.2's built in auth使用 Laravel 5.2 的内置身份验证将旧的 md5 密码迁移到 bcrypt
【发布时间】:2016-07-03 20:18:29
【问题描述】:

我正在将一个旧的 PHP 应用程序迁移到 Laravel 5.2。该应用程序有一个巨大的用户表(大约 50K 用户),密码都是 MD5 哈希。

显然这是不可接受的,但我不想向所有 50,000 名用户发送电子邮件要求他们重置密码,而是想在幕后将密码更改为 bcrypt 哈希。

为此,我想创建一个包含 MD5 哈希的 old_password 列,然后每当用户登录时,我都会根据 MD5 哈希(如果存在)检查密码,然后创建一个新的 bcrypt 哈希下次,删除 MD5 哈希。

我已经看到了一些关于如何执行此操作的示例(例如 thisthis),但没有一个专门用于 Laravel 5,也没有一个专门用于 Laravel 5.2 的内置身份验证。

有没有一种简洁的方法来调整内置身份验证来执行此操作,还是在这种情况下我最好编写自己的手动身份验证系统?

【问题讨论】:

    标签: laravel laravel-5


    【解决方案1】:

    所以,在 Laravel 5.8(可能更早)中,我认为最好/最安全的解决方案是使用事件监听器。我从其他解决方案中蚕食了一些代码...

    添加到您的 app\Providers\EventServiceProvider.php

        protected $listen = [
    ...
            'Illuminate\Auth\Events\Attempting' => [
                'App\Listeners\DrupalPasswordUpdate',
            ],
    ...
        ];
    
    

    然后创建文件app\Listeners\DrupalPasswordUpdate.php

    <?php
    
    namespace App\Listeners;
    
    use Illuminate\Auth\Events\Attempting;
    use Illuminate\Queue\InteractsWithQueue;
    use Illuminate\Contracts\Queue\ShouldQueue;
    
    class DrupalPasswordUpdate
    {
        public function handle(Attempting $event)
        {
            $this->check($event->credentials['password'], \App\User::where('email', $event->credentials['email'])->first()->password??'not found');
        }
    
        public function check($value, $hashedValue, array $options = [])
        {
            if($this->needsRehash($hashedValue))
            {
                if($this->user_check_password($value, $hashedValue))
                {
                    $newHashedValue = (new \Illuminate\Hashing\BcryptHasher)->make($value, $options);
                    \Illuminate\Support\Facades\DB::update('UPDATE users SET `password` = "'.$newHashedValue.'" WHERE `password` = "'.$hashedValue.'"');
                    $hashedValue = $newHashedValue;
                }
            }
        }
    
        public function needsRehash($hashedValue, array $options = [])
        {
            return substr($hashedValue, 0, 4) != '$2y$';
        }
    
        // DRUPAL PASSWORD FUNCTIONS
        function user_check_password($password, $stored_hash) {
          $hash = md5($password);
          return ($hash && $stored_hash == $hash);
        }
    }
    
    

    这将侦听登录尝试,检查数据库中的用户,并在适当时更新其密码,然后继续正常登录过程。

    你也可以用不同的散列方法替换 md5() 调用,你可以在你的 Drupal 安装中找到 include/password.inc

    【讨论】:

      【解决方案2】:

      我将 AuthController 从 @Leonardo Beal 更新到 Laravel 5.6。

      我将我的应用从 Laravel 4.2 迁移到 5.6,这就像一个魅力(将它添加到 app/Http/Controllers/Auth/LoginController.php):

      /**
       * Handle a login request to the application.
       *
       * @param  \Illuminate\Http\Request  $request
       * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse
       *
       * @throws \Illuminate\Validation\ValidationException
       */
      public function login(Request $request)
      {
          $this->validateLogin($request);
      
          // If the class is using the ThrottlesLogins trait, we can automatically throttle
          // the login attempts for this application. We'll key this by the username and
          // the IP address of the client making these requests into this application.
          if ($this->hasTooManyLoginAttempts($request)) {
              $this->fireLockoutEvent($request);
      
              return $this->sendLockoutResponse($request);
          }
      
          if ($this->attemptLogin($request)) {
              return $this->sendLoginResponse($request);
          }
      
          //If user got here it means the AUTH was unsuccessful
          //Try to log them IN using MD5
          if ($user = User::whereEmail($request->input('email'))
              ->wherePassword(md5($request->input('password')))->first()) {
              //It this condition is true, the user had the right password.
      
              //encrypt the password using bcrypt
              $user->password = bcrypt($request->input('password'));
              $user->save();
      
              $this->validateLogin($request);
      
              if ($this->hasTooManyLoginAttempts($request)) {
                  $this->fireLockoutEvent($request);
      
                  return $this->sendLockoutResponse($request);
              }
      
              if ($this->attemptLogin($request)) {
                  return $this->sendLoginResponse($request);
              }
          }
      
          // If the login attempt was unsuccessful we will increment the number of attempts
          // to login and redirect the user back to the login form. Of course, when this
          // user surpasses their maximum number of attempts they will get locked out.
          $this->incrementLoginAttempts($request);
      
          return $this->sendFailedLoginResponse($request);
      }
      

      【讨论】:

        【解决方案3】:

        在 Laravel 5.2 中,您的 AuthController.php 应该覆盖登录方法,只需添加以下内容。

        当登录失败时,它会尝试使用 md5() 来登录用户。

        public function login(Request $request)
        {
        
            $this->validateLogin($request);
        
            // If the class is using the ThrottlesLogins trait, we can automatically throttle
            // the login attempts for this application. We'll key this by the username and
            // the IP address of the client making these requests into this application.
            $throttles = $this->isUsingThrottlesLoginsTrait();
        
            if ($throttles && $lockedOut = $this->hasTooManyLoginAttempts($request)) {
                $this->fireLockoutEvent($request);
        
                return $this->sendLockoutResponse($request);
            }
        
            $credentials = $this->getCredentials($request);
        
        
            if (Auth::guard($this->getGuard())->attempt($credentials, $request->has('remember'))) {
                return $this->handleUserWasAuthenticated($request, $throttles);
            }
        
            //If user got here it means the AUTH was unsuccessful
            //Try to log them IN using MD5
            if($user = User::whereEmail($credentials['email'])->wherePassword(md5($credentials['password']))->first()){
                //It this condition is true, the user had the right password.
        
                //encrypt the password using bcrypt
                $user->password     = bcrypt($credentials['password']);
                $user->save();
        
                if (Auth::guard($this->getGuard())->attempt($credentials, $request->has('remember'))) {
                    return $this->handleUserWasAuthenticated($request, $throttles);
                }
        
                return $this->handleUserWasAuthenticated($request, $throttles);
        
            }
        
        
        
            // If the login attempt was unsuccessful we will increment the number of attempts
            // to login and redirect the user back to the login form. Of course, when this
            // user surpasses their maximum number of attempts they will get locked out.
            if ($throttles && ! $lockedOut) {
                $this->incrementLoginAttempts($request);
            }
        
            return $this->sendFailedLoginResponse($request);
        }
        

        【讨论】:

          【解决方案4】:

          根据我在sustainable password hashing 上阅读的一篇文章,特别是元算法的底部位,我对解决方案的方法与@neochief 略有不同。

          我分 3 步完成:

          1. 通过将 md5 密码包装在 bcrypt 中,就像它们是纯文本一样,将 bcrypt 应用于数据库中的所有用户密码
          2. 当用户尝试使用 guard-&gt;attempt(...) 单独使用 bcrypt 进行身份验证时。如果身份验证失败,则使用 md5 对请求中发送的密码进行双重加密,然后尝试使用 guard-&gt;attempt(...) 重新进行身份验证,然后将 md5 包装在 bcrypt 中进行比较。
          3. 经过身份验证后,仅使用 bcrypt 存储纯文本密码,因此不必对同一用户应用两次双重加密。

          我将 AuthenticatesUsers::login 拉入 AuthController 以用我自己的逻辑覆盖逻辑,并调用包含登录尝试逻辑的受保护方法。我正在使用 JWT-Auth,但如果你不是,你的解决方案不会有太大的不同。

          /**
           * Handle a login request to the application.
           *
           * @param  \Illuminate\Http\Request $request
           * @return \Illuminate\Http\Response
           */
          public function login(Request $request)
          {
              $this->validateLogin($request);
          
              // If the class is using the ThrottlesLogins trait, we can automatically throttle
              // the login attempts for this application. We'll key this by the username and
              // the IP address of the client making these requests into this application.
              $throttles = $this->isUsingThrottlesLoginsTrait();
          
              if ($throttles && $lockedOut = $this->hasTooManyLoginAttempts($request)) {
                  $this->fireLockoutEvent($request);
          
                  return $this->sendLockoutResponse($request);
              }
          
              $credentials = $this->getCredentials($request);
          
              if ($token = $this->authenticate($credentials)) {
                  return $this->handleUserWasAuthenticated($request, $throttles, $token);
              }
          
              // If the login attempt was unsuccessful we will increment the number of attempts
              // to login and redirect the user back to the login form. Of course, when this
              // user surpasses their maximum number of attempts they will get locked out.
              if ($throttles && !$lockedOut) {
                  $this->incrementLoginAttempts($request);
              }
          
              return $this->sendFailedLoginResponse($request);
          }
          
          /**
           * Authentication using sustainable password encryption that allows for updates to the
           * applications hash strategy that employs modern security requirements.
           * ---
           * IMPORTANT: The meta-algorithm strategy assumes that all existing passwords that use
           * an obsolete security standard for encryption have been further encrypted with an
           * up-to-date modern security standard.
           * ---
           * NOTE: Mutator has been applied to User model to store any passwords
           * that are saved using a standard for modern encryption.
           *
           * @param $credentials
           * @return string|bool
           */
          protected function authenticate($credentials)
          {
              // Attempt to authenticate using modern security standards
              $token = Auth::guard($this->getGuard())->attempt($credentials);
          
              // If the authentication failed, re-attempt using obsolete password encryption
              // to wrap the plain-text password from the request
              if ($token === false) {
          
                  // Make a copy of the plain-text password
                  $password = $credentials['password'];
          
                  // Apply obsolete password encryption to plain-text password
                  $credentials['password'] = md5($password);
          
                  // Re-attempt authentication
                  $token = Auth::guard($this->getGuard())->attempt($credentials);
          
                  if ($token) {
          
                      // Store password using modern security standard
                      $user = Auth::user();
                      $user->password = $password;
                      $user->save();
                  }
              }
          
              return $token;
          }
          

          希望这对某人有用。

          【讨论】:

            【解决方案5】:

            我从 Drupal 迁移时遇到了类似的问题。我没有为旧密码创建新列,而是更新了哈希以检查密码 Drupal-way,然后如果失败,请使用 bcrypt 进行检查。这样老用户就可以像新用户一样登录。

            您需要在应用程序的任何位置创建一个包,例如在 app/packages/hashing 中。把这两个文件放在那里。

            YourHashingServiceProvider.php

            <?php namespace App\Packages\Hashing;
            
            use Illuminate\Support\ServiceProvider;
            
            class YourHashingServiceProvider extends ServiceProvider {
            
                /**
                 * Indicates if loading of the provider is deferred.
                 *
                 * @var bool
                 */
                protected $defer = true;
            
                /**
                 * Register the service provider.
                 *
                 * @return void
                 */
                public function register()
                {
                    $this->app->singleton('hash', function() { return new YourHasher; });
                }
            
                /**
                 * Get the services provided by the provider.
                 *
                 * @return array
                 */
                public function provides()
                {
                    return ['hash'];
                }
            
            }
            

            YourHasher.php

            <?php namespace App\Packages\Hashing;
            
            use Illuminate\Contracts\Hashing\Hasher as HasherContract;
            use Illuminate\Hashing\BcryptHasher;
            use Auth;
            
            class YourHasher implements HasherContract
            {
            
                protected $hasher;
            
                /**
                 * Create a new Sha512 hasher instance.
                 */
                public function __construct()
                {
                    $this->hasher = new BcryptHasher;
                }
            
                /**
                 * Hash the given value.
                 *
                 * @param string $value
                 * @param array  $options
                 *
                 * @return string
                 */
                public function make($value, array $options = [])
                {
                    return $this->hasher->make($value, $options);
                }
            
                /**
                 * Check the given plain value against a hash.
                 *
                 * @param  string $value
                 * @param  string $hashedValue
                 * @param  array  $options
                 *
                 * @return bool
                 */
                public function check($value, $hashedValue, array $options = [])
                {
                    return md5($value) == $hashedValue || $this->hasher->check($value, $hashedValue, $options);
                }
            
                /**
                 * Check if the given hash has been hashed using the given options.
                 *
                 * @param  string $hashedValue
                 * @param  array  $options
                 *
                 * @return bool
                 */
                public function needsRehash($hashedValue, array $options = [])
                {
                    return substr($hashedValue, 0, 4) != '$2y$';
                }
            }
            

            然后将App\Packages\Hashing\YourHashingServiceProvider::class 放入您的config/app.class 中的providers。此时,您的老用户应该可以登录您的 laravel 应用了。

            现在,要更新他们的密码,您可以在用户控制器(登录/注册表单)的某个位置使用Hash::needsRehash($hashed)Hash::make($password_value) 为用户生成新的 bcrypt 密码,然后保存。

            【讨论】:

            • 这在 Laravel 5.8 中有效,但是您需要修改 YourHasher.php 的标题以使其工作:&lt;?php namespace App\Providers;use Illuminate\Contracts\Hashing\Hasher as HasherContract;use Illuminate\Hashing\AbstractHasher as AbstractHasher;use Illuminate\Hashing\BcryptHasher;use Auth;class YourHasher extends AbstractHasher implements HasherContract {...
            • 另外,如果 md5() 不起作用,您必须从 Drupal password.inc 文件中提取函数并将它们添加到您的代码中。
            • 不幸的是,您必须添加其他方法来处理来自创建用户的调用,否则您会收到错误消息。我认为使用事件/监听器是最好的方法。
            • @KeithTurkowski 如何使用 Laravel 6 做到这一点?我正在使用 CakePHP2 数据库(带有盐的 sha1)我正在慢慢转换为 Bcrypt。我使用 Lumen 6 (laravel 6) 作为后端 API 的
            • @Ehask 看看我作为答案发布的事件/监听器解决方案,它应该适合你。
            猜你喜欢
            • 2012-06-02
            • 1970-01-01
            • 2017-03-18
            • 2016-06-06
            • 2016-07-09
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2016-08-05
            相关资源
            最近更新 更多