이 POST에서는 pbkdf2 알고리즘을 통한 암호화 및 로그인 세션을 다룹니다.
단방향 암호화 알고리즘
- A -> B는 가능하지만 B -> A는 불가능함.
- 암호화 방법은 가능하지만 복호화 방법은 없음.
- 대표적인 함수 : MD5, SHA 시리즈
- MD5는 충돌값이 발견되어 권장되지 않으며, SHA 또한 SHA-2만 권장됨.
양방향 암호화 알고리즘
- 대칭키 / 비대칭키로 나뉨
- 대칭키 알고리즘 : DES,AES / 암호화 * 복호화 키가 같다.
- 비대칭키 알고리즘 : RSA, ElGamal, Diffie-Helman 등등..
보통 사용자 비밀번호를 암호화할 때는 해쉬 함수를 쓰곤 합니다.
다만 해쉬 함수의 단점이 있습니다.
- 동일한 입력 값은 동일한 해쉬 값을 가진다.
- 동일하지 않은 입력 값에도 동일한 해쉬 값이 나올 수 있다.
따라서 이를 방지하기 위한 Salt와 Stretching이 있습니다.
Salt : 무작위 문자열을 입력값과 합쳐 해쉬 함수에 넣는다.
KeyStretching : 해쉬 함수를 여러번 돌린다.
여기에 딱 들어맞는 함수는 pbkdf2입니다.
Password Based Key Derivation Function version 2
직역하면 비밀번호 기반 키 유도 함수입니다.
var pbkdf2 = require('pbkdf2')
var derivedKey = pbkdf2.pbkdf2Sync('password', 'salt', 1, 32, 'sha512')
인자로 비밀번호, salt, 반복 횟수, 키 길이, 해쉬 함수 를 받습니다.
즉 pbkdf2를 통해 salt와 keyStretching이 모두 가능합니다.
저는 여기에 salt값을 random으로 정하도록 randomBytes 함수를 덧붙였습니다.
// crypto.randomBytes( size, callback ) ... 예시
crypto.randomBytes(127, (err, buf) => {
if (err) {
// Prints error
console.log(err);
return;
}
size 인자에 맞는 길이의 랜덤한 바이트가 생성됩니다.
예시 코드의 아래와 같이, 콜백 함수를 지정해 작업을 할 수도 있습니다.
randomBytes와 pbkdf2는 crypto 모듈을 사용합니다.
// 아래 코드 전, 이미 DB를 조회하여 입력된 정보가 없음을 전제합니다.
crypto.randomBytes(64,(err,buf)=>{
crypto.pbkdf2(userpw,buf.toString('base64'),108326,64,'sha512',(err,key)=>
{
const hashedpw = key.toString('base64');
const salt = buf.toString('base64');
connection.query('
INSERT INTO USERS (id, pw, uname, salt) VALUES(?,?,?,?)',
[userid,hashedpw,username,salt],
(err,data)=>{
if (err){
console.log(err);
}
});
});
});
앞서 언급했듯이 randomBytes로 salt값을 받습니다.
총 길이가 64인 SHA512 함수를 108326번 돌린 결과가 key에 저장됩니다.
한편 salt와 key는 버퍼 형태로 출력되므로 base64(64진법)으로 바꿔줍니다.
이후 데이터베이스에 해쉬된 Password와 Salt값을 저장해주었습니다.
유저가 로그인할 때 입력한 비밀번호에 대해 똑같이 단방향 암호화한 결과가
해싱된 비밀번호와 같은지 검증해야하기 때문입니다.
// 사용자가 id, pw를 입력한 것을 전제합니다.
// 사용자가 입력한 pw는 userpw입니다.
// 이미 윗단에서 DB가 동일한 id를 조회한 것으로 가정합니다.
crypto.pbkdf2(userpw,results[0].SALT,108326,64,'sha512',(err,key)=>
{
const realPW = key.toString('base64');
if (realPW==results[0].PW){
req.session.displayName=userid;
res.render('infoHTML/loginInfo.html',
{name:req.session.displayName});
}
else{
res.send('<script>alert("로그인 정보가 일치하지 않습니다.");
document.location.href="/login";</script>');
}
});
사용자가 입력한 id가 DB에 있는 경우,
쿼리는 해싱키가 있는 PW 컬럼과 SALT 컬럼을 가져옵니다.
그것들을 각각 results[0].PW, results[0].SALT 로 제어합니다.
입력한 패스워드를 pbkdf2 알고리즘과 조회한 salt키를 통해 암호화합니다.
이 때 해싱된 키가 조회된 PW 컬럼값과 동일하면 정보가 맞는 것입니다.
세션은 현재 로그인한 유저를 인식하는 기능입니다.
유저가 접속할 때 서버에게 session id를 전달합니다.
서버는 session id를 받고 현재 접속한 유저가 누구인지 식별합니다.
쿠키와 세션의 기능은 거의 동일하지만 저장 위치가 다릅니다.
Cookie : 브라우저
Session : 서버
브라우저에 저장되는 방식으로 인해 Cookie는 request를 통해 스니핑당할 수 있습니다.
반면 Session은 session id만 전달될 뿐 그 외의 정보는 서버에 저장되므로 상대적으로 보안이 우수합니다.
다만 Session이 무분별하게 서버의 자원을 사용하여 메모리가 감당되지 않을 수 있을 경우가 있어 Cookie를 아예 사용하지 않는 것은 아닙니다.
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
app.use(session({
secret : '-',
resave : false,
saveUninitialized : true,
secure : true,
HttpOnly : true,
store : new MySQLStore({
host : 'localhost',
port : 3306,
user : 'root',
password: '-',
database: 'TEST_DB'
})
}));
세션은 express-session 모듈이 필요하고,
저는 MySQL을 통해 세션을 저장할 것이므로 express-mysql-session 모듈 또한 설치했습니다.
- secret : 세션 암호화에 사용될 키
- resave : 세션을 저장하고 불러올 때 세션 재저장 여부
- saveUninitialized : 세션 사용 전 발급 여부
- secure : true로 설정하게되면 https 환경에서만 session 정보를 주고받음
- HttpOnly : 값이 true면 사용자가 js로 세션을 사용할 수 없도록 강제함
- store : 세션이 저장될 저장소를 명시
위 항목에서,
resave : false
saveUninitialized : true
가 권장옵션이라고 합니다.
MySQLStore를 생성했으므로, 세션이 발생될 때 SQL에 세션 테이블이 만들어집니다.
원래 USERS 밖에 없던 테이블에 session 테이블이 새로 생성되었습니다.
A라는 유저로 로그인을 해보니
위와 같이 displayName:"A"의 값을 지닌 데이터가 생성됩니다.
이것은 로그인시 session.displayName을 id로 지정했기 때문입니다
위와 같이 로그인이 정상적으로 처리되는 경우
req.session.displayName = userid 를 통해 지정된 것입니다.
세션을 통해 유저를 식별하므로, Logout은 반대로 세션을 삭제하면 됩니다.
const router = require('express').Router();
const path = require('path');
router.post('/logout',(req,res)=>{
req.session.destroy();
res.render('infoHTML/info.html');
})
module.exports = router;
///userLogout.js
세션을 제거하는 명령은 req.session.destroy()입니다.
로그아웃 버튼을 누르면 세션이 제거되고 첫번째 페이지로 이동합니다.
위에서 접속한 A의 로그아웃 후 세션 데이터가 삭제되었습니다.
로그인한 상태에서 로그인 페이지를 들어갈 경우가 있습니다.
다음은 이를 방지하기 위한 코드입니다.
router.get('/login',(req,res)=>{
if (typeof req.session.displayName!=='undefined'){
res.send("<script>alert('이미 로그인되어있습니다.');
document.location.href='/loginInfo'</script>")
}else{
res.render("userHTML/login.html");
}
})
위는 로그인 페이지를 이동할 때 작동하는 login 라우터입니다.
displayName 속성은 login이 정상 처리되었을 때 발생하기 때문에
로그인하기 전 발급된 session에는 displayName 속성이 없습니다.
이에 착안하여, req.session.displayName의 타입이 undefined가 아니라면
로그인 후 접속되는 페이지로 이동시킵니다.
이제야 단순한 로그인 기능 구현이 끝났습니다.
다음은 CRUD 기반 게시판 만들기입니다.
게시판 글, 댓글을 저장하는 DB 테이블이 따로 있어야하고
여기에 유저의 세션ID가 필요하므로 테이블 간 관계가 있어야할 것입니다.
- 글쓰기 버튼을 누른다.
- 세션 ID가 전달되며, 글 쓰는 html 페이지로 이동한다.
- 글을 쓰면 Subject, Contents는 세션 ID 및 글 ID와 함께 DB로 전달된다.
- 게시판 HTML에 글 제목과 글 번호가 담긴 행이 생성된다.
기본 골자는 위와 같고, 부가 기능은 아래와 같을 것입니다.
검색 기능
- 글 ID로 검색
- SELECT 문 사용- 유저 ID로 검색
- SELECT 문 사용- 글 제목, 내용으로 검색
- SELECT 문 및 LIKE 사용
게시글 관리
- 생성
- 로그인 한 경우에만 가능. 미로그인시 로그인 페이지로 강제 이동
- 수정
- 게시자와 접속한 유저의 ID가 같으면 활성화
- 삭제
- 비밀번호를 입력해 삭제하도록 하는 방식 사용 가능
- 게시자와 접속유저의 ID가 같거나, 마스터 계정일 때 활성화
- 읽기
- 특정 게시판에 대해 유저가 가지고 있는 권한에 따라 조회 활성화
우선 간단한 게시판 html을 만들고 게시글 생성부터 차근차근할 예정입니다.