이제 나는 로그인 API도 만들었고, 비밀번호 중개사(?) 자격증도 취득했다!
그렇다고 또 거만해지면 안된다.. 아직 갈 길이 멀기 때문!
로그인에 성공 했다면 이후 부터는 로그인한 유저가 사용할 수 있는 기능을 누리게 되는데
그 때마다 계속해서 비밀번호를 요구할 수도 없다
그렇다면 비밀번호는 다른 곳에 따로 저장해두는 걸까?
따로 저장은 해두지만 비밀번호를 저장하면 또 위험해진다..
아무리 암호화했다 치더라도 우리 정보를 탈취해가는 그 분들은 늘 그렇듯.. 답을 찾아낸다..
그럴 때 저장해두는 것이 바로 토큰이다!
비유하자면 입장권이라고 생각하면 된다!
(전시회나 놀이동산이나 클럽 등에서 채워주는 팔찌 같은 느낌?)
앞서 포스팅한 바와 같이 HTTP는 기본적으로 state-less를 지향한다
간단하게 정리하자면
서버-클라이언트 구조에서 서버는 클라이언트의 상태 기억하고 있지 못하는 것
이때의 장단점을 따져보자면
이 때의 단점을 보완하기 위해 토큰을 사용하는 것이고
그 토큰 중에서도 제일 대중적이고 유명한 라이브러리가 JWT 이다
JWT는 세 부분으로 이루어져 있다
header.payload.signature
// header
// JWT의 종류와 해시 알고리즘 등의 정보를 담고 있는 부분
{
"alg": "HS256", // Signature을 만드는데 사용한 알고리즘 정보
"typ": "JWT" // Token의 타입
}
// payload
// 실제로 전달하려는 정보가 담긴 부분으로 위에서 예시로 든 티켓에 다시 비유하자면
// 그 티켓에 사용자의 정보(누구의 티켓인지)를 넣어두어 이용자를 특정한다
{
"sub": "1234567890", // 담긴 데이터 내용 1
"name": "John Doe", // 담긴 데이터 내용 2
"iat": 1516239022, // 토큰 발급 시간
"exp": 1516239082 // 토큰 만료 시간
}
// Signature
// Header와 Payload를 인코딩하고, 시크릿 키를 사용하여 서명한 값
// 이를 통해 토큰이 변조되지 않았음을 확인할 수 있다
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret // 시크릿키
)
여기까지는 설명이고 나는 뭐든지 실물을 봐야 더 잘 이해를 한다
바로 보여드리겠다
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZW1haWwiOiJ5b2tpZGRkZGRAbmF2ZXIuY29tIiwic3RhdHVzIjowLCJpYXQiOjE3MDQ4NjM5NDAsImV4cCI6MTcwNDg2NzU0MH0.JM9gwC-kG07N_AR3RaKOtYaRDHWFUK7z71oVlNxH36s
이런 식의 아주 긴 문자열이 나오는데
어떻게 보면 앞서 설명했던 bcrypt와 비슷하다고 볼 수 있다
중간 중간 마침표가 찍혀 있는 것을 볼 수 있는데
이 마침표로 header와 payload, signature가 구별된다고 볼 수 있다
그냥 문자열 하나일 뿐인데 여기에 우리 유저의 귀중한 정보가 담긴 것이니 잘 들고 있어야 한다.
프론트에게 물어보니 이 토큰이 유저의 로컬스토리지나 쿠키에 저장이 된다고 한다.
(이 부분에 대해서는 나중에 프론트 파트도 따로 공부할 터이니 그때 알아보자.. 꼭 :D)
그리고 유저의 이동이 있을 때마다 그곳에 저장되어 있는 토큰을 같이 보내는 것이지
그렇게 함으로써 유저의 정보(계정이나 비밀번호)는 계속 재 입력이 필요 없어 지는 것
일단은 설치(터미널 이용)
npm install jsonwebtoken
이후 코드를 작성 해보자
##javascript
// 토큰 발급
const jwt = require('jsonwebtoken');
// signatur생성에 필요한 암호화 키(bcrypt의 솔트와 비슷한 역할)
const secretKey = 'mySecretKey';
// 발급된 토큰에 담기게 될 유저 식별 정보
// 이메일이나, 이름, 전화번호 등은 위험한 정보이므로
// 보통은 유저 식별에 필요한 식별데이터와 어드민 계정 구별을 위한 상태값을 담는다
// 꼭 객체 형태로 담아줘야 한다
const payload = {
userId: '123456',
role: 'admin'
};
// 토큰을 생성
// jwt.sign(payload, secretKey)정도가 제일 기본 정보인데
// 로컬에 저장되는 부분이기때문에 만료기간 미설정시 언제든 탈취 가능(?) 상태가 된다
// 그러므로 꼭 만료시간을 지정해야 한다(보통 1시간으로 정한다)
// 숫자로 입력시 초단위, 문자로 입력시 1s, 1m, 1h등으로 입력하면 된다(3600 = 1m)
const token = jwt.sign(payload, secretKey, { expiresIn: '1h' });
// 이후 내가 확인하거나 프론트에게 이 토큰을 보내주면 된다
console.log('Generated Token:', token);
...
// 프론트에게 받은 토큰
const token = '...'; // 받은 토큰
// 토큰 검증
const verify = jwt.verify(token, secretKey)
// 검증된 토큰을 확인해보면 이전에 내가 담은 내용을 볼 수 있다
console.log(verify)
// 콘솔 값
{
userId: '23456',
role: 'not admin'
iat: 1704863940,
exp: 1704867540
}
// exp - iat = 3600 즉, 토큰 유효시간은 1시간인 것을 알 수 있다
탈취된 토큰은 그냥 아주 자동문 그자체다..
그러니 정말 조심한다고 하더라도 항상 나보다 멍청이보다는 똑똑이가 더 많은 세상이다..ㅜ
JWT 공식홈페이지에서 위에서 내가 생성했던 토큰을 입력해보자
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZW1haWwiOiJ5b2tpZGRkZGRAbmF2ZXIuY29tIiwic3RhdHVzIjowLCJpYXQiOjE3MDQ4NjM5NDAsImV4cCI6MTcwNDg2NzU0MH0.JM9gwC-kG07N_AR3RaKOtYaRDHWFUK7z71oVlNxH36s
ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
내가 만든 토큰이 간단하게 해제가 되어버린다 XD
난 이것을 보고 정말 충격을 먹었다..
않이.. 이게 이렇게 되면 안되는거 아닙니까?
나 같은 경우 이전 프로젝트에서 저정도의 양의 데이터를 넣었는데
(유저 식별을 위한 유저 고유 id, 교차 검증을 위한 이메일(만약을 위해), 관리자 여부)
다른 팀 같은 경우에는 페이로드 값에 이후 사용할 유저 정보를 모두 다 넣어버렸다
(이름, 성별, 핸드폰 번호, 집주소 등.. 심지어 이 팀은 배포도 했고 서비스 중이다..)
정말 나는 운이 좋게도 검증된 토큰에서 id만 쏙 뽑아
그에 해당하는 유저 정보를 뽑아내는 함수를 따로 만들어 이후에 사용했다
(다른 생각 없이 그냥 그렇게 한것이기 때문에 정말 나는 그냥 운이 좋았던 것이다)
그럼 시크릿 키는 왜 숨겨두었고, 왜 뭐 안전하다고 한거지?
심지어 공식홈페이지에서 바로 저렇게 자동문을 만들어뒀는데?
(심지어 토큰이 만료되어도 페이로드 값을 볼 수 있다 이건 jwt라이브러리의 decode함수를 써도 가능한 부분)
공식홈페이지에서는 시크릿키 조차 안다면
저기서 값을 바꿔 토큰을 재생성 할 수 있다!
하지만 시크릿키는 노출되지 않았으니 그나마 다행이라고 할 수도 있겠다..
(토큰 개봉은 쉽지만 재생성은 같은 시크릿키가 있어야 하기 때문에
탈취된 정보는 쉽게 노출되나 변조 및 재생성은 아주 어렵다!)
시크릿키와 관련된 민감한 정보를 숨기는 방법은 여기로!
그래도 페이로드는 털리기때문에 대책은 세워야 한다!
일단 내가 할 수 있는 방법은
민감한 정보, 누군가를 특정할 수 있는 값을 넣지 않는 것이다
그럼 id도 담을 수 없는 것일까? 당연히 그러면 안된다
저기 id는 정말 그 유저의 유일한 식별값이다
행여나 변조가 가능하다면 저 id를 다른 값(234)으로 바꿔서 사용하면
234번 유저의 비밀번호를 모르더라도 234번 유저로 로그인해서 기능을 쓸 수 있다!
아 그럼 어쩌란 말인가..
페이크를 넣자는 것이다
##javascript
// 담으려는 ID의 원본
const plainUserId = 5
console.log(plainUserId)
// 길이를 받아 그에 맞는 랜덤 문자열을 만들어 내는 함수
const ranStr = (len) => {
const set = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()'
let str = ''
for(i=0; i<len; i++){
str += set.charAt(Math.floor(Math.random()*set.length))
}
return str
}
const makeFakeId = (id) => {
return ranStr(3) + '-' + ranStr(3) + '-' + id + '-' + ranStr(3) + '-' + ranStr(8)
}
// 페이로드에 담기는 페이크ID
const payloadId = makeFakeId(plainUserId)
console.log(payloadId)
// 'MSe-7er-5-9*Y-P3*r8iRD'
// 토큰에 담긴 페이크ID를 다시 복구
console.log(payloadId.split('-')[2])
// 5
이렇게 페이크를 친 뒤 페이로드에 담는 것이다
## payload
{
"fake_id": MSe-7er-5-9*Y-P3*r8iRD,
"status": 0,
"iat": 1704863940,
"exp": 1704867540
}
당장 떠오르는 방법으로는 이렇게 하는 방법이 있을 것이고,
더 좋은 방법도 많을 것이다(이 부분은 앞으로 내가 더 탐구해봐야 할 문제)
앞서 말한 것처럼 내가 토큰을 발행해서 받은 프론트가
로컬스토리지든 쿠키든 저장을 하는 방식을 조금 조심히 다루거나
탈취가 당하지 않게끔 해야 하는 것
(이 부분도 결과적으로 풀스택이 될거라 내가 알아야 할 부분이고
아마 백엔드가 배포를 담당하게 될 터이니 더 방법이 있을 것 같다)
JWT는 간편하면서도 안전한 방법으로
사용자 인증 및 권한 부여를 처리하는 데에 활용할 수 있다
따라서 적절한 보안 조치를 취하고, 필요한 정보만을 담아 사용한다면
아주 강력하고 효과적으로 활용할 수 있는 도구가 될 수 있을 것이다 :)!
++
발돋움 중인 예비 개발자 입니다.
태클 및 의견 공유 언제나 환영 :D