1. JWT(JSON Web Token) 이란 ?
서버와 클라이언트 사이에 정보를 전달할 떄, 정보를 안전하고 가볍게 전달할 수 있는 표준 RFC7519 입니다.
토큰 내부에는 signature
가 존재해서, 이 토큰이 검증되었는지 확인할 수 있습니다. 또한 self-contained 해서, 정보가 내부에 자체적으로 저장되어 있어, 인증에 많이 사용됩니다.
토큰 구조
HEADER.PAYLOAD.SIGNATURE
점(.)으로 나누어진 세 부분을 갖는 문자열 구조로 되어 있습니다.
HEADER
JWT를 검증하는데 필요한 정보(해싱 알고리즘)와 토큰 타입을 담고 있는 부분입니다.
{
"typ": "JWT",
"alg": "HS256"
}
이 정보를 Base64 인코딩하면 헤더를 만들 수 있습니다.
HEADER = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
PAYLOAD
데이터가 담기는 부분입니다. 여기에 담기는 정보를 claim
이라 합니다.
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
마찬가지로 Base64 인코딩을 통해 페이로드를 만들 수 있습니다.
PAYLOAD = eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
SIGNATURE
마지막으로 가장 중요한 시그니쳐 부분입니다. 이 부분을 통해 토큰이 변조되었는지 확인할 수 있습니다.
헤더에 정의된 알고리즘과 secret key 를 이용하여 생성합니다.
SIGNATURE =
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
SECRET
)
이제 세 부분을 합쳐 JWT로 만들 수 있습니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o
2. 유저 인증에서의 사용
1) 기존의 인증 방식
HTTP 프로토콜은 stateless 한 프로토콜이기 떄문에, 서버가 클라이언트를 식별 및 리소스에 인가하기 위해서는 매 요청마다 추가적인 절차가 필요합니다. 이를 위해서 사용하는 방법이 서버의 session
을 이용한 방식입니다.
클라이언트가 인증 요청을 보내고, 서버가 확인하면 유저 정보를 session
에 저장하고 클라이언트에 session ID
을 발급합니다.
이제 다음 HTTP
요청부터는 session ID
을 같이 보내게 되고, 이제 서버는 session
에 요청된 유저 정보가 있는지 확인하여, 요청을 받아들이거나 거부합니다.
이러한 방식은 서버가 세션을 거친다는 점에서 응답 속도가 느려지고, 요청과 사용자가 많아지면 메모리에 부담이 될 수 있습니다.
2) JWT
이러한 단점을 극복하기 위해, session
을 사용하지 않고 유저 정보 자체를 담아 응답하는 JWT
(크게는 토큰) 방식이 사용되게 되었습니다.
클라이언트는 요청마다 토큰을 보내게 되고, 서버는 이 토큰을 받아 검증만 하고 추가적인 DB 요청 없이 인증 여부를 확인할 수 있습니다.
3. passport 를 이용한 JWT 인증
passport 는 다양한 인증방식(OAuth, local ..)을 지원하는 nodejs 미들웨어입니다.
1) 로그인 API
유저가 서버로 POST /auth/login
요청시, 서버는 passport strategy를 이용해 데이터(ID,PW) 가 유효한지 검사한 후 유효하면 클라이언트에 token
을 내려보내 줍니다.
Local stategy
export const localStrategy = (passport: passport.PassportStatic) => {
passport.use(new Strategy({
usernameField: 'username',
passwordField: 'password',
}, async (username, password, done) => {
try {
const user = await User.findOne({where: {username}});
if(user) {
if(user.password === password) {
done(null, user);
}
else{
done(null, false);
}
}
else {
done(null, false);
}
} catch (err) {
console.log(err);
done(err);
}
}));
};
로그인 API
router.post('/login', (req: Request, res: Response, next: NextFunction) => {
passport.authenticate('local', (err, user, info) => {
if(err || !user) {
res.status(401).send();
}
req.logIn(user, {session: false}, (err) => {
if(err) {
res.status(500).send();
}
else {
const token = jwt.sign(JSON.stringify({id: user.id, username: user.username}),process.env.JWT_SECRET as string)
}
});
})(req,res,next);
});
2) Jwt Strategy
토큰이 유효한지 검사하는 passport strategy 입니다.
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
}
export const JWTStrategy = (passport: passport.PassportStatic) => {
passport.use(new jwtStrategy(jwtOptions, async (payload, done) => {
try {
const user = await User.findOne({where: {username: payload.username}});
if(!user) {
done(null, false);
}
else {
done(null, user);
}
} catch (err) {
done(err);
}
}))
};
3) 인증 미들웨어
인증이 필요한 API 접근시, isLoggedIn
미들웨어를 거쳐 사용자가 인증되어있는지 확인할 수 있습니다.
export const isLoggedIn = (req: Request, res: Response, next: NextFunction) => {
passport.authenticate('jwt', {session: false}, (err, user) => {
if(user){
req.user = user;
next();
}
else {
res.status(403).send();
}
});
}
4) 다른 API 에서의 사용
이제 사용자 인증이 필요한 API 에서, isLoggedIn
미들웨어를 사용해 인증된 사용자만 리소스에 접근 가능하게 만들 수 있습니다.
import { isLoggedIn } from '../middleware';
router.get('/test', isLoggedIn, (req: Request, res: Response, next: NextFunction) => {
res.status(200).send("인증 성공하였습니다.");
});
5) 클라이언트에서의 요청
이제 클라이언트는 로그인 시 받은 토큰을 HTTP 헤더에 담아 요청하면, 서버는 해당 토큰을 검증해 리소스에 접근하게 할 수 있습니다.