Web/Node.js

Arcane프로젝트 - Bcrypt와 jwt를 통한 로그인, 회원가입구현

unwind 2022. 11. 16. 17:01
반응형

회원가입과 로그인을 구현할 때 사용했던 Bcrypt와 JsonWebToken(이하 jwt)를 살펴보겠습니다.

우선, 보안상의 이유로 사용자의 비밀번호를 그대로 데이터베이스에 저장하는 것은 좋지않습니다.
Bcrypt 라이브러리를 통해 비밀번호를 암호화할 수 있습니다. Bcrypt에 대한 정의는 다음과 같습니다.

해시 함수에 대해 먼저 알아봐야겠습니다. 해시함수는 임의의 길이를 갖는 데이터에 대해 고정된 길이의 데이터로 매핑하는 함수를 말합니다.


어떠한 데이터를 집어넣든지 같은 길이의 결과를 내놓는다는 뜻입니다.
해시함수에서 사용되는 용어는 키, 해시값, 해시테이블, 해싱이 있습니다.
원래 데이터 값을 키, 함수를 실행한 결과 값을 해시 값, 해시 값과 데이터의 인덱스 주소를 해시 테이블, 함수 실행과정을 해싱이라고 합니다.

Bcrypt가 다른 해시함수들에 비해 가진 장점은 salting에 있습니다.


salting이란, 해싱하고 나서나온 해시 값에 무작위 데이터를 섞어서 결과를 내놓는것을 말합니다.

게다가, Bcrypt는 Salting Rounds라고 하는 옵션을 통해서 해시 값에 salting을 여러번 적용할 수 있습니다.
가령, Salting Rounds를 12라고 한다면, 처음 사용자가 입력한 비밀번호를 처음 해싱한 해시값을 salting하고, 그 salting한 결과값을 다시 salting하는 행위를 12번 반복합니다.
실제로, Arcane프로젝트에서도 Salting Rounds를 12로 할당하기도 했습니다.

// 클라이언트
await axios
    .post("/auth/signup",  {
    username: inputUsername,
    password: inputPassword,
    email: inputEmail,
})

클라이언트에서 이렇게 회원가입 요청을 했다고 하면 사용자이름, 비밀번호, 이메일이 서버로 도달할겁니다.

// 서버
// req.body의 사용할 데이터를 가져오기
const  {  username,  password,  email  }  = req.body;

// 새로운 사용자라면 비밀번호르 해슁 (암호화)
const  hashed  =  await bcrypt.hash(password, config.bcrypt.saltRounds);

res.status(201);

이렇게 간단하게 Bcrypt를 통한 해슁을 할 수 있습니다.
hash함수 안 파라메터로는 우선 비밀번호 원본과 아까 설명했던 saltingRounds를 전달합니다.
이제 hashed라는 해시 값을 데이터베이스에 비밀번호로써 저장하면 되네요.

비밀번호를 암호화했기 때문에, 로그인같이 비밀번호가 올바른지 확인해야할 때 일반적인 방법으론 확인할 수 없습니다. Bcrypt에는 해쉬 값들을 비교해주는 메소드가 있기 때문에, 걱정할 필요는 없었습니다.

const  isValidPassword  =  await bcrypt.compare(password, user.password);
// 데이터베이스에있는 비밀번호와 로그인 시 사용자가 입력한 비밀번호 비교
if (!isValidPassword) {
//비번 틀릴시
return res.status(401).json({ message:  "Invalid user or password"  });
}

bcrypt.compare 메소드를 통해 간편하게 해쉬값을 대조해볼 수 있습니다.

다음은 JWT입니다. jwt는 선택적 서명 및 암호화를 사용하여 데이터를 만들기 위한 표준입니다.
사용자 인증에 사용한다고 합니다. jwt는 보통 클라이언트의 로컬 스토리지에 저장합니다.
그리고, 클라이언트가 서버에 무언가를 요청할 때마다 jwt도 함께 보내서 허가된 jwt인지 검사하는것 입니다.
이 과정 자체가 로그인이 되어있는 클라이언트와 그렇지 않은 클라이언트 사이에 차이를 주는 방법입니다.
JWT는 Header, Payload, Signature의 3 부분으로 이루어지며, Json 형태인 각 부분은 Base64Url로 인코딩 되어 표현됩니다.

// 로그인 서버
const  token  =  createJwtToken(user.id);
res.status(201).json({ token, username });

Arcane프로젝트에서의 사용예시 입니다.
토큰생성은 간단합니다. 위 코드는 로그인 시 실행되는 코드인데 로그인을 올바르게 요청했다면,
그 유저의 아이디를 담아서 jwt 토큰을 만들고 클라이언트에게 반환해줍니다.

// 클라이언트
.then((res)  =>  {
    token.saveToken(res.data.token);
})

saveToken(token)  {
    // localStorage는 브라우저에서 이용할 수 있는 임시 저장소 api
    // 이를 이용해서 토큰을 저장해둠
    // 하지만 이 방법은 안전하지가 않다!
    // but, 현재 과정상 이 방법은 아주 기본적인 방법이기에 알아두자
    localStorage.setItem(TOKEN, token);
}

클라이언트는 저 토큰을 받아 로컬 스토리지에 저장하게됩니다.
이제 클라이언트는 로그인이 필요한 동작을 할 때마다, 서버로 방금 발급받은 토큰을 보내 인증된 사용자임을 증명합니다.

// 클라이언트
await axios
    .get("/auth",  {
        headers:  {
        token: token,
        },
    })
    .then((res)  =>  {
        setLogin(true);
    })
    .catch((err)  => console.log(err));

예를 들면, 클라이언트가 커뮤니티에 글을 작성하고 싶다고 서버에 요청을 보낼때,
우선 위 get요청을 먼저 실행하게 됩니다. 로그인시 발급받았던 토큰을 서버에 검증받는것이죠.

//서버
const  {  token  }  = req.headers;
const  decoded  = jwt.verify(token, secretKey --> 사전에 임의로 설정한 암호);

if (!decoded) {
    return res.status(401).json({ message:  "unauthorized"  });
}

const  user  =  await userRepository.findById(decoded.id);
res.status(201).json({ username: user.username});

토큰인증도 jwt 메소드를 통해 쉽게 가능합니다.
클라이언트가 보낸 토큰을 jwt.verify에 파라메터로 넣어주기만 하면 검증된 토큰인지 아닌지를 true false로 반환합니다.
파라메터로 함께 보내는 secretKey는 사전에 임의로 설정한 암호(변수)입니다.
이 secretKey는 서버를 개발할때 본인만이 알고있어야됩니다. 노출되지 않도록 조심해요.
토큰검증이 성공적으로 완료되었다면, 클라이언트는 로그인을 해야만 할 수 있는 행동이 가능해집니다.
여기까지가 Arcane에서 로그인, 회원가입을 구현한 방법이었습니다.

반응형