암호화와 세션 #3

jh_leitmotif·2021년 6월 19일
0

🧐 개요

이 POST에서는 pbkdf2 알고리즘을 통한 암호화 및 로그인 세션을 다룹니다.

📂 암호화 알고리즘

단방향 암호화 알고리즘

  • A -> B는 가능하지만 B -> A는 불가능함.
  • 암호화 방법은 가능하지만 복호화 방법은 없음.
  • 대표적인 함수 : MD5, SHA 시리즈
  • MD5는 충돌값이 발견되어 권장되지 않으며, SHA 또한 SHA-2만 권장됨.

양방향 암호화 알고리즘

  • 대칭키 / 비대칭키로 나뉨
  • 대칭키 알고리즘 : DES,AES / 암호화 * 복호화 키가 같다.
  • 비대칭키 알고리즘 : RSA, ElGamal, Diffie-Helman 등등..

보통 사용자 비밀번호를 암호화할 때는 해쉬 함수를 쓰곤 합니다.

다만 해쉬 함수의 단점이 있습니다.

  1. 동일한 입력 값은 동일한 해쉬 값을 가진다.
  2. 동일하지 않은 입력 값에도 동일한 해쉬 값이 나올 수 있다.

따라서 이를 방지하기 위한 Salt와 Stretching이 있습니다.

Salt : 무작위 문자열을 입력값과 합쳐 해쉬 함수에 넣는다.
KeyStretching : 해쉬 함수를 여러번 돌린다.

여기에 딱 들어맞는 함수는 pbkdf2입니다.

📂 PBKDF2 알고리즘

Password Based Key Derivation Function version 2

직역하면 비밀번호 기반 키 유도 함수입니다.

📄 Example Code

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 모듈을 사용합니다.


📂 회원가입시 비밀번호 암호화

📄 Code

// 아래 코드 전, 이미 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값을 저장해주었습니다.

유저가 로그인할 때 입력한 비밀번호에 대해 똑같이 단방향 암호화한 결과가
해싱된 비밀번호와 같은지 검증해야하기 때문입니다.

📂 로그인 비밀번호 검증

📄 Code

// 사용자가 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

세션은 현재 로그인한 유저를 인식하는 기능입니다.

유저가 접속할 때 서버에게 session id를 전달합니다.
서버는 session id를 받고 현재 접속한 유저가 누구인지 식별합니다.

쿠키와 세션의 기능은 거의 동일하지만 저장 위치가 다릅니다.

Cookie : 브라우저
Session : 서버

브라우저에 저장되는 방식으로 인해 Cookie는 request를 통해 스니핑당할 수 있습니다.

반면 Session은 session id만 전달될 뿐 그 외의 정보는 서버에 저장되므로 상대적으로 보안이 우수합니다.

다만 Session이 무분별하게 서버의 자원을 사용하여 메모리가 감당되지 않을 수 있을 경우가 있어 Cookie를 아예 사용하지 않는 것은 아닙니다.

📄 Code

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 모듈 또한 설치했습니다.

  1. secret : 세션 암호화에 사용될 키
  2. resave : 세션을 저장하고 불러올 때 세션 재저장 여부
  3. saveUninitialized : 세션 사용 전 발급 여부
  4. secure : true로 설정하게되면 https 환경에서만 session 정보를 주고받음
  5. HttpOnly : 값이 true면 사용자가 js로 세션을 사용할 수 없도록 강제함
  6. store : 세션이 저장될 저장소를 명시

위 항목에서,

resave : false
saveUninitialized : true

가 권장옵션이라고 합니다.

MySQLStore를 생성했으므로, 세션이 발생될 때 SQL에 세션 테이블이 만들어집니다.

원래 USERS 밖에 없던 테이블에 session 테이블이 새로 생성되었습니다.

A라는 유저로 로그인을 해보니

위와 같이 displayName:"A"의 값을 지닌 데이터가 생성됩니다.

이것은 로그인시 session.displayName을 id로 지정했기 때문입니다

위와 같이 로그인이 정상적으로 처리되는 경우

req.session.displayName = userid 를 통해 지정된 것입니다.

📂 Logout 처리

세션을 통해 유저를 식별하므로, Logout은 반대로 세션을 삭제하면 됩니다.

📄 Code

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의 로그아웃 후 세션 데이터가 삭제되었습니다.


🎯 로그인한 상태에서의 처리

로그인한 상태에서 로그인 페이지를 들어갈 경우가 있습니다.

다음은 이를 방지하기 위한 코드입니다.

📄 Code

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가 필요하므로 테이블 간 관계가 있어야할 것입니다.

  1. 글쓰기 버튼을 누른다.
  2. 세션 ID가 전달되며, 글 쓰는 html 페이지로 이동한다.
  3. 글을 쓰면 Subject, Contents는 세션 ID 및 글 ID와 함께 DB로 전달된다.
  4. 게시판 HTML에 글 제목과 글 번호가 담긴 행이 생성된다.

기본 골자는 위와 같고, 부가 기능은 아래와 같을 것입니다.

검색 기능

  • 글 ID로 검색
    - SELECT 문 사용
  • 유저 ID로 검색
    - SELECT 문 사용
  • 글 제목, 내용으로 검색
    - SELECT 문 및 LIKE 사용

게시글 관리

  • 생성
    • 로그인 한 경우에만 가능. 미로그인시 로그인 페이지로 강제 이동
  • 수정
    • 게시자와 접속한 유저의 ID가 같으면 활성화
  • 삭제
    • 비밀번호를 입력해 삭제하도록 하는 방식 사용 가능
    • 게시자와 접속유저의 ID가 같거나, 마스터 계정일 때 활성화
  • 읽기
    • 특정 게시판에 대해 유저가 가지고 있는 권한에 따라 조회 활성화

우선 간단한 게시판 html을 만들고 게시글 생성부터 차근차근할 예정입니다.

profile
Define the undefined.

0개의 댓글