Программирование
Как правильно хранить пароли в базе данных
В этой статье я объясню, как хранить пароли просто и правильно.
Сохранение паролей пользователей в базе данных — обычное дело, но не каждый разработчик делает это правильно. Например, в сегодняшнем code review я обнаружил, что пароли хранятся в виде обычного текста. Когда я спросил разработчика, в чем опасность такого сохранения, он быстро осознал проблему и поинтересовался, было бы решением их зашифровать. Я ответил: «Нет».
В этой статье я объясню, как хранить пароли правильно и просто. Давайте начнем.
Сохранение паролей
Хранить пароль в виде обычного текста — ужасная идея, но хранить его в зашифрованном виде с использованием двустороннего алгоритма также плохо.
Почему? Потому что, если мы зашифруем и сохраним пароль с помощью ключа шифрования и алгоритма, который позволяет нам зашифровать пароль и расшифровать его с помощью этого ключа, любой, у кого есть ключ шифрования, сможет его расшифровать.
Итак, как правильно это сделать?
В большинстве случаев правильным решением является сохранение хеша пароля в базе данных.
Для этого вы должны хешировать пароль, используя различную соль для каждого пользователя, используя односторонний алгоритм, и сохранить результат, удалив исходный пароль. Затем, когда вы хотите проверить пароль, вы снова хешируете пароль из обычного текста, используя тот же алгоритм хеширования и ту же соль, и сравниваете его с хешированным сохраненным значением в базе данных.
Хорошо, но что вы имеете в виду, говоря «используя разные соли для каждого пользователя»?
Соль — это случайные данные, используемые в качестве дополнительных входных данных для одностороннего алгоритма хеширования. Исторически в системе хранилась только криптографическая хеш-функция пароля, но со временем были разработаны дополнительные меры безопасности для защиты от идентифицируемых общих или повторяющихся паролей.
Давайте посмотрим на практику.
Представим, что у нас есть таблица в базе данных для хранения пользователей моего приложения. В этой таблице есть следующие поля, которые мы будем использовать для хранения имени пользователя и пароля:
CREATE TABLE USERS | |
( | |
ID SERIAL NOT NULL CONSTRAINT USERS_PKEY PRIMARY KEY, | |
HASHED_PASSWORD VARCHAR(255) NOT NULL, | |
USERNAME VARCHAR(255) NOT NULL CONSTRAINT USERS_USERNAME_KEY UNIQUE, | |
SALT VARCHAR(1024) | |
); |
Следующим шагом является создание уникальной соли для каждого пользователя каждый раз, когда мы создаем пользователя в этой таблице. Для этого я определю класс с методом, который будет генерировать соль. Выполнение этого с использованием любого другого языка программирования аналогично.
package com.bunt.app.utils; | |
import ... | |
public class PasswordUtils { | |
public static final int KEY_LENGTH = 512; | |
private static final SecureRandom RAND = new SecureRandom(); | |
public static Optional<String> generateSalt(final int length) { | |
byte[] salt = new byte[length]; | |
RAND.nextBytes(salt); | |
return Optional.of(Base64.getEncoder().encodeToString(salt)); | |
} | |
} |
- В строке 7 мы определяем длину соли.
- В строке 11 мы создаем экземпляр класса SecureRandom. Этот класс предоставляет криптографически стойкий генератор случайных чисел, предоставляемый JDK (для других языков у нас есть аналогичные библиотеки).
- В строке 16 мы создаем значение соли, а затем возвращаем строку Base64.
Когда у нас есть соль для пользователя, следующим шагом будет хеширование пароля в виде обычного текста с использованием этой соли. Для этого мы собираемся добавить в наш класс метод под названием hashThePlainTextPassword:
public static final int ITERATIONS = 1000; | |
public static final int KEY_LENGTH = 512; | |
private static final String ALGORITHM = "PBKDF2WithHmacSHA512"; | |
public static Optional<String> hashThePlainTextPassword(String plainTextPassword, String salt) { | |
char[] chars = plainTextPassword.toCharArray(); | |
byte[] bytes = salt.getBytes(); | |
PBEKeySpec spec = new PBEKeySpec(chars, bytes, ITERATIONS, KEY_LENGTH); | |
Arrays.fill(chars, Character.MIN_VALUE); | |
try { | |
SecretKeyFactory fac = SecretKeyFactory.getInstance(ALGORITHM); | |
byte[] securePassword = fac.generateSecret(spec).getEncoded(); | |
return Optional.of(Base64.getEncoder().encodeToString(securePassword)); | |
} catch (NoSuchAlgorithmException | InvalidKeySpecException ex) { | |
//Handle the exception | |
return Optional.empty(); | |
} finally { | |
spec.clearPassword(); | |
} | |
} |
- В строке 10 мы указываем итерации и длину PBEKey.
- В строке 13 мы получаем экземпляр SecretKeyFactory.
- В строке 14 мы генерируем секретный ключ.
- В строке 15 мы возвращаем хешированный пароль в строке Base64.
После того, как мы добавили соль к нашему паролю, мы можем сохранить его хеш в таблице пользователя вместе с солью. Обратите внимание, что при использовании одностороннего алгоритма для декодирования пароля мы никогда не узнаем исходное значение пароля в виде обычного текста. Таким образом, даже если вы получите соль пользователя, если вы не знаете исходный пароль в виде открытого текста, вы никогда не сгенерируете тот же хеш.
Итак, как я могу узнать, правильный ли пароль?
Очень просто. Если мы хотим проверить пароль, мы снова хэшируем значение (используя тот же алгоритм хеширования и соль) и сравниваем его с хешированным значением пользователя в базе данных.
Для этого мы собираемся добавить в наш класс метод под названием verifyThePlainTextPassword. Этот метод хеширует открытый текстовый пароль с исходным соленым значением. Теперь, если сохраненное значение в базе данных и сгенерированное хеш-значение идентичны, значит пароль правильный.
public static boolean verifyThePlainTextPassword( | |
String plainTextPassword, | |
String hashedPassword, | |
String salt) { | |
Optional<String> optEncrypted = hashThePlainTextPassword(plainTextPassword, salt); | |
if (!optEncrypted.isPresent()) { | |
return false; | |
} | |
return optEncrypted.get().equals(hashedPassword); | |
} |
Все вместе
import javax.crypto.SecretKeyFactory; | |
import javax.crypto.spec.PBEKeySpec; | |
import java.security.NoSuchAlgorithmException; | |
import java.security.SecureRandom; | |
import java.security.spec.InvalidKeySpecException; | |
import java.util.Arrays; | |
import java.util.Base64; | |
import java.util.Optional; | |
public class PasswordUtils { | |
public static final int ITERATIONS = 1000; | |
public static final int KEY_LENGTH = 512; | |
private static final String ALGORITHM = "PBKDF2WithHmacSHA512"; | |
private static final SecureRandom RAND = new SecureRandom(); | |
public static Optional<String> generateSalt(final int length) { | |
byte[] salt = new byte[length]; | |
RAND.nextBytes(salt); | |
return Optional.of(Base64.getEncoder().encodeToString(salt)); | |
} | |
public static Optional<String> hashThePlainTextPassword( | |
String plainTextPassword, | |
String salt) { | |
char[] chars = plainTextPassword.toCharArray(); | |
byte[] bytes = salt.getBytes(); | |
PBEKeySpec spec = new PBEKeySpec(chars, bytes, ITERATIONS, KEY_LENGTH); | |
Arrays.fill(chars, Character.MIN_VALUE); | |
try { | |
SecretKeyFactory fac = SecretKeyFactory.getInstance(ALGORITHM); | |
byte[] securePassword = fac.generateSecret(spec).getEncoded(); | |
return Optional.of(Base64.getEncoder().encodeToString(securePassword)); | |
} catch (NoSuchAlgorithmException | InvalidKeySpecException ex) { | |
//Handle the exception | |
return Optional.empty(); | |
} finally { | |
spec.clearPassword(); | |
} | |
} | |
public static boolean verifyThePlainTextPassword( | |
String plainTextPassword, | |
String hashedPassword, | |
String salt) { | |
Optional<String> optEncrypted = hashThePlainTextPassword(plainTextPassword, salt); | |
if (!optEncrypted.isPresent()) { | |
return false; | |
} | |
return optEncrypted.get().equals(hashedPassword); | |
} | |
} |
Вывод
Как видите, правильно сохранить пароли в базе данных просто, а правильное их хранение позволит избежать многих проблем.
Надеюсь, эта статья оказалась для вас полезной.
-
Программирование4 недели назад
Конец программирования в том виде, в котором мы его знаем
-
Видео и подкасты для разработчиков7 дней назад
Как устроена мобильная архитектура. Интервью с тех. лидером юнита «Mobile Architecture» из AvitoTech
-
Магазины приложений3 недели назад
Магазин игр Aptoide запустился на iOS в Европе
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.8