jwt / introduction & access token
Introduction
JWT를 아주 예전에 접한적이 있다.
지금도 코찔이지만 그때 난 코찔찔찔이였기때문에 이해가 어려웠는데..
훈련소에서 cognito와 google oauth로 직접 로그인을 구현해보면서 자세히 알게됐었다.
하지만 이것도 클라우드서비스를 사용한거라서.. 개발단에서만 JWT를 해본적은 없으니..
실제 만든 서비스에선 JWT포스팅들이 잘못된부분도 많고 이미 널리 알려진 auth방식이라 난 개발과 인프라단에서 나만의 방식으로 보안체계를 구성했었어서(솔찍히 졸라 독특함 ㅋㅋ) 실제로 개발단에서 라이브러리로 해본적은 없다.
때문에 이번에 간단하게 JWT에 대해서 포스팅해보려고 한다.
직접 구현은 vue js와 nodejs express로 프론트백을 구현하여 JWT를 테스트해봤다.
# 개념
다른 사이트들 글을 읽어봤는데 인증 인가 뭐 이러면서 그러면서 꼭 옆에 영어로 괄호치고 적어놓음.
걍 나는 내가 이해하기 편하게 이렇게 클라이언트가 내 사이트 front파일 다운받고 거기서 로그인을 하면, 서버에서 티켓주는 방식으로 생각한다.
티켓을 자세히 보면, 몇시까지 이 티켓을 쓸 수 있는지, 티켓이 제대로 된 티켓인지 확인을 위한 인증바코드, 그리고 간단한 고객정보(주민번호같은 민감한건 없는)가 적혀있다.
이제 이 티켓을 가지고 서비스를 이용하려면
뭐 이런식으로 쓸 수 있다. 저 빨간색 화살표는 api요청과 응답이다.
게시판을 보려고할때 로그인이 보통 필요없으니까 애초에 티켓 제시도 안한다(프론트에서 설정)
백단에선 보드에있는 게시물들을 db에서 조회해서 응답해주면된다.
mypage같은 경우는 로그인해야 되니까 티켓을 제시해야한다. 물론 front의 api에서 토큰을 같이 보내도록 코딩해야함.
토큰이 맞으면 mypage에 대한 데이터를 보내준다. 참고로 중요한점은 server컴퓨터는 db를 조회하지 않았다.
마지막으로 글쓰는 기능 쓰려는데 로그인이 필요하니까 티켓을 제시한다.
근데 티켓이 이상하다? 그럼 글쓰기 기능 못하는 응답을 주는거다.400번대 응답을 줄거임.
뭐 이런식으로 개념을 잡으면 된다.
# 토큰 발급을 코드로 대충 구현해보자
일단 백단 코드부터
const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");
패키지 불러와주고,
dotenv는 뭐 기본적으로 다 알겠지만 비밀스러운 정보를 저장하고 사용하는데 쓰게되는데,
난 key라는 정보를 저장해줬다. 이게 토큰 만들어주고 검증하고 이럴때 쓰게되는거임. 중요하니까 이렇게 환경변수로 관리해주는거다.
그리고 login 이라는 api를 만들어줬다.
app.post("/api/login", (req, res, next) => {
const key = process.env.KEY;
// 받은 요청에 아이디 비번으로 디비 조회 한번 한다.
const nickname = "nurd customer";
let token = "";
token = jwt.sign(
{
type: "JWT",
nickname: nickname,
},
key,
{
expiresIn: "1m", // 1분후 만료
issuer: "nurd",
}
);
console.log(token);
// response
return res.status(200).json({
code: 200,
message: "token is created",
token: token,
});
});
처음에 로그인할때는 req에서 받은 아이디 비밀번호로 db조회를 하고 회원에 맞는 닉네임을 가져온다고 대충 생각해서 저렇게 헀다.
디비까지 연결하기엔 귀찮아서 난 그냥 nurd customer라는 회원이 접속시도한거라고 하고 제대로 맞았다고 가정하고 아래 코드를 썼다.
jwt패키지에서 sign메서드를 실행하고 적절한 데이터를 파라미터에 인수로 넣어주면 토큰이 생성된다.
프론트는 그냥 post 요청을 대충 하면 되기떄문에
const login = () => {
axios.post("/api/login", "data").then((res) => {
alert(JSON.stringify(res.data));
});
};
이런식으로 api요청만 가게 함수만들고 버튼이랑 연결해줬음.
테스트를 해보면?
로그인 버튼을 누르고 받은 응답을 alert로 띄웠는데 제대로 왔다. 토큰을 보면 더럽게 생겼다.
JWT.IO
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
jwt.io
요기 사이트에 가서 한번 적어보샘.
저 괴랄한 문자열은 양방향 암호화된거다. 그래서 디코딩될수있는데 이 디코딩에 대충 정보가 적혀있다.
payload에 exp가 만료시간이고 뭐 이것저것 정보가 있음..
암튼 이 정보로 입장권인 access토큰이 잘 발급된걸 볼 수 있다.
# 이제 api들을 이용할때 티켓검증하는 코드를 짜보자
이거 만들겠다는 소리임
const verify = (req, res, next) => {
const key = process.env.KEY;
const token = req.headers.authorization;
// 토큰이 없는 경우
if (!token) {
return next({
code: 401,
message: "토큰이 없습니다.",
});
}
// 인증 완료
try {
req.decoded = jwt.verify(token, key);
return next();
} catch (error) {
// 인증 실패
if (error.name === "TokenExpiredError") {
return next({
code: 419,
message: "토큰이 만료되었습니다.",
});
}
if (error.name === "JsonWebTokenError") {
return next({
code: 401,
message: "유효하지 않은 토큰입니다.",
});
}
if (error.name === "NotBeforeError") {
return next({
code: 401,
message: "토큰이 사용되기 전에 요청되었습니다.",
});
}
return next({
code: 500,
message: "서버 내부 오류입니다.",
});
}
};
verify라는 함수를 일단 만들어줄건데, 보통 모듈화해서 따로빼서 쓰지만 난 걍 이해쉽게 백단 서버코드에다가 통쨰로 집어넣음
이게 미들웨어로 동작할거다.
그리고 mypage요청을 받는 api 백단코드는
app.get("/api/mypage", verify, (req, res) => {
const nickname = req.decoded.nickname;
return res.status(200).json({
code: 200,
message: "토큰이 정상입니다.",
data: JSON.stringify({
nickname: nickname,
}),
});
});
이렇게 단순하게 생겼다. 다만 저기 app.get메서드에 들어간 두번쨰 파라미터 인수를 보면, verify함수가 들어간다. 위에 만들어놓은 verify함수가 이 api요청받을떄마다 호출된다는 소리임.
만약 이 verify함수에서 문제가 생기면 거기서 종료되니까 로그인안한사람한테 로그인해야 제공되는 api응답을 안줄수있다.
const mypage = () => {
const token =
"여기에 받은 토큰 넣으샘. 이건 원래 브라우저 스토리지에 저장되어있어야함.";
axios
.get("/api/mypage", {
headers: {
Authorization: token, // 토큰을 Authorization 헤더에 추가
},
})
.then((res) => {
alert(JSON.stringify(res.data));
})
.catch((error) => {
console.error("Error:", error);
alert("An error occurred while fetching the mypage.");
});
};
프론트단 함수인데, 이게 예를들어 로그인 필요한 서비스일경우 api요청하는 axios함수이다.
axios get요청에서 header값을 넣는데, 여기에 토큰을 넣은걸 백단에서 뜯어보고 디코딩할수 있다.
암튼 이상태에서 mypage button을 눌러보면
에러가 뜬다. 당연히 저기 token식별자에 '여기에 토큰넣으샘 머시기'이런 스트링 넣어놔서그럼.
그럼 여기따가 아까 로그인했을떄 만들어진 토큰스트링을 넣으면?
잘 받는다.
# 토큰을 살짝 고쳐보자.
위에 토큰 스트링에서 살짝 백스페이스 쳐서 글자 몇개지워봤다.
프론트에선 에러뜨고,
백에선 401에러를 띄운다.
아 참고로 난 코드로 에러처리 보려고
app.use((err, req, res, next) => {
if (err && err.code) {
console.log(err);
return res.status(err.code).json({
code: err.code,
message: err.message,
});
}
// 기타 오류 처리
return res.status(500).json({
code: 500,
message: "서버 내부 오류입니다.",
});
});
이거 적어놓음. app.use가 아마 모든 요청마다 처리하는걸텐데.. 뭐라하는지 기억이 안난다.
암튼 에러뜨면 그 에러메세지를 띄워주게함.
# 토큰을 변조해보면?
아까 분명히 jwt는 양방향 암호화라서 이걸 인코딩 디코딩 할 수 있다.
제대로된 토큰을 저 nickname만 바꿔보자.
돈이많은 rich customer로 바꿨다.
토큰을 바꿔서 rich customer의 계정으로 mypage를 들어간다음 포인트를 다 빼돌릴수 있지 않을까?
테스트를 해보면
위변조 방지까지 되어있다 ㄷㄷ
보안쪽은 잘 모르지만 소금치고 뭐 이것저것 아까 환경변수에 key값도 따로갖다쓰고 해서 아마 그런듯
암튼 토큰을 변조해서 들어갈수는 없다.
# 토큰이 만료된경우?
로그인할때 토큰만료시간을 짧게해서 새로 로그인하고 토큰 받아서 그걸로 mypage를 들어가보려고 하면,
419번 토큰만료가 뜬다.
# refresh토큰은 뭘까?
사용자 편의를 위한 토큰인데 로그인 만료시간이 지나도 다시 로그인을 자동으로 시켜주는 기능이다.
보통은 access토큰과 refresh 토큰을 둘다 만들어주고 사용자 브라우저 스토리지에 저장한다.
그리고 편하게 하려면 그냥 api요청 올떄마다 refresh 토큰을 발급해주는것도 괜찮은 방법임
이건 따로 포스팅해서 구현할거다.
# 전체코드
<template>
<h1>hello App.vue</h1>
<router-view></router-view>
<button v-on:click="test()">axios test</button>
<button v-on:click="login()">login button</button>
<button v-on:click="mypage()">mypage button</button>
<div class="sassTest">
<p>sassTest_blue</p>
</div>
</template>
<script>
import { reactive } from "vue";
import axios from "axios";
export default {
setup() {
///여기가 데이터
const state = reactive({
data: [],
});
///여기가 데이터
///여기가 메서드
const test = () => {
axios.get("/api/test").then((res) => {
alert(res.data);
});
};
const login = () => {
axios.post("/api/login", "data").then((res) => {
alert(JSON.stringify(res.data));
});
};
const mypage = () => {
const token =
"xcascascasc";
axios
.get("/api/mypage", {
headers: {
Authorization: token, // 토큰을 Authorization 헤더에 추가
},
})
.then((res) => {
alert(JSON.stringify(res.data));
})
.catch((error) => {
console.error("Error:", error);
alert("An error occurred while fetching the mypage.");
});
};
///여기가 메서드
return {
state,
test,
login,
mypage,
};
},
// 여기에 컴포넌트
};
</script>
<style lang="scss" scoped>
.sassTest {
p {
color: blue;
}
}
</style>
const express = require("express");
const app = express();
const bodyParser = require("body-parser");
const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");
app.use(bodyParser.json());
dotenv.config();
const port = process.env.PORT;
app.get("/api/test", (req, res) => {
res.json("hello axios test");
});
const verify = (req, res, next) => {
const key = process.env.KEY;
const token = req.headers.authorization;
// 토큰이 없는 경우
if (!token) {
return next({
code: 401,
message: "토큰이 없습니다.",
});
}
// 인증 완료
try {
req.decoded = jwt.verify(token, key);
return next();
} catch (error) {
// 인증 실패
if (error.name === "TokenExpiredError") {
console.log(419);
return next({
code: 419,
message: "토큰이 만료되었습니다.",
});
}
if (error.name === "JsonWebTokenError") {
console.log(4011);
return next({
code: 401,
message: "유효하지 않은 토큰입니다.",
});
}
if (error.name === "NotBeforeError") {
console.log(4012);
return next({
code: 401,
message: "토큰이 사용되기 전에 요청되었습니다.",
});
}
return next({
code: 500,
message: "서버 내부 오류입니다.",
});
}
};
app.post("/api/login", (req, res, next) => {
const key = process.env.KEY;
// 받은 요청에 아이디 비번으로 디비 조회 한번 한다.
const nickname = "nurd customer";
let token = "";
token = jwt.sign(
{
type: "JWT",
nickname: nickname,
},
key,
{
expiresIn: "0.1m", //여길 수정함
issuer: "nurd",
}
);
console.log(token);
// response
return res.status(200).json({
code: 200,
message: "token is created",
token: token,
});
});
app.get("/api/mypage", verify, (req, res) => {
const nickname = req.decoded.nickname;
return res.status(200).json({
code: 200,
message: "토큰이 정상입니다.",
data: JSON.stringify({
nickname: nickname,
}),
});
});
app.use((err, req, res, next) => {
if (err && err.code) {
console.log(err);
return res.status(err.code).json({
code: err.code,
message: err.message,
});
}
// 기타 오류 처리
return res.status(500).json({
code: 500,
message: "서버 내부 오류입니다.",
});
});
app.listen(port, () => {
console.log("서버시작");
});