Cookie / Session / Token(3)

이성은·2023년 1월 17일
0
post-thumbnail
post-custom-banner

3. Token

3-1. Token기반 인증

  • 세션기반 인증은 서버(혹은 DB)에 유저 정보를 담는 방식 → 서버의 부담을 덜어내는 방법
  • 클라이언트에서 인증 정보를 보관하는 방법으로 토큰기반 인증이 고안
  • 대표적인 토큰기반 인증 : JWT(JSON Web Token)
  • 토큰은 유저 정보를 암호화 하기 때문에 클라이언트에 담을 수 있음

3-2. JWT

JWT의 종류

  • JWT : json 포맷으로 사용자에 대한 속성을 저장하는 웹 토큰
  • 액세스 토큰 : 보호된 정보들에 접근할 수 있는 권한부여에 사용
  • 클라이언트가 처음 인증을 받게 될 때(로그인 시) 액세스 토큰, 리프레시 토큰 두가지를 다 받지만, 실제로 권한을 얻는 데 사용하는 토큰은 액세스 토큰
  • 액세스 토큰에는 짧은 유효기간을 주어 토큰을 탈취하더라도 오랫동안 사용할 수 없도록 하는것이 좋음
  • 리프레시 토큰 : 액세스 토큰의 유효기간이 만료되면 새로운 액세스 토큰 발급할 때 사용
  • 리프레시 토큰은 유효기간이 길어 보안을 위해 사용하지 않는 곳이 많음

JWT 구조

  • Header : 토큰 종류, 시그니처를 암호화하는 알고리즘 → JSON 객체를 base64 방식으로 인코딩
  • Payload : 서버에서 활용할 수 있는 유저의 정보 → JSON 객체를 base64로 인코딩
  • Signature : 서버의 비밀 키(암호화에 추가할 salt)와 헤더에서 지정한 알고리즘을 사용하여 해싱

3-3. Token기반 인증 절차

  • 클라이언트가 서버에 아이디/비밀번호를 담아 로그인 요청 보냄
  • 아이디/비밀번호가 일치하는지 확인, 클라이언트에게 보낼 암호화된 토큰 생성
    • access/refresh 토큰을 모두 생성
    • 토큰에 담길 정보(payload) : 유저를 식별할 정보, 권한이 부여된 카테고리(사진, 연락처)
    • 두 종류의 토큰이 같은 정보를 담을 필요 없음
  • 서버가 토큰을 클라이언트에게 보내주면, 클라이언트는 토큰을 저장
    • 저장하는 위치 : Local Storage, Session Storage, Cookie 등 다양
  • 클라이언트가 HTTP 헤더(Authorizition 헤더)또는 쿠키에 토큰을 담아 보냄
    쿠키에는 리프레시 토큰, 헤더 또는 바디에는 액세스 토큰을 담는 등 다양한 방법으로 구현
    - Authorizition 헤더를 사용한다면 Bearer Authorizition을 이용
  • 서버는 토큰을 해독하여 발급한 토큰이 맞으면, 클라이언트의 요청을 처리한 후 응답 보냄

3-2. Token 튜토리얼

// .env
ACCESS_SECRET=codestates
REFRESH_SECRET=codestates

// index.js
const express = require('express');
const cors = require('cors');
const logger = require('morgan');
const cookieParser = require('cookie-parser');
const fs = require('fs');
const https = require('https');
const controllers = require('./controllers');
const app = express();

//mkcert에서 발급한 인증서를 사용하기 위한 코드
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

const HTTPS_PORT = process.env.HTTPS_PORT || 4000;

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());

app.use(
  cors({
    origin: 'http://localhost:3000',
    methods: ['GET', 'POST', 'OPTIONS'],
    credentials: true,
  })
);

app.post('/login', controllers.login);
app.post('/logout', controllers.logout);
app.get('/userinfo', controllers.userInfo);

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(`🚀 HTTPS Server is starting on ${HTTPS_PORT}`));
} else {
  server = app.listen(HTTPS_PORT, () => console.log(`🚀 HTTP Server is starting on ${HTTPS_PORT}`));
}
module.exports = server;

// tokenFunctions.js
require("dotenv").config();
const {sign, verify} = require("jsonwebtoken");

module.exports = {
	generateToken: async (user, checkedKeepLogin) => {
		const payload = {
			id: user.id,
			email: user.email,
		};
		let result = {
			accessToken: sign(payload, process.env.ACCESS_SECRET, {
				expiresIn: "1d", // 1일간 유효한 토큰을 발행합니다.
			}),
		};

		if (checkedKeepLogin) {
			result.refreshToken = sign(payload, process.env.REFRESH_SECRET, {
				expiresIn: "7d", // 일주일간 유효한 토큰을 발행합니다.
			});
		}
		return result;
	},
	verifyToken: async (type, token) => {
		let secretKey, decoded;
		switch (type) {
			case "access":
				secretKey = process.env.ACCESS_SECRET;
				break;
			case "refresh":
				secretKey = process.env.REFRESH_SECRET;
				break;
			default:
				return null;
		}

		try {
			decoded = await verify(token, secretKey);
		} catch (err) {
			console.log(`JWT Error: ${err.message}`);
			return null;
		}
		return decoded;
	},
};

// login.js
const {USER_DATA} = require("../../db/data");
// JWT는 generateToken으로 생성할 수 있습니다. 
const {generateToken} = require("../helper/tokenFunctions");

module.exports = async (req, res) => {
	const {userId, password} = req.body.loginInfo;
	const {checkedKeepLogin} = req.body;
	const userInfo = {
		...USER_DATA.filter((user) => user.userId === userId && user.password === password)[0],
	};
	if (!userInfo.id) {
		res.status(401).send("Not Authorized");
	} else {
		const {accessToken, refreshToken} = await generateToken(userInfo, checkedKeepLogin);
		const cookiesOption = {
			domain: "localhost",
			path: "/",
			httpOnly: true,
			sameSite: "none",
			secure: true,
		};
		res.cookie("access_jwt", accessToken, cookiesOption);
		if (checkedKeepLogin) {
			cookiesOption.maxAge = 1000 * 60 * 60 * 24 * 7;
			res.cookie("refresh_jwt", refreshToken, cookiesOption);
		}
		res.redirect("/userinfo");
	}
};

// logout.js
module.exports = (req, res) => {
	const {access_jwt, refresh_jwt} = req.cookies;
	const cookiesOption = {
		domain: "localhost",
		path: "/",
		httpOnly: true,
		sameSite: "none",
		secure: true,
	};
	res.clearCookie("access_jwt", cookiesOption);
	if (refresh_jwt) {
		res.clearCookie("refresh_jwt", cookiesOption);
	}
	res.status(205).send("logout");
};

// userInfo.js
const {USER_DATA} = require("../../db/data");
// JWT는 verifyToken으로 검증할 수 있습니다.
const {verifyToken, generateToken} = require("../helper/tokenFunctions");

module.exports = async (req, res) => {
	const {access_jwt, refresh_jwt} = req.cookies;
	const accessPayload = await verifyToken("access", access_jwt);
	if (accessPayload) {
		const userInfo = {
			...USER_DATA.filter((user) => user.id === accessPayload.id)[0],
		};
		if (!userInfo.id) {
			res.status(401).send("Not Authorized");
		}
		delete userInfo.password;
		res.send(userInfo);
	} else if (refresh_jwt) {
		const refreshPayload = await verifyToken("refresh", refresh_jwt);
		if (!refreshPayload) {
			res.status(401).send("Not Authorized");
		}
		const userInfo = {
			...USER_DATA.filter((user) => user.id === refreshPayload.id)[0],
		};
		if (!userInfo.id) {
			res.status(401).send("Not Authorized");
		}
		const {accessToken} = await generateToken(userInfo);
		const cookiesOption = {
			domain: "localhost",
			path: "/",
			httpOnly: true,
			sameSite: "none",
			secure: true,
		};
		res.cookie("access_jwt", accessToken, cookiesOption);
		res.redirect("/userinfo");
	} else {
		res.status(401).send("Not Authorized");
	}
};



profile
함께 일하는 프론트엔드 개발자 이성은입니다🐥
post-custom-banner

0개의 댓글