Backends PHP

Laravel에서 SHA-2(SHA-256, SHA-512)를 구현하여 사용자 비밀번호 암호화 해싱함수로 사용하기

Laravel은 사용자 비밀번호를 암호화하기 위한 해싱함수로 Bcrypt와 Argon2를 제공합니다. 이 중에서 디폴트 함수인 Bcrypt는 현재까지 등장한 해시함수 중 가장 안전한 방식 중 하나로 알려져 있습니다. 때문에 대부분의 프로젝트에서는 Laravel의 기본 해싱함수를 변경할 필요가 없습니다.

그럼에도 여전히 암호화를 위해 SHA-2 방식을 채택해야 하는 상황이 있습니다. 특히 대한민국의 경우에는 공적기관의 보수적인 보안정책으로 인해 이러한 필요가 생기기도 합니다. 이 때는 Laravel의 기본 해싱함수를 직접 구현하여 변경해야 합니다.

이번 포스팅에서는 SHA-512 방식의 Hasher 클래스를 구현하고, 이를 Laravel에 바인딩하여 비밀번호를 암호화하기 위한 해싱함수로 사용하는 방법을 소개합니다. 또한 이를 구현하기 위해 관련 기술문서와 소스코드를 찾아들어가는 과정을 생략없이 다루어, 향후에 여러분이 자신만의 새로운 서비스를 Laravel에 추가할 때 단서가 될 수 있게 하려 합니다.


Laravel Hasher 클래스를 찾아가 살펴보기

Hasher 클래스를 직접 구현하여 사용하더라도, 그 형태는 Laravel에서 기본적으로 제공하는 Hasher 클래스들과 동일해야 합니다. Laravel의 내장 클래스의 형태를 살펴볼 수 있는 정석적인 방법은 바로 Laravel API 기술문서에서 원하는 내용을 검색하는 것입니다.

Laravel API 기술문서에서 “Hashing”으로 검색하면, Laravel에 내장된 해싱함수의 클래스인 BcryptHasher를 찾을 수 있습니다. 기술문서에서도 충분한 정보를 얻을 수 있지만, 해당 문서에서 View Source 링크를 클릭하여 소스코드를 살펴볼 수도 있습니다. 아래는 BcryptHasher 클래스 소스코드의 일부입니다.

namespace Illuminate\Hashing;

use Illuminate\Contracts\Hashing\Hasher as HasherContract;
use RuntimeException;

class BcryptHasher extends AbstractHasher implements HasherContract
{
    // 중략
}

BcryptHasher 클래스는 Illuminate\Contracts\Hashing\Hasher라는 인터페이스를 구현하고 있음을 볼 수 있습니다. 다시 한 번 Laravel API 기술문서에서 링크를 따라가 Hasher 인터페이스를 살펴보면, 이 인터페이스를 구현한 클래스는 info(), make(), check(), needsRehash()의 4개 메서드를 반드시 가져야 한다는 것을 발견하게 됩니다.

namespace Illuminate\Contracts\Hashing;

interface Hasher
{
    public function info($hashedValue);
    public function make($value, array $options = []);
    public function check($value, $hashedValue, array $options = []);
    public function needsRehash($hashedValue, array $options = []);
}

위 소스코드에는 주석은 제거했습니다만, 원본 소스에는 각 메서드들이 가진 역할들이 주석으로 작성되어 구현에 참고할 수 있습니다. 그러나 해당 내용은 Laravel API 기술문서에도 명시가 되어 있으므로, 기술문서만 잘 살펴보아도 충분한 정보를 얻을 수 있습니다.

이제 기존의 BcryptHasher 클래스와 위 인터페이스를 단서로 삼아 Hashing 인터페이스를 구현하면, 해싱함수 클래스를 커스터마이징할 수 있게 됩니다.

SHA-512 Hasher 함수의 구현

아래는 제가 구현한 SHA-512 방식의 해싱 클래스 Sha512Hasher의 소스코드입니다. 저는 SHA-2 방식 중 SHA-512를 구현하였지만, 아래 코드를 읽어보시면 SHA-256을 비롯한 다른 해시함수로 변경하시는 것도 어렵지 않음을 알 수 있습니다.

namespace App\Libraries\Hashing;

use Illuminate\Contracts\Hashing\Hasher as HasherContract;

class Sha512Hasher implements HasherContract
{
    protected $verifyAlgorithm = false;

    public function __construct(array $options = [])
    {
        $this->verifyAlgorithm = $options['verify'] ?? $this->verifyAlgorithm;
    }

    public function info($hashedValue)
    {
        return [
            'algo' => null,
            'algoName' => 'sha512',
            'options' => []
        ];
    }

    public function make($value, array $options = [])
    {
        return hash('sha512', $value);
    }

    public function check($value, $hashedValue, array $options = [])
    {
        if ($this->verifyAlgorithm && $this->info($hashedValue)['algoName'] !== 'sha512') {
            throw new RuntimeException('This password does not use the Sha512 algorithm.');
        }

        return self::make($value) === $hashedValue;
    }

    public function needsRehash($hashedValue, array $options = [])
    {
        return false;
    }
}

위 클래스는 Hashing 인터페이스에 담긴 4개의 메서드와 생성자 함수 __construct()를 구현하고 있습니다.

  • 먼저 생성자 함수는 간단하게 verify 값만 담았습니다. BcryptHasher나 ArgonHasher는 생성자 함수에서 해싱을 위한 여러 설정값을 받지만, 동일한 값에 대하여 언제나 동일한 해시값을 반환하는 SHA-2 방식에는 이런 설정이 필요 없습니다.

  • info() 메서드는 해시함수에 대한 정보를 담은 연관배열을 반환합니다. 반환하는 배열의 형태는 BcryptHasher와 ArgonHasher가 이 메서드에서 호출하는 password_get_info() 함수의 결과값과 동일하게 구현하였습니다.

  • make() 메서드는 실제 해시가 된 값을 반환합니다. 여기서는 PHP의 내장함수인 hash() 함수로 SHA-512 해싱값을 반환하게 하였습니다. 여러분이 SHA-256을 비롯한 다른 해싱함수로 클래스를 수정하기를 원하신다면, hash() 함수의 첫 번째 인자를 변경하시면 됩니다.

  • check() 메서드는 주어진 값과 해시된 값이 일치하는지 검증하는 로직을 담고 있습니다. BcryptHasher나 ArgonHasher는 여기에서 password_verify() 함수를 사용하여 해시값을 검증하지만, SHA-2 방식은 동일한 값에 대하여 언제나 동일한 해시값을 반환하기 때문에 단순히 두 값이 같은지만 비교하면 됩니다.

  • needsRehash() 메서드는 해당 해시값을 다시 해시할 필요가 있는지 확인하는 메서드입니다. BcryptHasher나 ArgonHasher는 옵션에 따라서 해시가 달라지기 때문에, 이 메서드에서 password_needs_rehash() 함수를 이용해서 해시값을 검사합니다. 그러나 SHA-2 방식은 어떤 값에 대한 해시값은 변하지 않기 때문에 언제든 다시 해시를 할 필요가 없으므로, 항상 false를 반환하게 하였습니다.

서비스 프로바이더의 작성

작성한 Hasher를 실제 Laravel 프레임워크에서 활용하기 위해서는 서비스 프로바이더을 작성하여, 해당 클래스를 바인딩하거나 드라이버로 추가할 필요가 있습니다. 금번 포스팅에서는 2가지 방법을 모두 소개합니다.

첫 번째로 소개할 방법은 Laravel 매뉴얼에 기재된 확장 바인딩을 사용하는 방법입니다. 이 방법을 사용하면 Laravel이 불러온 Hasher를 우리가 구현한 Sha512Hasher로 수정하게 됩니다. 이 방법은 기존의 Hahser를 Sha512Hasher로 덮어쓰는 것처럼 작동하며, 가장 간편하게 작성한 클래스를 Laravel에서 사용할 수 있는 방법입니다.

namespace App\Providers;

use Illuminate\Hashing\HashManager;
use Illuminate\Support\ServiceProvider;
use App\Libraries\Hashing\Sha512Hasher;

class ShaHashingServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->app->extend(HashManager::class, function () {
            return new Sha512Hasher();
        });
    }
}

두 번째는 HashManager에 extend() 메서드를 이용하여 Sha512Hasher를 새로운 드라이버로 추가하는 것입니다. Laravel이 불러온 HashManager 클래스의 인스턴스에는 $this->app[‘hash’]와 같이 접근할 수 있습니다.

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Libraries\Hashing\Sha512Hasher;

class ShaHashingServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->app['hash']->extend('sha512', function(){
            return new Sha512Hasher();
        });
    }
}

첫 번째 방법은 서비스 프로바이더를 작성하면 Sha512Hasher를 바로 사용할 수 있지만, 두 번째 방법은 드라이버만 추가했다는 점이 다릅니다. 이 방법으로 서비스 프로바이더를 작성한 후에는 아래 소스코드와 같이 config/hashing.php에서 driver 값을 변경해주어야 Hasher를 사용할 수 있습니다. 이 때 입력할 값은 extend() 메서드를 호출할 때 첫 번째로 입력했던 문자열입니다.

return [

    /*
    |--------------------------------------------------------------------------
    | Default Hash Driver
    |--------------------------------------------------------------------------
    |
    | This option controls the default hash driver that will be used to hash
    | passwords for your application. By default, the bcrypt algorithm is
    | used; however, you remain free to modify this option if you wish.
    |
    | Supported: "bcrypt", "argon", "argon2id"
    |
    */

    'driver' => 'sha512',
    // 이후 생략

마지막으로 자동로드(autoload) 서비스 프로바이더로 위에서 구현한 ShaHashingServiceProvider를 추가하면 모든 과정을 끝마치게 됩니다. /config/app.php 파일에서 반환하는 연관배열에 담긴 provider 배열에 새로 작성한 서비스 프로바이더를 추가합니다.


return [
    // 이전 생략

    /*
    |--------------------------------------------------------------------------
    | Autoloaded Service Providers
    |--------------------------------------------------------------------------
    |
    | The service providers listed here will be automatically loaded on the
    | request to your application. Feel free to add your own services to
    | this array to grant expanded functionality to your applications.
    |
    */

    'providers' => [
        // 이전 생략        
        App\Providers\ShaHashingServiceProvider::class
    ],
    // 이후 생략

여기까지 완수하였다면 이제 bcrypt나 argon2 대신 SHA-512로 비밀번호가 암호화되는 것을 확인할 수 있습니다. 더불어 이 긴 여정을 통해서, Laravel의 내장 클래스들을 탐구하는 방법, 내장 클래스를 대체할 사용자 정의 클래스를 생성하는 과정, 이것을 서비스 프로바이더에서 바인딩하는 절차 등을 엿볼 수 있었을 것입니다.

예제 소스코드

금번에 소개한 소스코드는 깃허브 저장소에 업로드 되어 있으며, hasher 브랜치의 623aa86 커밋에 해당하는 작업 내용이 담겨 있습니다. 구현은 위 2가지 방법 중 두 번째 방법인 드라이버를 추가하는 방법으로 이루어졌습니다.

Leave a Reply

Your email address will not be published. Required fields are marked *