Статьи

Перенос кодировщика паролей с помощью Spring Security 5

Недавно я работал в проекте, который использовал пользовательский PasswordEncoder и было требование перенести его в bcrypt . Текущие пароли хранятся в виде hash что означает, что невозможно вернуть его к исходной String — по крайней мере, не простым способом.

Задача состояла в том, чтобы поддержать обе реализации, старое хеш-решение и новую реализацию bcrypt . После небольшого исследования я смог найти DelegatingPasswordEncoder Spring Security 5 .

Познакомьтесь с DelegatingPasswordEncoder

Класс DelegatingPasswordEncoder позволяет поддерживать несколько password encoders на основе префикса . Пароль хранится так:

1
2
{bcrypt}$2a$10$vCXMWCn7fDZWOcLnIEhmK.74dvK1Eh8ae2WrWlhr2ETPLoxQctN4.
{noop}plaintextpassword

Spring Security 5 предоставляет удобный класс PasswordEncoderFactories , в настоящее время этот класс поддерживает следующие кодировщики:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public static PasswordEncoder createDelegatingPasswordEncoder() {
    String encodingId = "bcrypt";
    Map<String, PasswordEncoder> encoders = new HashMap<>();
    encoders.put(encodingId, new BCryptPasswordEncoder());
    encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
    encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
    encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
    encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
    encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
    encoders.put("scrypt", new SCryptPasswordEncoder());
    encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
    encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
    encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
 
    return new DelegatingPasswordEncoder(encodingId, encoders);
}

Теперь вместо объявления единого PasswordEncoder мы можем использовать PasswordEncoderFactories , например, такой фрагмент кода:

1
2
3
4
@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

Добавление пользовательского кодировщика

Теперь, возвращаясь к моей первоначальной проблеме, по старым причинам есть собственное решение для password encoding , и удобный PasswordEncoderFactories ничего не знает об этом, чтобы решить, что я создал класс, похожий на PasswordEncoderFactories и добавил все встроенные -в кодировщиках вместе с моим, вот пример реализации:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
 
import java.util.HashMap;
import java.util.Map;
 
class DefaultPasswordEncoderFactories {
 
    @SuppressWarnings("deprecation")
    static PasswordEncoder createDelegatingPasswordEncoder() {
        String encodingId = "bcrypt";
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put(encodingId, new BCryptPasswordEncoder());
        encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
        encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
        encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
        encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
        encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
        encoders.put("scrypt", new SCryptPasswordEncoder());
        encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
        encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
        encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
        encoders.put("custom", new CustomPasswordEncoder());
 
        return new DelegatingPasswordEncoder(encodingId, encoders);
    }
}

А затем я объявил свой @Bean используя вместо него DefaultPasswordEncoderFactories .

После первого запуска я понял другую проблему, мне нужно было запустить скрипт SQL чтобы обновить все существующие пароли, добавив префикс {custom} чтобы инфраструктура могла правильно связать префикс с правильным PasswordEncoder , не поймите меня неправильно, это прекрасное решение, но я действительно не хотел возиться с существующими паролями в базе данных, и, к счастью для нас, класс DelegatingPasswordEncoder позволяет нам устанавливать PasswordEncoder умолчанию , это означает, что всякий раз, когда платформа пытается найти префикс в сохраненном пароле, откат к default чтобы попытаться декодировать его.

Затем я изменил свою реализацию на следующее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
 
import java.util.HashMap;
import java.util.Map;
 
class DefaultPasswordEncoderFactories {
 
    @SuppressWarnings("deprecation")
    static PasswordEncoder createDelegatingPasswordEncoder() {
        String encodingId = "bcrypt";
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put(encodingId, new BCryptPasswordEncoder());
        encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
        encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
        encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
        encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
        encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
        encoders.put("scrypt", new SCryptPasswordEncoder());
        encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
        encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
 
        DelegatingPasswordEncoder delegatingPasswordEncoder = new DelegatingPasswordEncoder(encodingId, encoders);
        delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(new CustomPasswordEncoder());
        return delegatingPasswordEncoder;
    }
 
}

И объявление @Bean теперь:

1
2
3
4
@Bean
public PasswordEncoder passwordEncoder() {
    return DefaultPasswordEncoderFactories.createDelegatingPasswordEncoder();
}

Вывод

Кодировщики паролей миграции — это реальная проблема, и Spring Security 5 предоставляет довольно удобный способ легко справиться с ней, поддерживая несколько PasswordEncoder одновременно.

сноска

Опубликовано на Java Code Geeks с разрешения Маркоса Барберо, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Перенос кодировщика паролей с помощью Spring Security 5

Мнения, высказанные участниками Java Code Geeks, являются их собственными.