728x90
목차
031
Refresh token
이전에는 Access token만을 이용하여 로그인 검증을 진행했었다.
☹️ Access token만 사용한 방식
- 이용자가 로그인 시도를 한다.
- 서버에서 이용자를 확인하고 토큰을 발행해준다.
- JWT토큰 인증정보를 payload에 할당하고 생성한다.
- 생성한 토큰을 클라이언트에 반환해주고, 클라이언트는 이 토큰을 가지고 있는다.
- 클라이언트가 서버에 요청을 할 때, 이 토큰도 같이 보내서 요청을 시도한다.
- 서버는 요청을 받아 해당 토큰이 유효한지 확인하고 유효한 토큰이면 요청을 처리한 후 요청에 대한 응답을 해준다.
- 토큰이 정상적인지(썩었거나 위변조가 되었는지) 확인하고 썩었으면 다시 재 로그인 시킨다.
- 썩은 토큰이 발견되면 토큰을 새로 발행한다.
💡 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을 같이 사용한 인증방식
- 클라이언트가 로그인을 시도한다.
- 서버에서 사용자를 확인하고 토큰 권한 인증 정보를 payload에 할당하고 생성한다.
- Refresh token을 만들어서 데이터베이스에 저장해두고, 2개의 토큰을 전부 클라이언트에게 전달한다.
- 클라이언트는 Refresh token, Access token 두 토큰을 모두 가지고 있는다.
- 클라이언트가 서버에 요청을 할 때, Access token을 전달해서 요청한다.
- 서버는 전달받은 토큰을 확인하고 Access token을 디코드해서 사용자 정보를 확인한다.
- 서버는 토큰이 정상적인 토큰인지, 썩은 토큰인지를 확인한다.
- 위변조된 토큰이면 새로 로그인 할 수 있게 한다.
- 기간이 경과된 토큰이면, 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이 중복되었을 경우, 에러 페이지를 띄운다.
더보기
- Access token & Refresh token
- cookie-parser
- https://inpa.tistory.com/entry/JS-%F0%9F%93%9A-%EC%BF%A0%ED%82%A4Cookie-%EB%8B%A4%EB%A3%A8%EA%B8%B0#%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8_%EC%BF%A0%ED%82%A4_%EB%AC%B8%EB%B2%95
- https://inpa.tistory.com/entry/EXPRESS-%F0%9F%93%9A-bodyParser-cookieParser-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4
- https://jw910911.tistory.com/59
728x90
'블록체인_9기 > 💚 Node.js' 카테고리의 다른 글
33강_230518_ Node.js(VScode extension으로 Mysql DB연결) (0) | 2023.05.22 |
---|---|
32강_230516_Node.js(암호화, Hash 암호화, crypto, 해시화, salt, 키 스트레칭 기법, Bcrypt) (2) | 2023.05.19 |
30강_230512_Node.js(JWT토큰, dotenv, express-session) (0) | 2023.05.16 |
29강(과제X)_230508_Node.js(MVC 패턴으로 게시판 페이지 제작) (1) | 2023.05.09 |
28강_Node.js (express, ejs, mysql2, path로 게시판 만들기) (0) | 2023.05.08 |