• Czas czytania ~7 min
  • 04.12.2022

Generowanie haseł to coś, o czym wszyscy myślimy w pewnym momencie, ale jak możemy to zrobić - oraz sprawić, by te hasła były łatwe do zapamiętania i bezpieczne?

W tym samouczku przeprowadzę Cię przez to, jak niedawno zbudowałem generator haseł dla zabawy i jakie były moje przemyślenia. Stworzyłem go nie tylko dla Laravela, ale jako podejście niezależne od frameworka, które mogłem łatwo wykorzystać w Laravelu.

Podczas korzystania z narzędzia do tworzenia haseł często otrzymujesz wybór słów połączonych myślnikiem. Chciałem stworzyć coś, czego mógłbym używać i zapamiętywać ze względną łatwością, ale z opcją uczynienia haseł bardziej „bezpiecznymi” poprzez zamianę liter na cyfry w miejscach.

Włączmy nasz kod!

Najpierw chcemy zdefiniować standardowy interfejs, który nasz generator będzie chciał zaimplementować - pozwoli nam to używać go w Laravel, poza Laravel i wszędzie tam, gdzie chcemy go używać.

interface GeneratorContract
{
    /**
     * @return string
     */
    public function generate(): string;
 
    /**
     * @return string
     */
    public function generateSecure(): string;
}

Chcemy, aby nasza implementacja miała dwa możliwe podejścia, generowanie losowego hasła przy użyciu zapadających w pamięć słów - oraz drugą opcję generowania tego samego, co może być postrzegane jako bezpieczniejsze. Doskonałym tego przykładem byłoby:

latająca-ryba-pływająca-jaszczurka - niezapomniane
fly1ng-f1sh-sw1mm1ng-l1z4rd - "bezpieczne"

Aby to osiągnąć, będziemy musieli stworzyć sposób generowania słów. Będziemy potrzebować Rzeczowniki and Przymiotniki, co oznacza, że ​​musimy utworzyć kolejną umowę. Ta umowa będzie sposobem na pobranie losowego słowa z zarejestrowanej listy albo w standardowym podejściu, albo w naszym wymaganym secure zbliżać się.

interface ListContract
{
    public function random(): string;
 
    public function secure(): string;
}

Obie metody w tej umowie dadzą mniej więcej to samo, jedyną różnicą jest to, że w przypadku jednej z nich wykonamy trochę więcej przetwarzania przed powrotem. Zagłębmy się w kod, aby zobaczyć, jak to może działać.

Wiemy, że potrzebujemy listy rzeczowników i listy przymiotników, i oba zrobią to samo, jeśli chodzi o zachowanie. W przyszłości możemy to nieco zmienić, aby uwzględniać również czasowniki, ale na razie skupimy się na rzeczownikach i przymiotnikach. Aby to osiągnąć, użyję cechy PHP, aby dodać wspólne zachowanie do każdej implementacji.

trait HasWords
{
    /**
     * @param array $words
     */
    public function __construct(
        private readonly array $words,
    ) {
    }
 
    public function random(): string
    {
        return $this->words[array_rand($this->words)];
    }
}

Na początek tworzymy cechę, gdzie wiemy, że nasza implementacja będzie chciała być tworzona z tablicą słów. Następnie dodajemy metodę wybierania losowego elementu z tej tablicy. To pozwoli nam zbudować nasze standardowe łatwe do zapamiętania hasło, wywołując je kilka razy i łącząc wyniki. Dodajmy to do dwóch implementacji, które musimy utworzyć.

final class AdjectiveList implements ListContract
{
    use HasWords;
}
 
 
final class NounList implements ListContract
{
    use HasWords;
}

Nasze dwa wdrożenia pozwolą nam wygenerować połowę tego, czego potrzebujemy. Musimy zastanowić się, w jaki sposób chcemy następnie stworzyć nasze „bezpieczne” hasła. Wróćmy do cechy, którą stworzyliśmy.

trait HasWords
{
    /**
     * @param array $words
     */
    public function __construct(
        private readonly array $words,
    ) {
    }
 
    public function random(): string
    {
        return $this->words[array_rand($this->words)];
    }
 
    public function secure(): string
    {
        $word = $this->random();
 
        $asArray = str_split($word);
 
        $secureArray = array_map(
            callback: fn (string $item): string => $this->convertToNumerical($item),
            array: $asArray,
        );
 
        return implode('', $secureArray);
    }
 
    public function convertToNumerical(string $item): string
    {
        return match ($item) {
            'a' => '4',
            'e' => '3',
            'i' => '1',
            'o' => '0',
            default => $item,
        };
    }
}

Jak to działa? Jako „bezpieczne” hasło nadal chcemy pobrać losowe słowo z naszej listy, ale musimy wykonać dodatkowe przetwarzanie. Podzieliliśmy słowo na podstawowe litery, aby uzyskać tablicę. Następnie możemy zmapować każdą literę - i wywołać naszą metodę konwersji, aby zaktualizować dane wejściowe. Nasza metoda konwersji to prosta instrukcja dopasowania, której możemy użyć do odwzorowania danych wejściowych na dane wyjściowe, więc kontrolujemy sposób ich zapisywania. Zmapowałem je tutaj do typowego podejścia, w którym reprezentacja liczbowa wygląda jak reprezentacja literowa - pozwalając nam nadal mieć niezapomniane hasło.

Nasza kontrolka listy działa teraz i możemy utworzyć listę rzeczowników i przymiotników - które są wstrzykiwane do konstruktorów. Możemy pobrać słowo bezpośrednio lub przekazać je przez konwerter, aby było bardziej „bezpieczne”. Następnym krokiem jest przyjrzenie się implementacji naszego generatora. Jak dotąd mamy tylko implementacje dla samych list. Spójrzmy na trochę więcej kodu.

final class PasswordGenerator implements GeneratorContract
{
    public function __construct(
        private readonly ListContract $nouns,
        private readonly ListContract $adjectives,
    ) {
    }
 
    public function generate(): string
    {
        return $this->build(
            $this->nouns->random(),
            $this->adjectives->random(),
            $this->nouns->random(),
            $this->adjectives->random(),
        );
    }
 
    public function generateSecure(): string
    {
        return $this->build(
            $this->nouns->secure(),
            $this->adjectives->secure(),
            $this->nouns->secure(),
            $this->adjectives->secure(),
        );
    }
 
    private function build(string ...$parts): string
    {
        return implode('-', $parts);
    }
}

Nasz generator haseł przenosi dwie implementacje list do swojego konstruktora, abyśmy mogli uzyskać do nich dostęp. Następnie wdrażamy dwie metody generowania haseł: niezapomnianą i bezpieczną.

For each approach, we call a method internally that will allow us to budować the password by imploding the array with a joining string. If we want to extend this to more words, we must pass in more random words.

Nasza umowa nie musi wiedzieć o build metoda, ponieważ jest to szczegół implementacji. Moglibyśmy rozszerzyć tę metodę, aby dostosować postać łączącą - ale pozostawię to twojej implementacji, jeśli chcesz.

Jako dodatkowy dodatek utworzymy teraz dostawcę usług i fasadę, abyś mógł to szybko wywołać lub wstrzyknąć do kodu aplikacji. Ta część to jednak tylko Laravel. Do tej pory koncentrowaliśmy się na kodzie niezależnym od frameworka.

final class PackageServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(
            abstract: GeneratorContract::class,
            concrete: fn (): GeneratorContract => new PasswordGenerator(
                nouns: new NounList(
                    words: (array) config('password-generator.nouns'),
                ),
                adjectives: new AdjectiveList(
                    words: (array) config('password-generator.adjectives'),
                ),
            ),
        );
    }
 
    public function boot(): void
    {
        if ($this->app->runningInConsole()) {
            $this->publishes(
                paths: [
                    __DIR__ . '/../../config/password-generator.php',
                    config_path('password-generator.php'),
                ],
                groups: 'password-generator-config',
            );
        }
    }
}

Tutaj ładujemy naszą konfigurację, która będzie zawierać rzeczowniki i przymiotniki, których chcemy użyć w naszym generatorze haseł. Dostarczamy również Laravelowi zestaw instrukcji, w jaki sposób chcemy załadować naszą implementację z kontenera.

Teraz fasada będzie działać w ten sam sposób, ale rozwiąże implementację z kontenera, umożliwiając statyczne wywoływanie metod, które chcesz wywołać.

/**
 * @method static string generate()
 * @method static string generateSecure()
 *
 * @see GeneratorContract
 */
final class Generator extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return GeneratorContract::class;
    }
}

Nasza fasada zwraca kontrakt, którego użyliśmy do powiązania implementacji, i zawiera bloki doc, określające, jakie metody są dostępne i jakich typów zwrotów oczekuje.

Wszystko, co musimy teraz zrobić, to zadzwonić do generatora haseł i możemy zapewnić naszym użytkownikom losowo generowane hasła.

Generator::generate(); // flying-fish-swimming-lizard
Generator::generateSecure(); // fly1ng-f1sh-sw1mm1ng-l1z4rd

Stworzyłem pakiet, który dostarcza powyższy kod, który zawiera trochę więcej kodu, jeśli chcesz trochę bardziej zagłębić się w przykład. Możesz znaleźć to repozytorium tutaj: https://github.com/JustSteveKing/password-generator

Zastrzeżenie. To nie jest przeznaczone do użytku w środowisku produkcyjnym do tworzenia haseł. Moim przypadkiem użycia jest generowanie jednorazowych kodów, takich jak kody jednorazowego użytku. Nie jest to najbezpieczniejsze, ponieważ lista słów jest dość mała i naraża Cię na potencjalny atak słownikowy.

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

O

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

O autorze CrazyBoy49z
WORK EXPERIENCE
Kontakt
Ukraine, Lutsk
+380979856297