본문 바로가기

블록체인_9기/💚 Node.js

30강_230512_Node.js(JWT토큰, dotenv, express-session)

728x90

 

 

 

 


인증 방식의 종류

Cookie

  • Key - Value 형식의 문자열 덩어리이다.
  • 클라이언트가 웹 사이트를 방문할 경우, 그 사이트가 사용하고 있는 서버를 통해 클라이언트의 브라우저에 설치되는 작은 기록 정보 파일이다.
  • 각 사용자마다의 브라우저에 정보를 저장하니 고유 정보 식별이 가능하다.

Cookie 인증방식

  1. 클라이언트가 서버에 요청을 보낸다.
  2. 서버는 클라이언트의 요청에 대한 응답을 작성할 때, 클라이언트 측에 저장하고 싶은 정보를 응답 헤더의 Set-Cookie에 담는다.
  3. 이후 해당 클라이언트는 요청을 보낼 때마다, 매번 저장된 쿠키를 요청 헤더의 cookie에 담아 보낸다. 서버는 쿠키에 담긴 정보를 바탕으로 해당 요청의 클라이언트가 누군지 식별하거나 정보를 바탕으로 추천광고를 띄우거나 한다.

Cookie의 단점

  • 보안에 취약하다.
    • 요청 시 쿠키의 값을 그대로 보내기 때문에 유출 및 조작 당할 위험이 존재한다.
  • 용량 제한이 있어, 많은 정보를 담을 수 없다.
  • 웹 브라우저마다 쿠키에 대한 지원 형태가 다르기 때문에 브라우저간 공유가 불가능하다.
  • 쿠키의 사이즈가 커질수록 네트워크에 부하가 심해진다.

 

Sesseion

  • 쿠키의 보안이슈 때문에, 세션은 클라이언트의 민감한 인증정보(ex.비밀번호 등)를 브라우저가 아닌, 서버 측에 저장하고 관리한다.
  • 서버의 메모리에 저장하기도 하고, 서버의 로컬 파일이나 데이터베이스에 저장하기도 한다.

Session 인증방식

  1. 유저가 웹 사이트에서 로그인하면 세션이 서버메모리(또는 데이터베이스)상에 저장된다.
    1. 이때, 세션을 식별하기 위한 Session id를 기준으로 정보를 저장한다.
  2. 서버에서 브라우저에 쿠키에다가 Session id를 저장한다.
  3. 쿠키에 정보가 담겨있기 때문에 브라우저는 해당 사이트에 대한 모든 Request에 Session id를 쿠키에 담아 전송한다.
  4. 서버는 클라이언트가 보낸 Session id와 서버 메모리로 관리하고 있는 Session id를 비교하여 인증을 수행한다.

Session의 단점

  • 세션 id 자체에 유의미한 민감한 정보를 가지고 있진 않지만, 해커가 세션 id 자체를 탈취해 클라이언트인척 위장할 수 있다는 한계가 존재한다.
    • 서버에서 ip특정을 통해 해결할 수 있다.
  • 서버에서 세션 저장소를 사용하므로 요청이 많아지면 서버에 부하가 심해진다.

 

Token

  • 토큰 기반 인증 시스템은 클라이언트가 서버에 접속하면 서버에서 해당 클라이언트에게 인증되었단는 의미로 '토큰'을 부여한다.
    • 이 토큰을 유일하며, 토큰을 발급받은 클라이언트는 또 다시 서버에 요청을 보낼 때 요청 헤더에 토큰을 심어서 보낸다.
    • 서버에서는 클라이언트로부터 받은 토큰을 서버에서 제공한 토큰과 일치여부를 체크하여 인증과정을 처리하게된다.
  • 토큰은 세션과 달리 서버가 아닌 클라이언트에 저장되기 때문에 메모리나 스토리지 등을 통해 세션을 관리했던 서버의 부담을 덜 수 있다.
    • 세션 기반의 경우, 서버가 파일이나 데이터베이스에 세션정보를 가지고 있어야히고 이를 조회하는 과정이 필요하기 때문에 많은 오버헤드가 발생한다.
    • 토큰 자체에 데이터가 들어있기 때문에 클라이언트에서 받아 위조되었는지 판별만 하면 된다.

Token 인증방식

  1. 사용자가 아이디와 비밀번호로 로그인을 한다.
  2. 서버 측에서 사용자(클라이언트)에게 유일한 토큰을 발급한다.
  3. 클라이언트는 서버 측에서 전달받은 토큰을 쿠키나 스토리지에 저장해두고, 서버에 요청할 때마다 해당 토큰을 HTTP 요청 헤더에 포함시켜 전달한다.
  4. 서버는 전달받은 토큰을 검증하고 요청에 응답한다. 토큰에는 요청한 정보가 담겨있기에 서버는 DB를 조회하지 않고 누가 요청하는지 알 수 있다.

Token의 단점

  • 쿠키/세션과 다르게 토큰 자체의 데이터 길이가 길어, 인증요청이 많아질수록 네트워크 부하가 심해질 수 있다.
  • Payload 자체는 암호화되지 않기 때문에 유저의 중요한 정보는 담을 수 없다.
  • 토큰을 탈취당하면 대처하기 어렵다.(따라서 사용 기간 제한을 설정하는 식으로 극복한다.)

 

 

 


JWT 토큰

  • JWT : JSON Web Token
  • 인증에 필요한 정보들을 암호화 시킨 JSON 토큰을 의미한다.
  • JWT 기반 인증은 JWT토큰(Access Token)을 HTTP 헤더에 실어 서버가 클라이언트를 식별하는 방식이다.
  •  토큰 내부에는 위변조 방지를 위해 개인키를 통한 전자서명이 들어있다.
    • 따라서, 사용자가 JWT를 서버로 전송하면 서버는 서명을 검증하는 과정을 거치게 되며 검증이 완료되면 요청한 응답을 돌려준다.
      • 유저가 로그인을 요청하면 서버에서 유저의 정보를 가지고 정상적인 루트로 로그인을 요청한 유저면 토큰을 발급해서 전달해준다.
      • 유저가 서버에 요청을 할 때, JWT 토큰을 포함해서 요청을 하면 서버가 요청을 받고 토큰이 썩은 토큰인지 검사를 해서 착한 토큰이면 요청한 작업을 처리해주고 응답해준다.
  • 웹 표준으로 두 객체의 JSON 객체를 사용해서 정보를 안정성 있게 전달해준다.
  • 사용할 정보를 자체적으로 가지고 있다.
    • JWT로 발급한 토큰은 기본정보(유저의 정보 프로필)와 토큰이 정상인지 검증(전자서명.Signature)을 포함하고 있다.
  • 주로 로그인이 정상적인지 회원 인증 권한에서 사용한다.
  • JWT는 주로 로그인이 정상적인지 회원 인증 권한에서 사용한다.
  • JWT를 사용하는 이유: 안정성 있게 정보를 전달해서 요청할 수 있다.
  • JWT를 생성하면 사용할 모듈이 인코딩과 해싱 작업을 해준다
    • HMAC: 해싱 시법을 적용해서 메시지의 위.변조를 방지하는 기법
    • SHA256: 임의의 길이 메시지를 256 비트의 축약된 메시지로 만들어내는 해시 알고리즘

 

JWT 구조

  • ' . ' 구분자로 나누어지는 세 가지 문자열의 조합이다.
  • ' . ' 을 기준으로 Header, Payload, Signature를 의미한다. 

Header

  • 타입과 알고리즘의 정보를 가지고 있다.
let header = {
    // 사용하는 해싱 알고리즘
    alg : "SHA256",
    // 토큰의 타입
    type : "JWT"
}
  • alg: 서명 암호화 알고리즘
    • ex. HMAC, SHA256, RSA
  • type: 토큰 유형

Payload

  • 유저의 정보와 만료기간이 포함된 객체를 가지고 있다.
  • 서버와 클라이언트가 주고 받는 시스템에서 실제로 사용될 정보에 대한 내용을 담고 있는 섹션이다.
let payload = {
    // 토큰의 이름 제목
    sub : "2342",
    // 유저의 이름 (유저 프로필)
    name: "affsfge",
    // 토큰이 발급된 시간. 발급된지 얼마나 지났는지
    lat: "1433"
}
  • 정해진 타입은 없지만, 대표적으로 Registered claims, Public claims, Private claims 세 가지로 나뉜다.
    • Registered claims: 미리 정의된 클레임이다.
      • iss(issuer): 발행자
      • exp(expireation time): 만료시간
      • sub(subject): 제목
      • iat(issued At): 발행시간
      • jti: JWI ID
    • Public claims: 사용자가 정의할 수 있는 클레임 공개용 정보 전달을 위해 사용
    • Private claims: 해당하는 당사자들 간에 정보를 공유하기 위해 만들어진 사용자 지정 클레임
      • 외부에 공개되도 상관없지만 해당 유저를 특정할 수 있는 정보들을 담는다.

Signature

  • header, payload를 인코딩하고 합쳐서 해싱해 비밀키로 만든다.
  • 시그니처의 구조
    • 헤더, 페이로드와 서버가 갖고 있는 유일한 key 값을 합친 것을 헤더에서 정의한 알고리즘으로 암호화한다. 
    • Signature = Header(Base64Url) + . + Payload(Base64Url) + server's key
// 비밀키 생성
let signature = HMACSHA256(BASE64URL(header) + BASE64URL(payload));

 

JWT.sign( )

  • 토큰을 생성할 수 있는 메소드이다.
  • sign()메소드 구조
    • jwt.sign(payload, secret Key, [ options, callback ])
      • payload: 아이디, 비밀번호 등 사용자 정보가 들어간 object이다. 
      • secret Key: 여러가지 복잡한 문자열로 되어있는 키이다.
      • options: 토큰에 대한 여러가지 정보를 설정한다.
        • expiresIn: 토큰을 유지시킬 유효시간, 만료시간
        • issuer: 토큰을 발급한 사람 
        • 등의 정보가 들어간다.
      • callback: 토큰의 생성결과를 4번째 인자의 콜백함수로 받을 수 있으므로 넣어주는 함수이다.

 

JWT.verify( )

  • 토큰의 유효성을 검사할 수 있는 메소드이다.
  • verify( )메소드 구조
    • jwt.verify(token, key, callback)
      • token: 클라이언트에게서 받은 토큰
      • key: 토큰 생성 시 사용했던 secret Key
      • callback: 유효성 검사를 처리할 callback함수
        • err: 에러 내용 객체
        • decided: 해석된 객체
jwt.verify(token, key, (err, decoded)=>{
    // 유효하지 않은 토큰
    if(err){
        console.log("썩은 토큰");
        res.send("토큰이 썩었거나, 변조된 것이다.");
    }
    // 해석된 객체. 유효한 토큰
    else{
        console.log(decoded);
        res.send(decoded);
    }
})

 

 

 


dotenv

  • 환경변수를 .env 파일에 저장하고 process.env로 로드하는 의존성 모듈이다.
  • 우리가 개발을 하는 과정에서 서버주소, 고유 API KEY값 등 필요한 정보들을 저장하게 된다. 이러한 정보들은 민감한 정보에 해당하여 보안이 이루어져야 하는데, .env에 담아두면 보안을 유지할 수 있게된다.
    • 민감한 정보들이 오픈소스(깃허브 등)에 공개될 경우 해킹을 당하거나 보안적인 면에서 위험할 수 있다.
    • 따라서 dotenv를 이용하여 환경변수 파일을 외부에 만들어 접근할 수 있게 하므로써 보안을 유지할 수 있는 것이다.

dotenv 사용법

dotenv 설치

터미널에서 npm 으로 dotenv를 설치한다.

PS D:\test> npm i dotenv

 

dotenv 경로지정

  • env를 사용하고자 하는 파일안에 dotenv를 부른다.
  • config( ): 현재 디렉토리의 ' .env '파일을 자동으로 인식하여 환경변수를 세팅한다.
  • config( 경로 ): 원하는 ' .env '파일의 위치를 직접 지정하여 세팅할 수 있다.
const dot = require("dotenv").config();

 

dotenv 작성

  • .env 파일을 루트경로에 생성한 후 안에 들어갈 텍스트는 반드시 "Key = Value"의 형식으로 적어준다.
KEY = "qwer1234"

 

dotenv 사용

  • ' process.env.XXX ' 의 형태로 작성한다.
const KEY = process.env.KEY;

 

 

 


Token 발행하기_JWT / dotenv 활용

#. 폴더의 경로는 다음과 같다.

 

1.  app.js_필요한 모듈을 설치하고 불러온다. (express, path, jwt, dotenv, ejs)

  • 필요한 모듈을 설치한다.
PS D:\test> npm i express jsonwebtoken dotenv ejs
  • 사용할 모듈을 불러온다.
const express = require("express");
const path = require("path");
// JWT 모듈 가져오기
const jwt = require("jsonwebtoken");
// dotenv 모듈 가져오기. 가져오면서 config 메소드 실행
// config의 매개변수가 비어있으니, 현재 디렉토리의 ' .env '파일을 읽어온다.
const dot = require("dotenv").config();

2. .env_키를 생성한다.

  • ' .env ' 파일을 생성하여 해당 파일에 사용할 키와 키 값을 작성한다.
KEY = "mykey"

3. app.js_서버 인스턴스를 생성, views 파일경로 설정, view엔진 ejs파일 설정 등 설정 

  • 서버 인스턴스를 생성한다.
const app = express();
  • views 파일 경로 설정
app.set("views", path.join(__dirname, "page"));
  • view엔진 ejs파일 설정
app.set("view engine", "ejs");
  • body객체를 사용할 수 있도록 설정한다.
app.use(express.urlencoded({extended : false}));

4. main.ejs / app.js_메인페이지를 생성하고, app.js에서 render후 post된 값을 받아온다.

  • main.ejs_main페이지를 생성한다.
<body>
    <h1>로그인 페이지</h1>
    <form action="/login" method="post">
        <input type="text" name="id" placeholder="아이디">
        <input type="text" name="id" placeholder="비밀번호">
        <button>로그인</button>
    </form>
</body>
  • app.js_get메소드를 이용하여 페이지를 렌더하고 post메소드를 이용하여 post된 값을 받아온다.
app.get("/",(req,res)=>{
    res.render("main");
})

app.post('/login', (req,res)=>{
    // 로그인을 정상적으로 했다 가정하고 토큰을 발급한다.
    // 유저 정보는 객체로 만들어주자.
    const name = "user1";
    const KEY = process.env.KEY;
    // sign()메소드로 JWT 토큰을 생성
    let token = jwt.sign({
        // 타입은 JWT
        type : "JWT",
        // 유저 이름
        name : name
    }, KEY, {
        // 토큰을 유지시킬 유효시간, 만료시간.
        expiresIn : "5m",        // 5분 유지시킬 토큰
        // 토큰 발급한 사람
        issuer : "user1" 
    })
    res.send(JSON.stringify(token));
    // 결과: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiSldUIiwibmFtZSI6InVzZXIxIiwiaWF0IjoxNjgzODU4NTcwLCJleHAiOjE2ODM4NTg4NzAsImlzcyI6InVzZXIxIn0.ygd2xoVm2kX_y5ikXxAfUIupDsYa7MH7a5T_oPJePJQ"
        // 점( . )을 기준으로 나뉜다.
        // header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
        // payload: eyJ0eXBlIjoiSldUIiwibmFtZSI6InVzZXIxIiwiaWF0IjoxNjgzODU4NTcwLCJleHAiOjE2ODM4NTg4NzAsImlzcyI6InVzZXIxIn0
        // 서명(signature): ygd2xoVm2kX_y5ikXxAfUIupDsYa7MH7a5T_oPJePJQ
})

5. app.js_서버를 대기한다.

app.listen (8080,()=>{
    console.log("서버가 잘 열렸어요!")
})

 

 

 

 


express - session

  • 세션관리용 미들웨어
  • 로그인 등의 이유 세션을 구현하거나, 특정 사용자를 위한 데이터를 임시적으로 저장해둘 때 매우 유용하다.
  • 세션은 사용자별로 req.session 객체 안에 유지된다.
app.use(session({
    secure: ture,	// https 환경에서만 session 정보를 주고받도록처리
    secret: process.env.COOKIE_SECRET, // 암호화하는 데 쓰일 키, 세션을 발급할 때 사용할 키.
    resave: false, // 세션을 언제나 저장할지 설정함, 세션이 변경되거나 저장할 때나 불러올 때 다시 저장할지 여부를 결정
    saveUninitialized: true, // 세션을 저장할 때 초기화할지 여부를 결정
    cookie: {	//세션 쿠키 설정 (세션 관리 시 클라이언트에 보내는 쿠키)
      httpOnly: true, // 자바스크립트를 통해 세션 쿠키를 사용할 수 없도록 함
      Secure: true
    },
    name: 'session-cookie' // 세션 쿠키명 디폴트값은 connect.sid이지만 다른 이름을 줄수도 있다.
}));

 

 

 


Token 발행 및 회원의 토큰정보 확인_JWT / express-session / dotenv

  • JWT와 dotenv를 활용하여 Token을 발행하고 express-session을 활용하여 세션에 해당 토큰을 저장해 유효성을 검사한다.

 

#. 폴더의 경로는 다음과 같다.

 

1.  app.js_필요한 모듈을 설치하고 불러온다. (express, path, jwt, dotenv, ejs, express-session)

  • 필요한 모듈을 설치한다.
PS D:\test> npm i express jsonwebtoken dotenv ejs express-session
  • 사용할 모듈을 불러온다.
const express = require("express");
const path = require("path");
const session = require("express-session");

2. app.js_서버 인스턴스를 생성,  views 경로 설정, view엔진 ejs사용, body객체 사용을 설정한다.

  • 서버 인스턴스 생성
const app = express();
  • views경로 설정 및 view엔진 ejs 사용 설정
app.set("views", path.join(__dirname,"page"));
app.set("view engine", "ejs");
  • body객체 사용
app.use(express.urlencoded({ extended : false }));

3. .env_ 보안을 위한 키 값을 생성한다.

  • ' .env ' 파일을 생성하여 해당 파일에 사용할 키와 키 값을 작성한다.
KEY = "mykey"
KEY2 = "mykey2"

4. app.js_session 데이터를 메모리에 저장한다.

  • session 데이터를 메모리에 저장한다.
app.use(session({
    // 세션을 발급할 때 사용할 키. 이것도 나중에는 소스코드에 노출 안되게 바꿔놓자.
    secret : process.env.KEY2,
    // 세션이 변경되거나 저장할 때나 불러올 때 다시 저장할지 여부를 결정
    resave : false,
    // 세션을 저장할 때 초기화할지 여부를 결정
    saveUninitialized : true,
    name : "session-cookie"
}));

 

  • 해당 방식은 session 정보를 디스크나 DB에 저장하는 것보다 훨씬 빠른 반응속도를 보인다는 장점이 있지만, 그만큼  메모리에 적제되는 session양이 많아졌을  때는 부하가  심한 방식이다.
    • 따라서, 메모리에 적제되는 양을 조절하거나, 세션이 유지되는 기간을 조절하는것이 중요하다.

5. page.js_루트경로의 페이지 render를 설정한다.

  • 사용할 모듈을 불러온 후, 루트경로에서 render될 페이지를 설정한다.
const express = require("express");
const router = express.Router();

router.get("/", (req,res)=>{
    res.render("page");
})

module.exports = router;

6. Token.js_Token을 생성한다.

  • 토큰 생성에 필요한 모듈을 불러온다.
// 익스프레스 받아서 실행하고 익스프레스 안에 있는 라우터 객체 실행
const router = require("express").Router(); 

const dot = require("dotenv").config();
const jwt = require('jsonwebtoken');
  • '/login'페이지에서 post로 값을 받아 Token을 생성한다.
router.post("/login", (req,res)=>{
    const name = "mr.hong";
    const key = process.env.KEY;

    //토큰을 생성하여 'token'변수에 담는다.
    let token = jwt.sign({
        type : "JWT",
        name : name,
    }, key, {
        // 토큰의 유효시간
        expiresIn : "3m",
        // 토큰 발급자
        issuer : name
    } )
    // 요청받은 세션의 토큰에는 token이 담긴다.
    req.session.token = token;
    res.render("page2");
})

7. page.ejs / page2.ejs_동작을 실행할 수 있는 페이지를 생성한다.

  • 버튼을 클릭하면 사용자가 로그인을 진행하여, 토큰을 생성한다. 생성한 토큰은 세션에 저장된다.
<body>
    <h1>첫 로그인 페이지</h1>
    <form action="/login" method="post">
        <button>사용자 로그인</button>
    </form>
</body>

 

  • 버튼을 클릭하면 토큰의 유효성 검사를 처리하고, 에러를 띄우거나(썩은 토큰일 경우) 복호화된 토큰(유효한 토큰)을 보여준다.
<body>
    <form action="/userVerify" method="post">
        <button>사용자 토큰 정보</button>
    </form>
</body>

 

8. verify.js_Token의 유효성을 검사하고 복호화된 토큰을 노출한다.

  • 유효성 검사를 위해 필요한 모듈을 불러온다.
  • 루트경로에서 post받은 세션 토큰의 유효성을 검사한다.
    • 토큰이 시간이 지나 썩거나, 위변조가 되었을 경우 err로 넘어간다.
    • 유효한 토큰일 경우 해석된 객체가 나타난다.
router.post('/', (req,res)=>{
    const token = req.session.token;
    const key = process.env.KEY;
    // 토큰이 유효한지 검증
    jwt.verify(token, key, (err, decoded)=>{
        if(err){
            console.log("썩은 토큰");
            res.send("토큰이 썩었거나, 변조된 것이다.");
        }
        else{
            // 해석된 객체
            console.log(decoded);
            res.send(decoded);
        }
    })
})

9. app.js_라우터 설정을 한 후, 서버를 대기 시킨다.

  • 라우터 설정을 불러온다.
const pageRouter = require("./routers/page");
const tokenRouter = require("./routers/token");
const verifyRouter = require("./routers/verify");

app.use(pageRouter);
app.use(tokenRouter);
app.use("/userVerify", verifyRouter);
  • 서버를 대기시킨다.
app.listen(8000, ()=>{
    console.log("JWT 서버 잘 열림!")
})

 

결과_토큰이 썩었을 경우

결과_토큰이 유효할 경우

 

 

 

 

 

 

 

 

728x90