본문 바로가기
백엔드

JWT토큰 인증(Local Storage)

by 하_영 2023. 5. 15.

Authorization Header로 jwt토큰을 주고받는 방식의 특징

  • jwt토큰의 데이터를 클라이언트에서 저장
  • 세션관리가 필요없음

JWT토큰을 사용해서 인증을 검증하는 방식은 다음과 같이 설계할 수 있다.

 

 

 

 

정보가 일치하면 JWT토큰 발급

const secret = "비밀키: 단순하고 짧게해놓으면 보안 이슈가 생길수 있으므로 주의"
function setUserToken(user,isOnlyAccess){
//isOnlyAccess가 참일때: access토큰만 발급, 거짓일때: access,refresh 둘다 발급
	const accessPayload = {
		shortId: user.shortId,
		name: user.name,
		email: user.email,
		profileImage: user.profileImage,
		isAdmin: user.isAdmin,
		isTempPassword: user.isTempPassword,
	};
	const accessOptions = { algorithm: 'HS256', expiresIn: '1h' };
	const accessToken = jwt.sign(accessPayload, env.ACCESSSECRET, accessOptions);

	if (!isOnlyAccess) {
		const refreshPayload = {
			shortId: user.shortId,
		};
		const refreshOptions = { algorithm: 'HS256', expiresIn: '7d' };
		const refreshToken = jwt.sign(
			refreshPayload,
			env.REFRESHSECRET,
			refreshOptions
		);
		User.updateOne(
			{ shortId: refreshPayload.shortId },
			{
				refreshToken: refreshToken,
			}
		)
			.then((res) => {
				console.log('res : ', res);
			})
			.catch((err) => {
				console.log('fail : ', err);
			});

		return { accessToken, refreshToken };
	} else {
		return { accessToken };
	}
//----------------------------------------------------------------------------------
//로그인
router.post('/', async (req, res, next) => {
	const { email, password } = req.body;
	const user = await User.findOne({ email });
	if (!user) {
		console.log('회원을 찾을 수 없습니다.');
		return res.status(401).json({ message: '로그인 실패' });
	}
	// 검색 한 유저의 비밀번호와 요청된 비밀번호의 해쉬값이 일치하는지 확인
	if (user.password !== hashPassword(password)) {
		return res.status(401).json({ message: '로그인 실패' });
	}
	console.log('토큰 만들기 실행');
	res.send(setUserToken(user,0));
});

 

 

JWT토큰 검증

검증이 필요한 api에 JWT토큰을 검증하는 함수를 미들웨어로 등록한다.

아래의 코드는 Access JWT토큰을 검증하는 함수이다.

const passport = require('passport');

module.exports = (req, res, next) => {
	console.log('getUserFromJwt 미들웨어 실행');

	console.log('인증전략을 사용하여 검사 시작');
	//토큰검증 미들웨어
	passport.authenticate('access', { session: false })(req, res, (err) => {
		if (err) {
			console.log('authenticate 에러남');
			res.status(500).send(err.message);
		} else {
			console.log('Authorized');
			next();
		}
	});
};

passport.authenticate가 실행되면 passport 미들웨어로 등록한 'access'전략 함수가 실행된다.

아래는 Access 토큰의 인증전략 함수이다.

const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const env = require('../../.env');
const { User } = require('../../models/index.js');

const jwtOptions = {
	secretOrKey: env.ACCESSSECRET,
	jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
};

const accessJwtStrategy = new JwtStrategy(jwtOptions, async (payload, done) => {
	try {
		const user = await User.findOne({ shortId: payload.shortId });

		return done(null, user);
	} catch {
		return done(err, false);
	}
});

module.exports = accessJwtStrategy;

인증 전략 함수가 성공하면 req.user객체에 저장되고, passport.authenticate에서 next()가 실행되어 클라이언트가 요청한api를 실행한다.

Access 토큰의 유효기간이 만료되면 가지고있는 Refrash 토큰을 사용해서 Access토큰을 다시 발급받는다.

 

 

새로운 토큰 발급

refresh 토큰 검증후 access토큰을 발급하는 코드

//새로운 토큰 요청
authRouter.get('/', refreshToken, (req, res) => {//refresh토큰 검증 미들웨어 실행
	console.log('새로운 토큰 만들기 실행');
  const { shortId } = req.user;

  const userForToken = await userService.getUserForToken(shortId);
  res.send(setUserToken(userForToken, 1)); //access 토큰만 발급
})

Refresh토큰을 검증하는 미들웨어

const passport = require('passport');
const { userService } = require('../services/index');

module.exports = (req, res, next) => {
	//토큰검증 미들웨어
	passport.authenticate('refresh', { session: false })( //아래의 전략함수 실행
		req,
		res,
		async (err) => {
			if (err) {
				console.log('refresh authenticate 에러');
				res
					.status(500)
					.send({ message: `토큰검증 미들웨어 에러: ${err.message}` });
			} else {
				const { shortId } = req.user;
				const userRefreshToken = await userService.getUserRefreshToken(shortId);
				const InputRefreshToken = req.headers.authorization.substring(7);

				if (userRefreshToken.refreshToken === InputRefreshToken) {
					console.log('refresh Authorized');
					next();
				} else {
					res.status(403).send({ message: 'refresh토큰이 만료되었습니다.' });
				}
			}
		}
	);
};

DB에 저장된 refresh토큰과 비교하기때문에 refresh토큰은 유저당 한개씩만 사용 가능하다.

passport.authenticate가 실행되면 passport 미들웨어로 등록한 'refresh' 전략 함수가 실행된다.

아래는 Refresh토큰의 인증전략 함수이다.

const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const env = require('../../.env');
const { User } = require('../../models/index.js');

const jwtOptions = {
	secretOrKey: env.REFRESHSECRET,
	jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
};

const refreshJwtStrategy = new JwtStrategy(
	jwtOptions,
	async (payload, done) => {
		try {
			const user = await User.findOne(
				{ shortId: payload.shortId },
				{ refreshToken: 1, shortId: 1 }
			);

			return done(null, user);
		} catch {
			return done(err, false);
		}
	}
);

module.exports = refreshJwtStrategy;

 

 

passport 미들웨어 설정

const passport = require('passport');
const accessJwtStrategy = require('./strategies/accessJwtStrategy');
const refreshJwtStrategy = require('./strategies/refreshJwtStrategy');

module.exports = () => {
  //전략함수의 이름을 지정한다.
	passport.use('access', accessJwtStrategy); 
	passport.use('refresh', refreshJwtStrategy);
};

📌정리

로그인요청시 access, refresh토큰 발급

jwt토큰 인증전략 함수가 passport 미들웨어로 실행

토큰을 검증하는 함수가 api 미들웨어로 실행

→ api미들웨어가 실행되기전에 항상 jwt토큰 인증전략함수가 먼저 실행된다

refresh 토큰은 DB에 저장되기 때문에 로그인을 다시하면 기존의 refresh 토큰이 만료가 안되었더라도 사용이 불가능


📌주의사항

jwt토큰은 디코딩하기 매우 쉽기때문에 중요한 개인정보는 토큰에 저장하면 안된다.

보안을 위해서 jwt.sign의 옵션에 알고리즘을 꼭 작성한다.

토큰의 만료시간을 설정해서 탈취, 악용의 위험을 낮춘다.

시크릿키는 반복적이고 쉬운 유추가능한 문자열로 사용하지 않는다.


📌localStorage를 사용한 이유

쿠키의 장점은 httpOnly 옵션을 둬서 XSS 공격에 localStorage보다 안전하다는 점이라고 생각했는데 이또한 완전히 안전한것은 아니라서 쿠키의 장점이 와닿지 않았고, 검증이 필요없는 api들도 있었기 때문에 모든 요청마다 함께 전송되는 쿠키가 성능을 떨어트리는 원인이 될 수 있다고 생각해서 localStorage에 저장하는 방식을 택했다. 그러나 localStorage도 단점이 있었는데 그중 하나가 토큰을 강제로 만료시킬수 없다는 점이었다. 이 부분을 개선 시키기 위해 refresh 토큰을 사용해서 보안을 강화했다. 아래의 코드처럼 refresh 토큰을 DB에 저장해서 비교하기 때문에 토큰이 만료되지 않았더라고 이전에 사용한 토큰이라면 검증에 실패하게 만들었다.

if (userRefreshToken.refreshToken === InputRefreshToken) {
//userRefreshToken.refreshToken: DB에 저장된 토큰
//InputRefreshToken: 헤더로 받은 토큰
	console.log('refresh Authorized');
	next();
} else {
	res.status(403).send({ message: 'refresh토큰이 만료되었습니다.' });
}