JWT (JSON Web Token)는 클라이언트와 서버간에 정보를 안전하게 전달하기 위한 간편 방법 중에 하나라고 한다.
헤더(Header), 페이로드(payload), 서명(sign) 세 부분으로 구성된 토큰이다. 헤더에는 토큰의 타입과 서명에 사용되는 hashing 알고리즘 정보가 담겨있고, 페이로드는 정보가 포함된다.
서버는 JWT를 생성하여 클라이언트에게 발급하고, 클라이언트는 JWT를 이용하여 인증 작업을 진행할 수 있다.
이러한 JWT기반의 인증은 JWT를 HTTP 헤더에 실어 서버가 클라이언트를 식별하는 방법이다.

간단하게 이렇게 이루어진다고 생각하면 된다.
사용자가 로그인 정보를 담아서 서버로 로그인을 요청하면, 서버는 전달받은 정보를 확인하여 올바른 사용자 정보의 경우, Access Token과 Refresh Token을 발급하게 된다.
여기서 Access Token과 Refresh Token이란?
(어렵다 백엔드 ...)
npm install jsonwebtoken 을 설치하면 된다.
jwt를 생성하기 위한 secretKey를 설정해야 하는데, 이는 .env 파일에 저장하여 사용하는 것이 보편적이라고 한다.
JWT를 생성하고 검증하는ㄷ 있어서 가장 중요한 역할을 하게 된다
1 . secretKey를 사용하여 토큰을 서명함으로써, 토큰의 내용을 변경하거나 위조할 수 없도록 한다.
2 . 서버는 클라이언트로부터 받은 JWT를 검증하여 토큰이 변조되지 않았음을 확인하고, 이 과정에서 secretKey를 사용하여 서명을 한다.

나또한 이렇게 지정을 해주었다.
이제 JWT를 활용하여 로그인을 구현해보도록 하겠다
app.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) {
console.log('이메일을 찾을 수 없습니다.');
return res.status(404).send('이메일을 찾을 수 없습니다.');
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
console.log('비밀번호가 일치하지 않습니다.');
return res.status(400).send('비밀번호를 잘못 입력하였습니다.');
}
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, {
expiresIn: '1h',
});
res.type('application/json');
return res.status(200).json({ token });
} catch (e) {
console.error('로그인 오류:', e.message);
return res.status(500).send('Error logging in: ' + e.message);
}
});
/login 경로에 대한 POST 요청을 처리하는 라우터 핸들러를 정의했다.bcrypt 라이브러리를 사용하여 입력된 비밀번호와 데이터베이스에 저장된 해시된 비밀 번호를 비교한다const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, {
expiresIn: '1h',
});
jsonwebtoken 라이브러리를 사용하여 JWT를 생성한다. 토큰은 사용자 ID(user._id)를 페이로드에 포함하고, 환경변수 JWT-SECRET를 사용하여 서명하게 된다. 토큰은 1시간동안 유효하다!
이후, 로그인 이후 인증이 필요한 라우트들에 loginAuth 미들웨어 적용한다.
app.use(loginAuth);
왜 ?
--> 보호된 라우트에 접근하려는 사용자가 유효한 인증 토큰을 가지고 있는지를 확인하기 위함이다. 간단하게 설명하자면, 사용자가 로그인하여 JWT토큰을 받았는지 확인을 하고, 그 토큰이 유효한지를 검증한다 (사용자가 인증된 상탱서만 특정 라우터에 접근할 수 있도록 한다)
토큰이 없는 사용자나 유효하지 않은 토큰을 가진 사용자가 보호된 리소스에 접근하지 못하도록 막을 수 있게 된다. 이를 통해 시스템의 무단 접근을 방지할 수 있다.
const jwt = require('jsonwebtoken');
const dotenv = require('dotenv');
dotenv.config();
module.exports = (req, res, next) => {
const token =
req.header('Authorization') &&
req.header('Authorization').replace('Bearer ', '');
if (!token) {
return res.status(401).send('Access denied');
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
console.error('Invalid token:', error.message);
// Content-Type을 application/json으로 설정하여 JSON 형식의 응답을 보냄
res.type('application/json').status(400).json({ error: 'Invalid token' });
}
};
const token = req.header('Authorization') && req.header('Authorization').replace('Bearer ', ''); Authorization 헤더를 읽고, "Bearer" 접두사를 제거하여 토큰만 추출한다.const decoded = jwt.verify(token, process.env.JWT_SECRET);req.user에 저장한다.app.use(loginAuth);
// 이후에 다른 라우트들 작성
app.get('/home', (req, res) => {
res.send('This is a protected route');
});
이렇게 적용하게 되면, /home fkdnxmsms loginAuth 미들웨어가 적용된 보호된 라우트이다. (라우트에 접근하려면 유효한 JWT 토큰이 필요하다)
--> 간단하게 설명하면 회원가입을 완료하고, 로그인에 성공한 사람만 해당 페이지에 접속할 수 있는 것이다!
import { Link } from 'react-router-dom';
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
interface ILoginData {
email: string;
password: string;
}
const Login = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<ILoginData>();
const [message, setMessage] = useState<string>('');
const navigate = useNavigate();
const onSubmitLogin = async (data: ILoginData) => {
try {
const response = await axios.post(
'http://localhost:3000/login',
{
email: data.email,
password: data.password,
},
{
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
}
);
const { token } = response.data;
if (token) {
sessionStorage.setItem('token', token);
setMessage('로그인 성공!');
setTimeout(() => {
navigate('/home');
}, 2000);
} else {
console.error('토큰이 없습니다.');
setMessage('로그인 실패. 다시 시도하세요.');
}
} catch (error: any) {
console.error('로그인 요청 실패:', error);
if (error.response) {
console.error('응답 데이터:', error.response.data);
console.error('응답 상태 코드:', error.response.status);
if (error.response.status === 401) {
setMessage('인증에 실패했습니다. 다시 로그인하세요.');
} else {
setMessage('로그인 실패. 다시 시도하세요.');
}
}
}
};
return (
<div>
<h1>로그인</h1>
<form onSubmit={handleSubmit(onSubmitLogin)}>
<div>
<label htmlFor="email">이메일:</label>
<input
id="email"
type="email"
placeholder="이메일을 입력하세요"
{...register('email', {
required: '이메일을 입력하세요',
pattern: {
value: /^\S+@\S+$/i,
message: '올바른 이메일 형식을 입력하세요',
},
})}
/>
{errors.email && <p>{errors.email.message}</p>}
</div>
<div>
<label htmlFor="password">비밀번호:</label>
<input
id="password"
type="password"
placeholder="비밀번호를 입력하세요"
{...register('password', {
required: '비밀번호를 입력하세요',
minLength: {
value: 6,
message: '비밀번호는 최소 6글자 이상이어야 합니다.',
},
})}
/>
{errors.password && <p>{errors.password.message}</p>}
</div>
<button type="submit">로그인</button>
</form>
<Link to="/signup">
<button type="button">회원가입</button>
</Link>
{message && <p>{message}</p>}
</div>
);
};
export default Login;
간단하게 설명하자면,
axios.post : 서버에 POST 요청을 보낸다. 여기서는 백엔드 코드에서 정의했던 /login 엔드포인트에 사용자의 이메일과 비밀번호를 저장한다!
성공적인 응답을 받은 토큰을 세션 스토리지에 저장하고, navigate 함수를 사용하여 '/home'페이지로 이동하게 된다.
여기서 중요한 점은, 로그인 한 사람만 /home 페이지에 접속할 수 있다라는 점이다
만약, 로그인을 하지 않은 사용자가 http://localhost:3001/home를 url로 타고 들어왔는데 들어가면 안되지 않을 까!
이때, privateRoute.tsx 컴포넌트를 만들어서
import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
import { isLoggedIn } from '../authUtils';
export default function PrivateRoute({
userAuthentication,
}: {
userAuthentication?: boolean;
}) {
const isLogin = isLoggedIn();
if (userAuthentication) {
// 사용자 인증이 필요한 페이지일 경우
if (!isLogin) return <Navigate to="/login" />;
return <Outlet />;
} else {
// 사용자 인증이 필요하지 않은 페이지일 경우
if (isLogin) return <Outlet />;
return <Navigate to="/login" />;
}
}
로그인 여부를 판단해서, 로그인을 안했을 경우(사용자 인증을 안했을 경우) -> 로그인 페이지로,
인증이 된 상태일 경우 -> /home 페이지로 이동할 수 있도록 !
여기서 사용자가 로그인 했는지 판단해주는 isLoggedIn 코드를 먼저 살펴보자면,
export function isLoggedIn() {
const token = sessionStorage.getItem('token');
return !!token;
}
export function login() {
sessionStorage.setItem('token', 'true');
}
export function logout() {
sessionStorage.removeItem('token');
}
sLoggedIn 함수
login 함수
logout 함수
이후
const App = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/signup" element={<Signup />} />
<Route path="/login" element={<Login />} />
<Route path="/" element={<Login />} />
<Route element={<PrivateRoute userAuthentication={true} />}>
<Route path="/home" element={<Home />} />
</Route>
</Routes>
</BrowserRouter>
);
};
export default App;
라우터를 이렇게 설정해 주었다!
우선, /home 페이지만 인증을 한 상태일 때 이동이 가능하도록 처리를 해주었다.
이렇게 처리를 해주면,
아무리 http://localhost:3001/home 를 접속한다고 해도, 로그인이 안됬을 때에는 /login 페이지로 이동하게 될 것이다!
로그아웃 기능을 구현할 때 단순히 클라이언트의 로컬 저장소 (sessionStorage)에서 데이터를 제거하는 방식으로 구현하면 된다고 한다.
위에서 구현한 logout()을 사용하면 된다
const Home = () => {
const navigate = useNavigate();
const handleLogout = () => {
logout(); // 로그아웃 함수 호출
navigate('/');
};
return (
<>
<h1>메인화면 입니다.</h1>
{isLoggedIn() && <button onClick={handleLogout}>Logout</button>}
</>
);
};
export default Home;
간단하게 home페이지에 Logout 버튼을 누르면 logout() 함수를 호출하고, logout() 함수에서 token을 제거해서 로그아웃 상태가 된다.
이후 -> / 로 이동해서 다시 로그인을 페이지를 보여주도록 하였다!
logout 버튼을 누르게 되면

다시 로그인 화면으로 이동! 하게 된다!