사전 작업
환경 변수 설정
.env
파일 생성 후 데이터 베이스 관련 환경변수 설정
https 사설 인증서 발급 및 디렉토리에 복사
// mkcert 설치
sudo apt install libnss3-tools
wget -O mkcert https://github.com/FiloSottile/mkcert/releases/download/v1.4.3/mkcert-v1.4.3-linux-amd64
chmod +x mkcert
sudo cp mkcert /usr/local/bin/
// 인증서 생성
mkcert -install
mkcert -key-file key.pem -cert-file cert.pem localhost 127.0.0.1 ::1
// 127.0.0.1(IPv4), ::1(IPv6)
const jwt = require('jsonwebtoken');
const token = jwt.sign(토큰에 담을 값, ACCESS_SECRET, { 옵션1: 값, ...});
서버 구현
require("dotenv").config();
const fs = require("fs");
const https = require("https");
const cors = require("cors");
const cookieParser = require("cookie-parser");
const express = require("express");
const app = express();
const controllers = require("./controllers");
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(
cors({
origin: ["https://localhost:3000"],
credentials: true,
methods: ["GET", "POST", "OPTIONS"],
})
);
app.use(cookieParser());
app.post("/login", controllers.login);
app.get("/accesstokenrequest", controllers.accessTokenRequest);
app.get("/refreshtokenrequest", controllers.refreshTokenRequest);
const HTTPS_PORT = process.env.HTTPS_PORT || 4000;
// 인증서 파일들이 존재하는 경우에만 https 프로토콜을 사용하는 서버를 실행
// 만약 인증서 파일이 존재하지 않는경우, http 프로토콜을 사용하는 서버를 실행
// 파일 존재여부를 확인하는 폴더는 서버 폴더의 package.json이 위치한 곳
let server;
if(fs.existsSync("./key.pem") && fs.existsSync("./cert.pem")){
const privateKey = fs.readFileSync(__dirname + "/key.pem", "utf8");
const certificate = fs.readFileSync(__dirname + "/cert.pem", "utf8");
const credentials = { key: privateKey, cert: certificate };
server = https.createServer(credentials, app);
server.listen(HTTPS_PORT, () => console.log("server runnning"));
} else {
server = app.listen(HTTPS_PORT)
}
module.exports = server;
const { Users } = require('../../models');
const jwt = require('jsonwebtoken');
module.exports = async(req, res) => {
// 로그인에 필요한 정보를 HTTP 요청의 body에 담아 전송
const userInfo = await Users.findOne({
where: { userId: req.body.userId, password: req.body.password }
})
// 요청한 userId, password와 일치하는 유저가 DB에 존재하는지 확인
if (!userInfo) { // 일치하는 유저가 없을 경우
res.status(400).send({ data: null, message: 'not authorized' });
} else { // 일치하는 유저가 있을 경우 : 필요한 데이터를 payload에 담아 JWT token을 생성
// access token, refresh token 두 가지를 생성
// access token/refresh token은 각각 다른 비밀키를 생성
// 환경 변수에 저장된 ACCESS_SECRET, REFRESH_SECRET 값을 사용
const payload = {
id: userInfo.id,
userId: userInfo.userId,
email: userInfo.userId,
createdAt: userInfo.createdAt,
updatedAt: userInfo.updatedAt
}
const accessToken = jwt.sign( payload, process.env.ACCESS_SECRET, { expiresIn: '30s' } );
const refreshToken = jwt.sign( payload, process.env.REFRESH_SECRET, { expiresIn: '2d' } );
// 생성된 refreshToken을 쿠키에 담는다.
res.cookie('refreshToken', refreshToken)
// 클라이언트에 json객체 반환
res.status(200).json({ data: { 'accessToken': accessToken }, message: 'ok' } )
}
};
해당 스크립트에 나온 헤더를 콘솔로 찍어보면 authorization에 토큰 값이 들어오는 것을 확인할 수 있다.
{
host: '127.0.0.1:4000',
'accept-encoding': 'gzip, deflate',
authorization: 'Bearer token...',
}
JWT라이브러리를 사용해 토큰을 verify하는 방법
const jwt = require('jsonwebtoken');
const authorization = req.headers['authorization'];
const token = authorization.split(' ')[1];
const data = jwt.verify(token, process.env.ACCESS_SECRET);
const { Users } = require('../../models');
const jwt = require('jsonwebtoken');
module.exports = (req, res) => {
// accesstokenrequest 구현에 필요한 로직을 작성
// Authorizaton header에 담은 access token이 유효한지,
// 서버가 가지고 있는 비밀키로 생성한 토큰이 맞는지 확인
// Authorization header에 토큰이 담겨 있지 않거나, 해독할 수 없는 토큰이면 json객체 반환
if (!req.headers.authorization) {
res.status(400).send({ data: null, message: 'invalid access token' })
} else { // 토큰 verify
const authorization = req.headers['authorization'];
const token = authorization.split(' ')[1];
const data = jwt.verify(token, process.env.ACCESS_SECRET);
// JWT를 해독하여 얻은 payload안의 값으로 DB에 유저를 조회
if (!data) { // 일치하지 않은 경우
res.status(400).send({ data: null, message: 'invalid access token' })
} else { // 일치하는 경우, 필요한 데이터를 응답에 담아 반환
res.status(200).json( {
data: {
userInfo: {
id: data.id,
userId: data.userId,
email: data.email,
createdAt: data.createdAt,
updatedAt: data.updatedAt
}
}, message: 'ok'
})
}
}
};
const { Users } = require('../../models');
const jwt = require('jsonwebtoken');
module.exports = (req, res) => {
const isRefreshToken = req.cookies.refreshToken
if (!isRefreshToken) { // 토큰 값이 있을 경우
res.status(400).send({ data: null, message: 'refresh token not provided' })
} else if ( isRefreshToken === 'invalidtoken' ) { // 토큰 값이 유효한 경우
res.status(400).send({ data: null, message: 'invalid refresh token, please log in again' })
} else {
const data = jwt.verify(isRefreshToken, process.env.REFRESH_SECRET);
if (!data) {
res.status(400).send({ data: null, message: 'not data' })
} else { // 정보가 일치하면 payload에 새로운 데이터와 accesstoken을 생성해서 보내준다.
const payload = {
id: data.id,
userId: data.userId,
email: data.email,
createdAt: data.createdAt,
updatedAt: data.updatedAt
}
const accessToken = jwt.sign( payload, process.env.ACCESS_SECRET, { expiresIn: '30s' } );
res.status(200).json({ data: {'accessToken': accessToken, userInfo: payload}, message: 'ok' })
}
}
};