인증, 보안 개념정리 - 계정만들기, 로그인 과정
0.
이쯤오면 여러분들은 이미 웹서비스의 많은 것들을 배웠을 것이다.
프론트 엔드단에서 HTML, CSS, 브라우저측 JS를 사용해서
웹사이트를 만드는 법을 배웠을 것이고
백엔드 단에서 서버를 만들고 데이버테이스를 만든 뒤
데이터와 파일을 주고받고 클라이언트에게 response를 되돌려 보내는 방법을 배웠을 것이다.
이를 통해서 CRUD 기능이 달린 단순한 게시판 정도는 만들어냈다고 가정을 하자.
1.
이 정도의 게시판을 이해하는 것도 훌륭하고,
지금까지 배운 것만으로도 많은 웹서비스를 제공할 수 있지만
더 제대로 된 웹사이트들이 가지고 있는
사용자 인증과 쿠키, 세션에 대해서 배울 역량이 생겼다는 의미이기도 하다.
그래서 이러한 인증(Authentication)의 기능을 가진 Sginup(회원가입)&Login(로그인)
기능을 구현해보고..
쿠키와 세션의 개념을 알아보도록 하자.
2. 인증의 개념
인증Authentication이란 무엇일까?
대부분의 웹사이트 상에서는, 모든 사람이 접근가능accessible한 공간이 있는 반면
아무나 접근해서는 안되는 페이지들이 존재한다.
개인프로필 페이지나, 타인의 쇼핑카트/구매내역 등을
남들이 들여다볼 수 있게 웹사이트를 만드는 것은
개인정보 보호법 위반이다.. 미필적 고의로 잡혀가도 마땅하다..
3.
그래서 제대로된 웹서비스를 제공하기 위해서는
Sign Up 과정을 통해 서버에 Create Account를 한 뒤,
User Login과정에서 ID가 되는 이메일과 패스워드를 입력해서
서버에 인증을 받아 유효한 세션을 만들어야한다.
4. Create Account 기능 구현하기.
NodeJS를 이용해서 인증을 하는 기능을 구현하는 과정은 다음과 같다.
우선 클라이언트의 HTML 상에서 다음과 같이 forn data를 형성해서
서버단에 /login 라우터를 향하여 POST 메소드를 쏴주고
<form action="/login" method="POST">
<div class="form-control">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-control">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button class="btn">Log in</button>
</form>
서버 단에서는 이러한 정보를 받아서
DB의 유저정보 테이블에 넣어주면 된다.
router.get('/login', function(req, res) {
res.render('login');
})
router.post('/signup', async function (req, res) {
// sign up
// post로 받은 req 객체에서 데이터 분리
const userData = req.body;
const enteredEmail = userData.email; // userData['email'];
const enteredConfirmEmail = userData['confirm-email'];
const enteredPassword = userData.password;
const user = {
email:enteredEmail,
password: enteredPassword
};
await db.getDb().collection('users').insertOne(); // 몽고DB에 유저데이터 삽입
res.redirect('/login'); // 로그인 페이지로 리다이렉션
});
router.post('/login', async function (req, res) {
// log in
});
router.get('/admin', function (req, res) {
res.render('admin');
});
5.
Create Account에서
위에서 처럼 id와 password를 raw data로 받아서 DB에 넣으면 심각한 문제가 생긴다.
DB가 이렇게 PW를 직접저장해버리면..
돈을 받고 사주받은 직원, 하드디스크를 수거해가는 업체사람,
네트워크든 물리적방법이든 잠입하여 DB를 탈취한 해커들 등등..
악의를 가진 사람들에게 언제든지 DB를 털릴 가능성이 존재한다.
이런사람들에게 걸리면..
허구한날 발생하는 개인정보 유출 뉴스의 책임자가
우리가 되어버리는 것이다.
항상 현실은 우리의 상상을 뛰어넘고
상상이상의 방법으로 유저데이터를 유출당했을때 문제가 발생한다.
유저들은 동일한 비밀번호를
많은 사이트에 공유해서 사용하는 경향이 있으며
특정 A사이트에서 털린 raw ID-PW데이터는 특정 B사이트에서 사용하여
연쇄적으로 문제를 일으키는 경우가 많다.
그렇기 때문에..
웹사이트에서는 Password를 그냥 저장해서는 안된다.
이것이 정보처리기사에서.. 보안 알고리즘을 무작정 암기부터 시키고 보는 이유이다.
SHA-256같은 해싱Hashing 알고리즘은
사용자가 사회공학적, 물리적인 방법으로 Raw비밀번호를 털리지 않은 이상은
양자컴퓨터가 나오기 전에는 뚫리지 않는 보안알고리즘이다.
물론.. HTTPS안쓰고 HTTP를 쓰면서 내가 전송한 데이터 패킷을 감청당하거나
무선공유기에서 허접 보안알고리즘을 써서 서버에 도달하기 전에 비밀번호를 털리는 것도 가능하다.
국정원 행님들, 중국북한 전업해커들 뜨면 뭔짓을 해도 털리겠지..
6.
NodeJS에서는 bcrypt라는 패키지를 통해서
해싱을 할 수 있다.
아래의 Hashed PW를 통해서
기존의 RawPW를 아는 사람은 해쉬된 PW를 쉽게 알아낼 수 있지만
해쉬된 PW만을 아는 사람은 기존의 RawPW를 알수 없게 된다.
이러한 비대칭적인 관계가 해시 알고리즘의 핵심이다.
router.post('/signup', async function (req, res) {
// sign up
// post로 받은 req 객체에서 데이터 분리
const userData = req.body;
const enteredEmail = userData.email; // userData['email'];
const enteredConfirmEmail = userData['confirm-email'];
const enteredPassword = userData.password;
const hashedPassword = await bcrypt.hash(enteredPassword, 12); // 12는 보안의 강도(强度)
const user = {
email:enteredEmail,
password: hashedPassword
};
await db.getDb().collection('users').insertOne(); // 몽고DB에 유저데이터 삽입
res.redirect('/login'); // 로그인 페이지로 리다이렉션
});
7.
그래서, DB에는 이메일과 해싱된 Password를 하나의 레코드로 저장하고
Log In 과정에서는
Bcrypt 패키지를 이용해 유저 패스워드를 해싱하여 비교하는 방식으로
로그인을 수행할 수 있다.
router.post('/login', async function (req, res) {
// log in
// request from client
const userData = req.body;
const enteredEmail = userData.email;
const enteredPassword = userData.password;
// get data from db
const existingUser = await db
.getDb()
.collection('users')
.findOne({ email: enterdEmail});
// DB로부터 id에 해당하는 데이터를 반환받지 못한 경우
if (!existingUser){
console.log('Could not log in!');
return res.redirect('/login');
}
// 사용자로부터 입력받은 PW를 해싱하고
// 저장되어 있는 DB의 hashedPW와 비교
const passwordsAreEqual = await bcrypt.compare(
enteredPassword,
existingUser.password
);
if (!passwordsAreEqual) {
console.log('Could not log in -passwords are not equal!');
return res.redirect('/login');
}
// validation(가입정보 유효성검사)
if (
!entered Email ||
!enteredConfirmEmail ||
!enteredPassWord ||
enteredPassword < 6||
enteredEmail !== enteredConfirmEmail
!enteredEmail.includes('@');
) {
console.log('Incorrect Data');
return res.redirect('/signup');
}
// 로그인 성공
console.log('User is authenticated!');
res.redirect('/admin');
});
8.
그리고..
마지막으로 가입된 정보가 유효한 정보인지..
가령 이전 시스템에서 마이그레이션 해온 과정에서
새로운 버젼의 데이터와 형식이 같지 않다던지
하는 경우가 생길 수도 있다.
그런 경우를 대비해 가입정보가 유효한지 체크하는 것도
의미있는 로그인 과정이 될 수도 있을 것이다.
if (
!entered Email ||
!enteredConfirmEmail ||
!enteredPassWord ||
enteredPassword < 6||
enteredEmail !== enteredConfirmEmail
!enteredEmail.includes('@');
) {
console.log('Incorrect Data');
return res.redirect('/signup');
}