웹사이트를 만들 때 까다로운 부분 중 하나는 회원관리 기능을 만드는 작업입니다. 데이터베이스 테이블(또는 컬렉션)을 설계하고, 프로그래밍해야 하는데, 안정적인지 보안성이 높은지 고려해야 하므로 귀찮은 작업임에는 분명합니다.
회원관리 기능을 구현하는 것도 중요하지만 회원 데이터를 어떻게 저장할 것인지도 중요합니다. 특히 개인정보 보호를 위해 보안성이 좋아야 하는 것은 당연합니다. 이 글은 회원 정보를 안정적으로 저장하기 위한 데이터베이스 테이블 생성 방법과 보안성을 높이는 방법을 정리하였습니다.
테이블(컬렉션) 만들기
MySQL에서 보안성이 좋은 회원관리 데이터베이스 테이블을 설계하려면 다음과 같이 하면 됩니다. 먼저, 회원 정보를 저장할 테이블을 만듭니다. 예를 들어, “members” 테이블을 만들 수 있습니다.
CREATE TABLE members ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY (username), UNIQUE KEY (email) ) ENGINE=InnoDB;
- id: 회원 식별자 (INT)
- username: 회원 이름 (VARCHAR)
- password: 암호화된 비밀번호 (VARCHAR)
- email: 회원 이메일 (VARCHAR)
- created_at: 회원 가입일 (TIMESTAMP)
- updated_at: 회원 정보 업데이트 일자 (TIMESTAMP)
username과 email 컬럼에 UNIQUE KEY 제약 조건을 걸어 중복 회원 가입을 방지합니다. 또한, password 컬럼은 암호화하여 저장해야 하며, MySQL에서는 PASSWORD() 함수를 사용하여 암호화할 수 있습니다.
이외에도 회원 정보에 따라 추가적인 컬럼을 생성할 수 있습니다. 필요에 따라 외래키를 설정하여 관계형 데이터베이스를 구성할 수도 있습니다. 또한, 애플리케이션 수준에서는 입력값 검증, SQL 인젝션 등의 보안 이슈에 대해 고려해야 합니다.
참고: MongoDB 컬렉션 만들기
보통 회원관리는 MySQL 같은 관계형 데이터베이스로 하는 것이 일반적이지만 혹시 MongoDB로 회원관리 기능을 개발하는 분들이 있을 수도 있으니, MongoDB도 잠깐 언급해보겠습니다.
db.createCollection("users", { validator: { $jsonSchema: { bsonType: "object", required: [ "username", "password", "email" ], properties: { username: { bsonType: "string", description: "must be a string and is required" }, password: { bsonType: "string", description: "must be a string and is required" }, email: { bsonType: "string", pattern: "^\\S+@\\S+$", description: "must be a string and match the regular expression pattern" }, created_at: { bsonType: "date", description: "must be a date and is required" }, updated_at: { bsonType: "date", description: "must be a date and is required" } } } }, validationAction: "error" })
이외에도, 회원 정보에 따라 추가적인 필드를 생성하고, 필요에 따라 인덱스를 설정하여 검색 속도를 개선할 수 있습니다.
MongoDB에서는 기본적으로 SSL을 지원하며, 접근 제어를 위한 인증 및 권한 부여를 위한 기능도 제공합니다. 따라서, MongoDB를 사용하면 자체 기능을 활용하여 보안성 높은 회원관리 시스템을 구축할 수 있습니다.
패스워드 암호화
기본 회원정보와 패스워드를 분리하여 패스워드만 따로 데이터베이스 테이블을 만들어 관리할 수도 있습니다. 그러나, 패스워드만 따로 테이블로 관리하는 것은 일반적으로는 권장되지 않습니다. 패스워드를 따로 테이블로 분리하는 경우, 악의적인 공격자가 접근했을 때 패스워드를 얻기 위한 공격이 더욱 쉬워지기 때문입니다.
패스워드는 members 테이블과 같은 회원 정보를 저장하는 테이블에 함께 저장해야 하며, 패스워드는 반드시 암호화(해싱)하여 저장합니다. 이를 위해, MySQL에서는 PASSWORD() 함수를 사용하여 암호화할 수 있습니다. 또한, 보안 강화를 위해 salt와 같은 추가적인 보안 요소를 사용하여 패스워드를 보호할 수 있습니다.
예를 들어, 아래와 같이 패스워드를 암호화하여 저장할 수 있습니다.
CREATE TABLE members ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password_hash VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY (username), UNIQUE KEY (email) ) ENGINE=InnoDB;
위 예시에서 password_hash 컬럼에 암호화된 패스워드를 저장하도록 했습니다. 이렇게 함으로써 악의적인 공격자가 데이터베이스에 접근하여 패스워드를 얻는 것을 어렵게 만들 수 있습니다. 이 컬럼에 패스워드를 저장할 때는 MySQL 또는 기타 보안기술을 활용하여 스트링을 생성하여 저장합니다.
암호화 방법으로는 해시 함수 중에서 가장 보편적으로 사용되는 Bcrypt나 Scrypt 등이 있습니다. Bcrypt와 Scrypt는 모두 안정적인 해시 함수로 알려져 있으며, 보안 강도를 조정할 수 있는 장점이 있습니다. 이는 해싱 비용을 증가시켜 무차별 대입 공격을 방지하기 위한 것입니다.
Bcrypt는 Blowfish 알고리즘을 기반으로 하며, 높은 성능과 보안성을 자랑하며, 많은 웹 프레임워크에서 사용하고 있습니다. 개발자가 해시 비용(cost factor)을 설정할 수 있으며, 이 값은 공격자가 패스워드를 추측하는 데 필요한 작업량을 결정합니다.
Scrypt는 Bcrypt와 비슷하지만, 메모리 집약적이라는 점에서 더 강력한 보안성을 제공합니다. 이는 공격자가 해시를 빠르게 계산하는 것을 방지하기 위한 것입니다. Scrypt도 개발자가 비용(cost factor)을 설정할 수 있으며, 높은 비용은 보안성을 더 강화하지만, 계산 비용이 더 많이 들어갑니다.
Bcrypt나 Scrypt와 같은 안정적인 해시 함수를 사용하여 패스워드를 암호화하는 것이 일반적으로 권장되는 방법입니다. 하지만, 보안에 대한 우려가 있거나 더 나은 암호화 방법을 사용하고 싶다면, 해시 함수 외에도 대칭키 암호화(AES 등)와 비대칭키 암호화(RSA 등) 등 다른 방법을 고려할 수도 있습니다.
PHP에서 Bcrypt 암호화하는 방법
PHP에서 Bcrypt 암호화를 사용하는 방법은 다음과 같습니다. 참고로 이 암호화 기술은 PHP 버전 5.5.0 이상에서만 사용할 수 있습니다. 사실 구형 버전이기 때문에 크게 지장이 있을 것 같지는 않습니다.
PHP에서 Bcrypt 암호화를 사용하려면, password_hash() 함수를 사용해야 합니다. 이 함수는 입력받은 패스워드를 암호화하고, Bcrypt 해시값을 반환합니다. password_hash() 함수는 다음과 같이 사용할 수 있습니다.
$password = "mypassword"; $hashed_password = password_hash($password, PASSWORD_DEFAULT);
위 예시에서 $password 변수에 암호화할 패스워드를 저장하고, $hashed_password 변수에 password_hash() 함수를 사용하여 암호화된 패스워드를 저장합니다.
PASSWORD_DEFAULT 상수는 Bcrypt 암호화 알고리즘을 나타냅니다. 이외에도 PASSWORD_BCRYPT 상수를 사용해도 Bcrypt 알고리즘을 지정할 수 있습니다.
PHP에서 Bcrypt로 암호화된 패스워드를 검증하려면, password_verify() 함수를 사용해야 합니다. 이 함수는 입력받은 패스워드와 암호화된 패스워드를 비교하고, 일치하면 true 값을 반환합니다. password_verify() 함수는 다음과 같이 사용할 수 있습니다.
해싱된 텍스트는 원본으로 되돌릴 수 없습니다. 즉, 암호화된 텍스트는 복호화할 수 없습니다. 따라서 패스워드의 일치 여부를 확인할 때는 저장된 해싱 텍스트와 입력한 텍스트를 해싱한 값의 일치 여부만을 비교할 수 있을 뿐입니다. 데이터베이스에 저장된 해싱 텍스트를 본래 패스워드로 복호화하는 것은 불가능합니다. 웹사이트에서 패스워드 초기화 기능은 제공하지만, 패스워드 찾기 기능은 지원하지 않는 이유입니다.
$password = "mypassword"; $hashed_password = "$2y$10$JpHlS82fANaUcmJGVLAddeX4Z/N4aw1fQgJcTxjSOHD/sxndy/BqO"; // 암호화된 패스워드 if (password_verify($password, $hashed_password)) { echo "패스워드 일치"; } else { echo "패스워드 불일치"; }
위 예시에서, $password 변수에 검증할 패스워드를 저장하고, $hashed_password 변수에는 미리 암호화된 패스워드를 저장합니다. password_verify() 함수를 사용하여 입력받은 패스워드와 암호화된 패스워드를 비교하고, 일치하면 “패스워드 일치”라는 메시지를 출력합니다.
위와 같이 PHP에서 Bcrypt 암호화를 사용하는 방법은 간단합니다. 그러나 더 강력한 보안성을 위해 패스워드 암호화 과정에서 salt를 사용하거나, password_hash() 함수에서 제공하는 옵션을 사용하여 보안성을 높이는 것이 좋습니다. 또한, 패스워드 검증 시 password_verify() 함수를 사용하여 입력값 검증을 해야 하며, SQL 인젝션 등의 보안 이슈에 대해서도 항상 유의해야 합니다.
PHP에서 Scrypt 암호화 사용 방법
PHP에서 Scrypt 암호화를 사용하는 방법은 다음과 같습니다. 이 암호화 기술은 PHP 버전 7.2.0 이상부터 사용할 수 있습니다. 그러니 버전을 확인해주시기 바랍니다. PHP 버전 확인 방법은 다음과 같습니다.
echo phpversion();
PHP에서 Scrypt 암호화를 사용하려면, sodium_crypto_pwhash_str() 함수를 사용해야 합니다. 이 함수는 입력받은 패스워드를 암호화하고, Scrypt 해시값을 반환합니다. sodium_crypto_pwhash_str() 함수는 다음과 같이 사용할 수 있습니다.
$password = "mypassword"; $hashed_password = sodium_crypto_pwhash_str($password, SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE);
위 예시에서, $password 변수에 암호화할 패스워드를 저장하고, sodium_crypto_pwhash_str() 함수를 사용하여 암호화된 패스워드를 저장합니다.
SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE와 SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE 상수는 Scrypt 알고리즘의 비용(cost factor)을 나타냅니다. 이 값들은 공격자가 패스워드를 추측하는 데 필요한 작업량을 결정합니다. 더 높은 비용은 더 강력한 보안성을 제공하지만, 계산하는데 시간이 많이 소요됩니다.
PHP에서 Scrypt 암호화된 패스워드를 검증하려면, sodium_crypto_pwhash_str_verify() 함수를 사용해야 합니다. 이 함수는 입력받은 패스워드와 암호화된 패스워드를 비교하고, 일치하면 true 값을 반환합니다. sodium_crypto_pwhash_str_verify() 함수는 다음과 같이 사용할 수 있습니다.
$password = "mypassword"; $hashed_password = "$argon2id$v=19$m=65536,t=4,p=1$eG9wYXNzd29yZF92MzY3NjIw$c+T2QJyCCvxF/9Px10lR+37RbALlNOp13sZ+Ygbt97I"; // 암호화된 패스워드 if (sodium_crypto_pwhash_str_verify($hashed_password, $password)) { echo "패스워드 일치"; } else { echo "패스워드 불일치"; }
위 예시에서, $password 변수에 검증할 패스워드를 저장하고, $hashed_password 변수에는 미리 암호화된 패스워드를 저장합니다. sodium_crypto_pwhash_str_verify() 함수를 사용하여 입력받은 패스워드와 암호화된 패스워드를 비교하고, 일치하면 “패스워드 일치”라는 메시지를 출력합니다.
위와 같이 PHP에서 Scrypt 암호화를 사용하는 방법은 간단합니다. 하지만, 패스워드 암호화에 따른 보안 이슈에 대해서는 항상 유의해야 합니다. 패스워드 암호화 과정에서 salt를 사용하거나, sodium_crypto_pwhash_str() 함수에서 제공하는 옵션을 사용하여 보안성을 높이는 것이 좋습니다. 또한, 패스워드 검증 시 sodium_crypto_pwhash_str_verify() 함수를 사용하여 입력값 검증을 해야하며, SQL 인젝션 등의 보안 이슈에 대해서도 항상 유의해야 합니다.
패스워드 보안성 강화하는 방법
Scrypt 알고리즘은 일반적인 해시 알고리즘에 비해 공격자가 패스워드를 추측하는 것이 굉장히 어려운 보안성 높은 해싱 알고리즘입니다. 하지만, 보안성을 더 높이기 위해서는 다음과 같은 추가적인 작업이 필요합니다.
1. Salt 사용
Scrypt 알고리즘에서 salt는 입력받은 패스워드를 암호화할 때 사용되는 무작위 바이트값입니다. 이 값을 암호화된 패스워드와 함께 저장하여 보안성을 높일 수 있습니다.
sodium_crypto_pwhash_str() 함수는 기본적으로 salt를 자동으로 생성합니다. 하지만, sodium_crypto_pwhash() 함수를 사용하여 salt를 수동으로 생성할 수도 있습니다.
$salt = random_bytes(SODIUM_CRYPTO_PWHASH_SALTBYTES); $password = "mypassword"; $hashed_password = sodium_crypto_pwhash_str($password, SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE, $salt);
위 예시에서, random_bytes() 함수를 사용하여 salt를 생성하고, $salt 변수에 저장합니다. sodium_crypto_pwhash_str() 함수를 호출할 때, 생성한 salt 값을 넘겨줍니다.
2. 알고리즘 매개변수 지정
Scrypt 알고리즘에서 사용되는 매개변수(비용, salt 크기 등)를 적절하게 설정하여 보안성을 높일 수 있습니다.
$password = "mypassword"; $ops_limit = SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE; $mem_limit = SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE; $hashed_password = sodium_crypto_pwhash_str($password, $ops_limit, $mem_limit);
sodium_crypto_pwhash_str() 함수에서는 SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE와 SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE 상수를 사용하여 비용(cost factor)을 설정할 수 있습니다. 이 값들은 공격자가 패스워드를 추측하는 데 필요한 작업량을 결정합니다. 더 높은 비용은 더 강력한 보안성을 제공하지만, 계산 시간이 늘어날 수 있습니다.
3. 타이밍 공격 대응
Scrypt 알고리즘은 일반적인 해시 알고리즘에 비해 타이밍 공격에 강합니다. 하지만, 여전히 타이밍 공격에 취약한 경우가 있을 수 있습니다. 이를 방지하기 위해서는 sodium_crypto_pwhash_str_verify() 함수를 사용하여 패스워드 검증을 수행할 때 일정한 시간을 유지하도록 하는 것이 좋습니다. 이를 위해서는 time_nanosleep() 함수를 사용하여 일정 시간 동안 대기하는 방법이 있습니다.
$password = "mypassword"; $hashed_password = sodium_crypto_pwhash_str($password, SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE); // 일정한 시간 동안 대기 $verify_start = microtime(true); $sleep_time = 50000; // 50ms 대기 time_nanosleep(0, $sleep_time); $verify_end = microtime(true); // 패스워드 검증 $verify_time = $verify_end - $verify_start; if (sodium_crypto_pwhash_str_verify($hashed_password, $password) && $verify_time >= 0.05) { echo "패스워드 일치"; } else { echo "패스워드 불일치"; }
위 예시에서, microtime() 함수를 사용하여 현재 시간을 측정하고, time_nanosleep() 함수를 사용하여 50ms 동안 대기합니다. 그리고 sodium_crypto_pwhash_str_verify() 함수를 사용하여 패스워드 검증을 수행하고, 검증 시간이 50ms 이상인 경우에만 패스워드 일치로 판정합니다.
위와 같이 Scrypt 알고리즘을 사용할 때 보안성을 더 높이기 위해 여러 작업을 추가할 수 있습니다.
파이썬에서 Scrypt 암호화 사용 방법
Python에서 Scrypt 암호화를 사용하려면, scrypt 패키지를 설치해야 합니다. 다음과 같은 명령어를 사용하여 설치할 수 있습니다.
pip install scrypt
Python에서 Scrypt 암호화를 사용하려면 scrypt.hash() 함수를 사용해야 합니다. 이 함수는 입력받은 패스워드를 암호화하고, Scrypt 해시값을 반환합니다. scrypt.hash() 함수는 다음과 같이 사용할 수 있습니다.
import scrypt password = b'mypassword' salt = b'salt' hashed_password = scrypt.hash(password, salt=salt, N=16384, r=8, p=1, buflen=64)
위 예시에서, b 접두사는 문자열을 바이트열로 변환하는 역할을 합니다. password 변수에 암호화할 패스워드를 저장하고, salt 변수에는 salt 값을 저장합니다.
scrypt.hash() 함수에서 N, r, p, buflen 인자는 Scrypt 알고리즘의 비용(cost factor)을 설정하는 값들입니다. 이 값들은 공격자가 패스워드를 추측하는 데 필요한 작업량을 결정합니다. 더 높은 비용은 더 강력한 보안성을 제공하지만, 계산 시간이 늘어날 수 있습니다. 따라서 이 값은 적절하게 조정해야 합니다.
Python에서 Scrypt 암호화된 패스워드를 검증하려면, scrypt.verify() 함수를 사용해야 합니다. 이 함수는 입력받은 패스워드와 암호화된 패스워드를 비교하고, 일치하면 True 값을 반환합니다. scrypt.verify() 함수는 다음과 같이 사용할 수 있습니다.
import scrypt password = b'mypassword' salt = b'salt' hashed_password = scrypt.hash(password, salt=salt, N=16384, r=8, p=1, buflen=64) if scrypt.verify(password, hashed_password): print("패스워드 일치") else: print("패스워드 불일치")
위 예시에서 password 변수에 검증할 패스워드를 저장하고, hashed_password 변수에는 미리 암호화된 패스워드를 저장합니다. scrypt.verify() 함수를 사용하여 입력받은 패스워드와 암호화된 패스워드를 비교하고, 일치하면 “패스워드 일치”라는 메시지를 출력합니다.
위와 같이 Python에서 Scrypt 암호화를 사용하는 방법은 간단합니다. 하지만, PHP에서와 마찬가지로 패스워드 보안 이슈에 대해서는 항상 유의해야 합니다. 패스워드 암호화 과정에서 salt를 사용하거나, scrypt.hash() 함수에서 제공하는 매개변수를 사용하여 보안성을 높이는 것이 좋습니다. 또한, 패스워드 검증 시 scrypt.verify() 함수를 사용하여 입력값 검증을 해야 하며, SQL 인젝션 등의 보안 이슈에 대해서도 항상 유의해야 합니다.
1. salt 사용
Scrypt 알고리즘에서 salt는 입력받은 패스워드를 암호화할 때 사용되는 무작위 바이트값입니다. 이 값을 암호화된 패스워드와 함께 저장하여 보안성을 높일 수 있습니다.
import scrypt import secrets password = b'mypassword' salt = secrets.token_bytes(16) hashed_password = scrypt.hash(password, salt=salt, N=16384, r=8, p=1, buflen=64)
위 예시에서, secrets.token_bytes() 함수를 사용하여 salt를 생성하고, salt 변수에 저장합니다. scrypt.hash() 함수를 호출할 때, 생성한 salt 값을 넘겨줍니다.
2. 알고리즘 매개변수 설정
Scrypt 알고리즘에서 사용되는 매개변수(비용, salt 크기 등)를 적절하게 설정하여 보안성을 높일 수 있습니다.
scrypt.hash() 함수에서는 N, r, p, buflen 인자를 사용하여 비용(cost factor)을 설정할 수 있습니다. 이 값들은 공격자가 패스워드를 추측하는 데 필요한 작업량을 결정합니다. 더 높은 비용은 더 강력한 보안성을 제공하지만, 계산 시간이 늘어날 수 있습니다.
password = b'mypassword' salt = secrets.token_bytes(16) N = 32768 r = 8 p = 1 buflen = 64 hashed_password = scrypt.hash(password, salt=salt, N=N, r=r, p=p, buflen=buflen)
위 예시에서, N, r, p, buflen 값을 적절하게 조정하여 비용을 설정합니다.
3. 타이밍 공격 대응
Scrypt 알고리즘은 일반적인 해시 알고리즘에 비해 타이밍 공격에 더 강합니다. 패스워드 탈취 등 보안 문제를 방지하기 위해서는 time.sleep() 함수를 사용하여 일정한 시간을 유지하는 타이밍을 설정하는 것이 좋습니다.
password = b'mypassword' salt = secrets.token_bytes(16) N = 32768 r = 8 p = 1 buflen = 64 hashed_password = scrypt.hash(password, salt=salt, N=N, r=r, p=p, buflen=buflen) # 일정한 시간 동안 대기 import time start = time.monotonic() time.sleep(0.05) end = time.monotonic() # 패스워드 검증 if scrypt.verify(password, hashed_password) and (end - start) >= 0.05: print("패스워드 일치") else: print("패스워드 불일치")
위 예시에서, time.monotonic() 함수를 사용하여 현재 시간을 측정하고, time.sleep() 함수를 사용하여 50ms 동안 대기합니다. 그리고 scrypt.verify() 함수를 사용하여 패스워드 검증을 수행하고, 검증 시간이 50ms 이상인 경우에만 패스워드 일치로 판정합니다.