본문 바로가기

블록체인_9기/💚 Node.js

31강_230515_Node.js(Refresh token, Access token, cookie-parser)

728x90

 

 

 

 

 


Refresh token

이전에는 Access token만을 이용하여 로그인 검증을 진행했었다.

☹️ Access token만 사용한 방식

  1. 이용자가 로그인 시도를 한다.
  2. 서버에서 이용자를 확인하고 토큰을 발행해준다.
    1. JWT토큰 인증정보를 payload에 할당하고 생성한다.
  3. 생성한 토큰을 클라이언트에 반환해주고, 클라이언트는 이 토큰을 가지고 있는다.
  4. 클라이언트가 서버에 요청을 할 때, 이 토큰도 같이 보내서 요청을 시도한다.
  5. 서버는 요청을 받아 해당 토큰이 유효한지 확인하고 유효한 토큰이면 요청을 처리한 후 요청에 대한 응답을 해준다.
  6. 토큰이 정상적인지(썩었거나 위변조가 되었는지) 확인하고 썩었으면 다시 재 로그인 시킨다.
    1. 썩은 토큰이 발견되면 토큰을 새로 발행한다.

 

💡 Refresh token이란?

  • Access token처럼 사용자를 인증하는 방식으로 사용하는 토큰이 아닌, 새로운 Access token을 생성하는 용도로만 사용한다.

 

🤔 Refresh token을 같이 사용하면?

  • Access token만 사용한 경우에는 보안이 취약하다.
    • 해커가 Access token을 탈취했을 때, 토큰의 유효시간이 끝날 때까지는 막을 수 없다.
    • 그렇기 때문에 해당 토큰의 유효시간을 짧게 주는 것이다.
      • 유효시간이 짧으면 사용자가 로그인을 계속 해야하는 번거로움이 발생해 서비스 이용이 힘들다.
  • Refresh token의 유효시간은 길게주고, Access token의 유효시간을 짧게준다.
    • 사용자는 Access token과 Refresh token 둘 다 서버에 전송하여 Access token으로 인증하고, 만료되었을 시 Refresh token으로 새로운 Access token을 발급받는다.
  • 정상적인 클라이언트의 경우, Access token의 유효시간이 지나더라도, Refresh token을 사용하여 새로운 Access token을 생성 후, 사용할 수 있다.
    • 하지만, 해커의 경우에는 탈취한 Access token의 유효시간이 지나면 사용할 수 없다.
  • 두 토큰 모두 유효시간이 경과되면, 재 로그인을 하여 새로운 토큰을 발급받아야 한다.

 

😊 Access token과 Refresh token을 같이 사용한 인증방식

  1. 클라이언트가 로그인을 시도한다.
  2. 서버에서 사용자를 확인하고 토큰 권한 인증 정보를 payload에 할당하고 생성한다.
    1. Refresh token을 만들어서 데이터베이스에 저장해두고, 2개의 토큰을 전부 클라이언트에게 전달한다.
  3. 클라이언트는 Refresh token, Access token 두 토큰을 모두 가지고 있는다.
  4. 클라이언트가 서버에 요청을 할 때, Access token을 전달해서 요청한다.
  5. 서버는 전달받은 토큰을 확인하고 Access token을 디코드해서 사용자 정보를 확인한다.
  6. 서버는 토큰이 정상적인 토큰인지, 썩은 토큰인지를 확인한다.
  7. 위변조된 토큰이면 새로 로그인 할 수 있게 한다.
  8. 기간이 경과된 토큰이면, Refresh token으로 다시 Access token을 재발급 해준다.

 

 

 


cookie-parser

  • 요청된 쿠키를 쉽게 추출할 수 있도록 도와주는 미들웨어이다.
    • express의 req객체에 cookies속성이 부여된다.
  • 요청과 함께 들어온 쿠키를 해석하여 곧바로 req.cookies객체로 만든다.
  • ' res.cookie( ) ' 를 이용하여 쿠키를 생성할 수 있다.

 

 

 


Refresh token & Access token 생성_cookie 사용

  • 저장된 계정 값으로 로그인을 시도하면, Refresh token과 Access token이 발행된다.
    • 쿠키에 Refresh token이 담기게 되고, Refresh token이 유효한 상태면 Access token을 재발급 할 수 있다.

 

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

 

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

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

2. app.js_서버 인스턴스를 생성, views 파일경로, cookies 사용 등 설정 

  • 서버 인스턴스를 생성한다.
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}));
  • cookie-parser 사용
app.use(cookies());
  • 루트 경로를 ' /login '으로 설정
app.get('/',(req,res)=>{
    res.render("login");
})

 

3. join.ejs / login.ejs_동작을 실행할 수 있는 페이지를 생성한다.

  • login.ejs_로그인을 성공하면 Refresh token과 Access token을 발행, 쿠키를 생성한다.
<body>
    <form action="/login" method="post">
        <label for="">아이디</label> <br>
        <input type="text" name="user_id"> <br>
        <label for="">비밀번호</label> <br>
        <input type="text" name="user_pw"> <br>
        <button>로그인</button>
    </form>
</body>
  • join.ejs_Refresh token이 유효하면 Access token을 새롭게 발행한다.
<body>
    <form action="/refresh" method="post">
        <p><%= AccessToken %></p>
        <button>로그인 유지(Access Token 재발급)</button>
    </form>
</body>

4. app.js_진행을 위해 더미로 계정정보 생성, 로그인 시 Refresh token, Access token, 쿠키 생성

  • 더미로 회원가입 한 사람의 정보 객체 생성
// 더미로 회원가입 한 사람의 정보객체 생성
const user = {
    id: "weee",
    pw: "123"
}
  • ' /login '에서 post받아온 요청으로 Refresh token, Access token 생성 후, Refresh token을 값으로 가지는 쿠키생성
app.post("/login", (req,res)=>{
    // 요청 객채의 body에 user_id, user_pw를 구조분해할당으로 가져옴
    const {user_id, user_pw} = req.body;
    if(user_id === user.id && user_pw === user.pw){
        // access token 발급
        const AccessToken = jwt.sign({
            // payload
            id : user.id
        }, process.env.ACCESS_TOKEN_KEY,{
            expiresIn : "20s"
        });
        // refresh token 발급
        const refreshToken = jwt.sign({
            id : user.id
        }, process.env.REFRESH_TOKEN_KEY,{
            expiresIn : "1d"
        })
        //쿠키 생성
        //refresh_token이라는 이름의 refreshToken의 값이 담긴 쿠기가 생성되고 이 쿠키의 만료시간은 1일이다.
        res.cookie("refresh_token", refreshToken, {maxAge : 24 * 60 * 60 * 1000});
        res.render("join", {AccessToken});
    }
})

5. app.js_Refresh token확인 후 Access token 새로 발급

  • ' /refresh ' 에서 post 받아온 요청으로 쿠키를 옵션 체이닝으로 확인 후, 해당 쿠키가 존재할 경우 verify로 쿠키의 값과 Refresh token의 값을 비교하여 유효성을 검사한다. 문제가 없을 경우 새로운  Access token을 발급한다.
app.post("/refresh", (req,res)=>{
    // 옵션 체이닝. 뒤에 오는 키값이 있는지 먼저 확인하고 값을 호출해서 반환
    // 그래서 크래쉬 방지
    if(req.cookies?.refresh_token){
        // console.log("쿠키",req.cookies)
        const refreshToken = req.cookies.refresh_token;
        jwt.verify(refreshToken, process.env.REFRESH_TOKEN_KEY, (err, decode)=>{
            // err가 있으면 다시 로그인 하세요!
            if(err){
                // 쿠키에 refresh_token이 존재하지만, 그 값이 변조되었을 경우
                res.send("로그인을 다시 해주세요!");
            }
            else{
                const AccessToken = jwt.sign({
                    id : user.id
                }, process.env.ACCESS_TOKEN_KEY,{
                    expiresIn : "20s"
                })
                res.render("join", {AccessToken});
            };
        })
    }
    // 쿠키에 refresh_token 자체가 없거나 값이 없을 경우
    else{
        res.send("로그인 해주세요!")
    }
})

6. app.js_서버 대기하기

  • listen 메소드를 사용하여 서버를 대기상태로 만든다.
app.listen(8000, ()=>{
    console.log("서버가 잘 열렸어요!")
})

 

결과_로그인을 성공했을 경우

결과_Access token 재발급을 성공했을 경우

  •  Access token 값이 변한것을 확인할 수 있다.

결과_Refresh token가 없거나, 해당 토큰이 존재하지만 값이 없을 경우

결과_Refresh token의 값이 변조되었을 경우

 

 

 


Refresh token & Access token 생성_Mysql 사용

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

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

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

2. app.js_서버 인스턴스를 생성, views 파일경로, body객체 사용 등 설정 

  • 서버 인스턴스를 생성한다.
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}));

3. config.js_Mysql을 연결한다.

  • createPool 사용을 위해 promise api를 사용한다.
const mysql2 = require("mysql2/promise");
  • mysql을 연결한다.
const mysql = mysql2.createPool({
    user : "root",
    password : "000000",
    multipleStatements : true,
    database : "test4"
})

4. .env_controller 단 에서 사용할 키와 키 값을 설정한다.

  • controller단에서 사용할 키를 작성한다.
SESSION_KEY = dnfljsnbvfskj
ACCESS_TOKEN_KEY = edjfbwnbowpig
REFRESH_TOKEN_KEY = dbgvjsnamcingvrwn

5. app.js_session을 사용할 수 있게 해주는 미들웨어를 설정한다.

app.use(session({
    // 세션 발급에 사용할 비밀 키. 노출 안되도록 env로 만들자
    secret : process.env.SESSION_KEY,
    // 세션을 저장하고 불러올 때 세션을 다시 저장할지 여부
    resave : false,
    // 세션을 저장할 때 초기화 여부 
    saveUninitialized : false
}));

6. userModel.js_회원가입, 로그인을 진행할 모델을 설정한다.

  • mysql에 연결하여 테이블을 생성, 추가, 수정, 삭제 등의 작업을 수행해야 하므로, mysql을 연결한 config.js를 불러온다.
const mysql = require("./config");
  • userInit: 테이블을 생성하는 함수. id, user_id, user_pw, refresh 컬럼을 생성한다.
    • id: 계정의 고유 값.
    • user_id: 계정의 id 값.
    • user_pw: 계정의 pw 값. 
    • refresh: 계정의 refresh 토큰 값. 
// 테이블을 생성해주는 함수
exports.userInit = async ()=>{
    try {
        // users 테이블이 있는지 확인
        await mysql.query("SELECT * FROM users");
    } catch (error) {
        const sql = "CREATE TABLE users(id INT AUTO_INCREMENT PRIMARY KEY, user_id VARCHAR(20), user_pw VARCHAR(20), refresh VARCHAR(255))"
        await mysql.query(sql);
    }
}

 

  • userSelect: user_id로 계정이 존재하는지를 검색한다.
exports.userSelect = async (user_id)=>{
    try {
        const [result] = await mysql.query("SELECT * FROM users WHERE user_id = ?",[user_id]);
        return result[0];
    } catch (error) {
        console.log(error);
    }
}
  • userRefresh: user_id로 해당 계정을 확인하고 refresh 값을 insert 한다.
exports.userRefresh = async (user_id, refresh)=>{
    try {
        await mysql.query("UPDATE users SET refresh = ? WHERE user_id = ?", [refresh,user_id]);
    } catch (error) {
        console.log(error);
    }
}

7. userController.js_모델과 라우터를 이어주는 함수를 설정한다.

  • Signup: 회원가입을 진행하는 함수
// 회원가입
exports.Signup = async (req,res)=>{
    const { user_id, user_pw } = req.body;
    try {
        await userInsert(user_id,user_pw);
        res.redirect("/login");
    } catch (error) {
        console.log(error);
    }
}
  • Login: 로그인을 진행하는 함수
// 로그인
exports.Login = async (req,res)=>{
    const {user_id, user_pw} = req.body;
    try {
        const data = await userSelect(user_id);
        // 유저 조회가 되었으면 ' user_id '가 있겠죠?
        if(!data?.user_id){
            return res.send("아이디 없음");
        }

        if(data.user_pw !== user_pw){
            return res.send("비밀번호 틀림");
        }

        // 여기까지 통과하면 로그인 성공!
        // access token 발급
        const acessToken = jwt.sign({
            user_id : data.user_id,
            mail : "user_1@naver.com",
            nick : "user1"
        },process.env.ACCESS_TOKEN_KEY,{
            expiresIn: "5s"
        });
        // refresh token 발급
        const refreshToken = jwt.sign({
            user_id : data.user_id,
        }, process.env.REFRESH_TOKEN_KEY,{
            expiresIn: "20s"
        });

        await userRefresh(user_id, refreshToken);

        req.session.acess_Token = acessToken;
        req.session.refresh_Token = refreshToken;
        res.send({access : acessToken, refresh : refreshToken});

    } catch (error) {
        console.log(error);
    }
}
  • verifyLogin: 유저 토큰 검증
// 유저 토큰 검증
exports.verifyLogin = async (req,res, next)=>{
    // next 함수를 실행시켜주면 다음 미들웨어로 이동
    const { acess_Token, refresh_Token } = req.session;
    jwt.verify(acess_Token, process.env.ACCESS_TOKEN_KEY, (err, acc_decoded)=>{
        if(err){
            // Access 토큰이 썩은 토큰이면
            jwt.verify(refresh_Token, process.env.REFRESH_TOKEN_KEY, async(err, ref_decoded)=>{

                if(err){
                    console.log("refresh token 만료",err)
                    res.send("다시 로그인 하세요!")
                }
                else{
                    const data = await userSelect(ref_decoded.user_id);
                    if(data.refresh == refresh_Token){
                        const accessToken = jwt.sign({
                            user_id: ref_decoded.user_id
                        }, process.env.ACCESS_TOKEN_KEY,{
                            expiresIn : "5s"
                        })
                        req.session.acess_Token = accessToken;
                        console.log("access token 재발급");
                        next();
                    }
                    else{
                        res.send("중복 로그인 방지")
                    }
                }
            })
        }
        //엑세스 토큰이 유효하면
        else{
            console.log("로그인 정상 유지 중!")
            next();
        }
    })
}

8. joinRouter.js_회원가입 시 진행되는 라우터를 설정한다.

  • express의 Router메소드를 실행하여 변수 router에 반환되는 라우터 값을 받는다.
const router = require("express").Router();
  • 컨트롤러단에서 설정한 singup 함수를 받는다.
const {Signup} = require("../controllers/usersController");
  • '/join'으로 접속 시, ' join.ejs '의 페이지가 열리도록 render한다. 
router.get('/',(req,res)=>{
    res.render("join");
})
  • ' join.ejs '에서 post받은 요청을 컨트롤러 단에서 받은 signup 함수로 받아 실행하도록 한다.
router.post('/',Signup);

9. loginRouter.js_로그인 시 진행되는 라우터를 설정한다.

  • express의 Router메소드를 실행하여 변수 router에 반환되는 라우터 값을 받는다.
const router = require("express").Router();
  • 컨트롤러단에서 설정한 Login, verifyLogin 함수를 받는다.
const { Login, verifyLogin } = require("../controllers/usersController");
  • '/login'으로 접속 시, ' login.ejs '의 페이지가 열리도록 render한다. 
router.get('/',(req,res)=>{
    res.render('login');
})
  • ' login.ejs '에서 post받은 요청을 컨트롤러 단에서 받은 Login 함수로 받아 실행하도록 한다.
router.post('/', Login)
  • ' login.ejs '에서 '/mypage'로 넘어가는 버튼을 클릭하면 컨트롤러 단에서 받은 verifyLogin 함수를 받아 실행하고 콜백함수로 넘어가 ' res.send'를 실행한다.
// 로그인 상태에서 요청해야 하는 작업은
router.get("/mypage", verifyLogin, (req,res)=>{
    res.send("로그인 상태고 마이페이지 보여줄게");
})

10. app.js_회원가입, 로그인의 라우터를 불러와 app.use를 이용하여 실행시킨다.

  • 라우터 단에 joinRouter와 loginRouter를 불러온다.
const joinRouter = require("./routers/joinRouter");
const loginRouter = require("./routers/loginRouter");
  • url 주소에 '/join'을 입력하면, joinRouter가 실행되고, '/login'을 입력하면 loginRouter가 실행된다. 
app.use('/join', joinRouter);
app.use('/login', loginRouter);

11. app.js_서버를 대기시킨다.

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

 

결과_회원가입한 계정으로 로그인을 성공했을 경우

결과_Refresh token이 썩었을 경우 (기간만료)

결과_Access token이 썩었을 경우 (기간만료)

결과_Access token이 썩었을 경우 (기간만료), Access token을 재발급 해준다.

결과_Refresh token이 중복되었을 경우, 에러 페이지를 띄운다.

 

 

 

 

 

 

 

728x90