Study/Node.js

[새싹x코딩온] 웹 개발자 부트캠프 과정 8주차 회고 | 비밀번호 암호화(feat. bcrypt)

다니니니 2024. 7. 6. 19:34
728x90

1. 들어가며

  • 비밀번호 암호화에 대해 알아보고 bcrypt 모듈을 사용해서 암호화하기

2. 비밀번호 암호화

비밀번호 암호화

전에 회원가입 기능을 구현하는 실습을 할때는 비밀번호 암호화를 하지 않고 DB에 저장을 했다.

그런데 만약 실서비스에서 DB에 비밀번호를 암호화를 하지 않고 저장한다면??

그 DB가 해킹이라도 당하거나 혹은 개발자가 정말 나쁜 마음을 가지고 그 DB에 있는 유저 정보를 이용한다면??

 

이를 방지하기 위해 똑똑한 개발자들이 비밀번호 암호화 라는 기능을 만들었다.

비밀번호 암호화에는 두가지 종류가 있는데 단방향양방향이다.

이를 그림으로 표현하면 아래와 같다.

출처 : 코딩온 강의 교안

 

단방향 암호화

단방향 암호화는 주로 비밀번호(패스워드) 저장 시 활용한다. 

단방향이기 떄문에 원본 데이터를 복원(복호화)을 할 수 없다.

'해시' 라는 것을 사용하는데, 동일한 데이터에서는 같은 해시값이 생성되지만

미세한 데이터의 변화에도 완전 다른 값이 나온다.

비밀번호 '1234'와 '1234'는 같은 해시값이지만 '1234'와 '1233'은 다른 해시값이다.

 

양방향 암호화

양방향 암호화는 단뱡향 암호화와는 다르게 복호화가 가능하다.

공개키와 대칭키라는 것을 이용해서 암호화/복호화를 한다.

대칭키는 암호화/복호화할 때 쓰는 키가 동일하므로 키 공유가 안전하게 이뤄져야 한다.

공개키는 암호화할때의 키와 복호화할 때의 키가 다르다.

(공개키로 암호화하고 개인키로 복호화를 한다.)

 

 

Bcrypt

오늘 포스팅을 할 bcrypt는 비밀번호를 암호화하는 알고리즘 중 하나다.

이 모듈은 단뱡향 암호화만을 지원하지만 오히려 그 점 때문에

비밀번호 암호화 시 많이 사용된다.(비밀번호같은 민감한 정보는 복호화할 필요가 없기 때문이다.)

 

bcrypt는 외부 라이브러리라 npm i bcrypt  로 설치를 해줘야 한다.

 

// bcrypt 모듈 불러오기
const bcrypt = require('bcrypt')

// 정답 비밀번호 정의
const originalPw = '1234'

// 솔트 생성 라운드 수 정의
// 라운드? 해시 함수를 반복하는 횟수
const saltRounds = 10 // 2^10 = 1024회 반복 ( 10~12 사이의 값을 보통 씀)
// 솔트 라운드 숫자가 커짐? -> 해시 생성 시간이 길어짐(느려짐), but 보안성이 높아진다.

// 비밀번호 해싱 함수 정의
const hashPw = (pw) => {
    // hashSync(비밀번호, 솔트생성라운드수)
    return bcrypt.hashSync(pw, saltRounds)
}

// 비밀번호 정답 검증 함수 정의
const comparePw = (inputPw, originalPw) => {
    return bcrypt.compareSync(inputPw, originalPw) 
    // 평문 비밀번호와 암호화된 비밀번호를 받아서 비교
    // 사용자가 입력한 평문과 해시값을 비교하여 boolean 형태로 반환
}

const hashedPw = hashPw(originalPw)
// console.log(`hashedPw ${hashedPw}`);

// 비교 
const isMatch = comparePw(originalPw, hashedPw)
console.log(isMatch ? '비번 일치' : '비번 불일치');
const isMatch2 = comparePw('1233', hashedPw)
console.log(isMatch2 ? '비번 일치' : '비번 불일치');

 

위의 코드에서 isMatch는 1234와 1234

isMatch2는 1233와 1234를 비교한다.

 

이 둘에 대한 결과는 어떻게 나올까?

당연히 위와 같이 나온다.

같은 값에 대해선 동일한 해시값이 생성되므로 일치하고

다른 값에 대해선 완전히 다른 해시값이 생성되므로 일치하지 않는다.

 

이 기능을 이용해서 회원가입 시 암호화된 비밀번호 해시값을 DB에 저장하고, 

로그인을 할 때 입력된 비밀번호의 해시값과 저장된 비밀번호 해시값을 비교해서 처리해줄 수 있다.

 

Bcrypt를 이용한 실습

배운것은 써먹어야 하므로 bcrypt를 이용해서 회원가입/로그인 기능을 구현하는 실습을 하던 중 

생각하지 못한 실수를 해서 이에 대해 포스팅 하고자 한다.

회원 정보 수정을 했을 때 였다. 

 

이런식으로 비밀번호와 닉네임을 수정할 수 있게 해놨다.

그런데 코드를 조금 섬세하게 짜지 못해서 이미 해싱처리되어 보여져 있는 비밀번호에 대해 또다시 해싱처리를 해서

디비값에 저장이 되도록 해버렸다.

// 회원정보수정
exports.patchProfile = async (req, res) => {
    try {
        const { pw, name } = req.body
        const userid = req.session.userid
        const isPwMatch = await Member.findOne({
            where : {userid},
            attributes : ['pw']
        })

        const updateName = async () => {
            const updateName = await Member.findOne({
                where : {userid},
                attributes : ['name']
            })
            req.session.name = updateName.name
            res.send('edit')
        }
        
       
        const updateMemberInfo = await Member.update(
            { pw:hashPw(pw), name},
            { where : {userid} }
        )
        updateName()
        
    } catch(err){
        console.error(err);
    }
}

그러다보니 비밀번호 변경을 하지 않았음에도 해싱처리가 되어서... 기존 로그인 정보로는 로그인을 할 수가 없었다.

그래서 아래와 같이 if문을 추가해서 비밀번호가 기존과 같으면 변경이 되지 않도록 코드를 변경했다. 

// 회원정보수정
exports.patchProfile = async (req, res) => {
    try {
        const { pw, name } = req.body
        const userid = req.session.userid
        const isPwMatch = await Member.findOne({
            where : {userid},
            attributes : ['pw']
        })

        const updateName = async () => {
            const updateName = await Member.findOne({
                where : {userid},
                attributes : ['name']
            })
            req.session.name = updateName.name
            res.send('edit')
        }
        
        if(comparePw(pw, isPwMatch.pw)){ // 비밀번호 같으면 메시지 보여주기
            res.send('same')
        } else if(pw===isPwMatch.pw) { // 비밀번호가 같으면 닉네임만 수정
            const updateMemberInfo = await Member.update(
                {name},
                {where : {userid}}
            )
            updateName()
        } else { // 비밀번호랑 닉네임 둘 다 수정
            const updateMemberInfo = await Member.update(
                { pw:hashPw(pw), name},
                { where : {userid} }
            )
            updateName()
        }
    } catch(err){
        console.error(err);
    }
}

 

3. 마치며

오늘 비밀번호 암호화를 배움으로써 뭔가 안심되는? 회원가입/로그인 기능을 구현할 수 있게 되어 좋았다.

실습을 하면서 해시처리함수를 어디서 써야 하는것인가에 대한 의문이 생겼다.

프론트엔드에서 해시처리를 해서 백엔드쪽으로 비밀번호를 전달하는게 맞는것인가?

백엔드에서 해시처리를 하지 않은 비밀번호를 해시처리를 해서 DB에 저장하는게 맞는것인가?

그래서 이에 대해 리더님께 질문을 드렸다!

답은 백엔드에서 해싱처리를 하는 것이 바람직하다는 것이었다.

 

1. 프론트에서 처리를 하게 되면 이 알고리즘이 외부에 노출될 수 있으므로 외부에서 분석이 가능하여 해킹의 위험을 높인다.

2. 백엔드에서 해싱 처리를 하면 다양한 클라이언트(웹 혹은 모바일)에서 동일한 해싱 로직 적용이 가능하다.

 

실제로 프론트에서 해보려고 했더니 .bcrypt.가 node.js 라이브러리라 프론트딴에서 저 함수를 가져올 수 없었다.

(검색을 해보니 babel이라는 것을 사용하면 된다고는 했는데.. 이미 프론트에서 처리를 하지 않기로 했으므로 굳이 사용하지 않았다.)

의문이 왜 들었냐면 프론트에서 해싱처리를 하지 않으면 백엔드 쪽에서 원문값을 알기 때문에 위험하지 않을까? 싶은 것이었는데.. 백엔드 단에서 비밀번호에 대한 로그를 찍지 않고 원문값을 메모리에 저장하지 않으면 안전하다고 하셨다!

 

4. Reference

1. 코딩온 강의 교안 및 실습

728x90