우리가 만든 서버에 회원 인증 시스템을 구현해보겠습니다. 이 시스템을 구현하기 위해 JWT라는 기술을 사용합니다. JWT는 JSON Web Token의 약자로, 데이터가 JSON으로 이루어져 있는 토큰을 의미합니다. 두 개체가 서로 안전하게 정보를 주고받을 수 있도록 웹 표준으로 정의된 기술입니다.
세션 기반 인증과 토큰 기반 인증의 차이
< 세션 기반 인증 시스템 >
세션을 기반으로 인증 시스템을 만든다는 것은 어떤 의미일까요? 한마디로 서버가 사용자가 로그인 중임을 기억하고 있는 것입니다. 세션 기반 인증 시스템에서 사용자가 로그인을 하면, 서버는 세선 저장소에 사용자의 정보를 조회하고 세션 id를 발급합니다. 발급된 id는 주로 브라우저의 쿠키에 저장하게 됩니다. 그 다음에 사용자가 다른 요청을 보낼 때마다 서버는 세션 저장소에서 세션을 조회한 후로그인 여부를 결정하여 작업을 처리하고 응답을 합니다. 세션 저장소는 주로 메모리, 디스크, 데이터베이스 등을 사용합니다.
세션 기반 인증의 단점은 서버를 확장하기가 번거로워질 수 있다는 점입니다. 만약 서버의 인스턴스가 여러 개가 된다면, 모든 서버끼리 같은 세션을 공유해야 하므로 세션 전용 데이터베이스를 만들어야 할 뿐 아니라 신경 써야 할 것도 많습니다.
< 토큰 기반 인증 시스템 >
토큰은 로그인 이후 서버가 만들어 주는 문자열입니다. 해당 문자열 안에는 사용자의 로그인 정보가 들어 있고, 해당 정보가 서버에서 발급 되었음을 증명하는 서명이 들어 있습니다.
서명 데이터는 해싱 알고리즘을 통해 만들어지는데, 주로 HMAC SHA256 혹은 RSA SHA256알고리즘이 사용됩니다.
서버에서 만들어 준 토큰은 서명이 있기 때문에 무결성이 보장됩니다. 여기서 무결성이란 정보가 변경되거나 위조되지 않았음을 의미하는 성질입니다. 사용자가 로그인을 하면 서버에서 사용자에게 해당 사용자의 정보를 지니고 있는 토큰을 발급해 주고, 추후 사용자가 다른 API를 요청하게 될 때 발급 받은 토큰과 함꼐 요청하게 됩니다. 그러면 서버는 해당 토큰이 유효한지 검사하고, 결과에 따라 작업을 처리하고 응답합니다.
토큰 기반 인증 시스템의 장점은 서버에서 사용자 로그인 정보를 기억하기 위해 사용하는 리소스가 적다는 것입니다. 사용자 쪽에서 로그인 상태를 지닌 토큰을 가지고 있으므로 서버의 확장성이 매우 높습니다. 서버의 인스턴스가 여러 개로 늘어나도 서버끼리 사용자의 로그인 상태를 공유하고 있을 필요가 없습니다.
JWT에서는 기본적으로 공개 키 암호 방식을 사용합니다. 비대칭 암호 방식을 이용해 공개 키와 비밀 키를 생성하고 이 키들을 상황에 따라 나누어 가지고 통신합니다.
서명은 비밀 키가 있는 곳에서만 할 수 있고 공개 키를 가진 어느 곳에서나 이 데이터의 서명을 검증할 수 있습니다.
공개키를 가진 누구나 데이터를 암호화해서 데이터를 보낼 수 있지만, 비밀 키를 가진 곳에서만 데이터를 복호화해서 내용을 확인할 수 있는 것입니다.
서명: 비밀 키를 가진 극소수(주로 한명)만 데이터에 서명할 수 있다. 공개 키를 가진 아무나 데이터의 서명을 검증할 수 있다.
암호화: 공개 키를 가진 아무나 데이터를 암호화할 수 있다. 비밀 키를 가진 극소수만 데이터를 복호화해 확인할 수 있다.
즉 정리하면 이러하다
- 사용자가 id와 password를 입력하고 서버로 로그인 요청을 보낸다.
- 서버는 비밀키를 통해서 서명을 하고 공개키로 암호화 시킨 accessToken을 발급한다.
- accessToken을 사용자에게 보낸다.
요기 까지 과정이 사용자가 로그인 하는 과정이다.
- 로그인 정보가 필요한 API Call마다 토큰을 실어서 보낸다.
- ( 비밀키로 복호화 하는 것은 자신만 보려고, 공개키로 복호화 하는 것은 신뢰의 문제) 즉 서버에서는 사용자가 보낸 토큰을 공개키로 서명을 체크라고 안에 담긴 정보를 확인합니다. 그리고 서버는 비밀키로 유효한 토큰인지 확인합니다.( 자신이 발급하고 무결성에 문제가 없는지 )
- 서버가 요청에 대한 응답을 사용자에게 전달합니다.
- Refresh API
- 새로고침, AccessToken만료시에 호출
- RefreshToken을 쿠키에서 읽어와서 서버로 보냄
- RefreshToken, AccessToken을 다 받아올지 AccessToken만 받아올지는 선택
- Login API
- 로그인 시 호출
- RefreshToken, AccessToken을 받아옴
- API호출 후
- AccessToken는 header에 default로 설정하여 API마다 보낻고록 설정
- AccessToken의 유효기간이 끝나기 전 자동으로 Refresh API가 호출되도록 설정 가능( 선택적 )
- RefreshToken는 쿠키에 저장
- RefreshToken또한 기간이 만료되면 재로그인이 필요
즉 정리해서
장점
- 세션 방식과 다르게 별도의 인증 저장소가 필요 없어서 서버와의 커뮤니케이션을 최소한으로 할 수 있다.
- 트래픽에 대한 부담이 적다
- 세션과 다르게 독립적인 느낌의 JWT를 활용한다는 것
단점
- JWT의 크기가 커질수록 거의 모든 요청에 대해 전송되므로 데이터 트래픽 크기에 영향을 미칠 수 있다.
- 토큰은 클라이언트에 저장되기 때문에 DB에서 사용자 정보를 수정하더라도 토큰에 직접 적용할 수 없다.
user 스키마
< 모델 메서드 만들기 >
모델 메서드는 모델에서 사용할 수 있는 함수를 의미하며, 두 가지 종류가 있습니다. 첫 번째는 인스턴스 메서드로, 모델을 만든 문서 인스턴스에서 사용할 수 있는 함수를 의미합니다.
두 번재는 스태틱(static)메서드로, 모델에서 바로 사용할 수 있는 함수를 의미합니다.
< 인스턴스 메서드 만들기 >
// src/models/user.js
import mongoose, { Schema } from 'mongoose';
import bcrypt from 'bcrypt';
const UserSchema = new Schema({
username: String,
hashedPassword: String,
});
UserSchema.methods.setPassword = async function (password) {
const hash = await bcrypt.hash(password, 10);
this.hashedPassword = hash;
};
UserSchema.methods.checkPassword = async function (password) {
const result = await bcrypt.compare(password, this.hashedPassword);
return result; // true or false
};
const User = mongoose.model('User', UserSchema);
export default User;
인스턴스 메서드를 작성할 때는 화살표 함수가 아닌 function키워드를 사용하여 구현해야 합니다. 함수 내부에서 this에 접근해야 하기 때문입니다. 여기서 this는 문서 인스턴스를 가리킵니다. 화살표 함수를 사용하면 this는 문서 인스턴스를 가리키지 못하게 됩니다.
< 스태틱 메서드 만들기 >
// src/models/user.js
import mongoose, { Schema } from 'mongoose';
import bcrypt from 'bcrypt';
const UserSchema = new Schema({
username: String,
hashedPassword: String,
});
UserSchema.methods.setPassword = async function (password) {
const hash = await bcrypt.hash(password, 10);
this.hashedPassword = hash;
};
UserSchema.methods.checkPassword = async function (password) {
const result = await bcrypt.compare(password, this.hashedPassword);
return result; // true or false
};
UserSchema.statics.findByUsername = function (username) {
return this.findOne({ username });
};
const User = mongoose.model('User', UserSchema);
export default User;
인스턴스 메서드와 다른 점은 스태틱 함수에서의 this는 모델을 가리킵니다. 지금 여기선 User를 가리킬 겁니다.
회원가입 구현하기
// src/api/auth/auth.ctrl.js
...
/**
* POST /api/auth/register
* {
* username: 'velopert',
* password: 'mypass123',
* }
*/
export const register = async (ctx) => {
// 회원가입
// request body 검증하기
const schema = Joi.object().keys({
username: Joi.string().alphanum().min(3).max(20).required(),
password: Joi.string().required(),
});
const result = schema.validate(ctx.request.body);
if (result.error) {
ctx.status = 400;
ctx.body = result.error;
return;
}
const { username, password } = ctx.request.body;
try {
// username이 이미 존재하는지 확인
const exists = await User.findByUsername(username);
if (exists) {
ctx.status = 409; // Conflict
return;
}
const user = new User({
username,
});
await user.setPassword(password); // 비밀번호 설정
await user.save(); // 데이터베이스에 저장
// 응답할 데이터에서 hashedPassword 필드 제거
const data = user.toJSON();
delete data.hashedPassword;
ctx.body = data;
} catch (e) {
ctx.throw(500, e);
}
};
...
회원가입을 할 때 중복되는 계정이 생기지 않도록 기존에 해당 username이 존재하는지 확인했습니다. 이 작업은 findByUsername스태틱 메서드를 사용해 처리했습니다. 그리고 비밀번호를 설정하는 과정에서는 setPassword인스턴스 함수를 사용했습니다.
이렇게 스태틱 또는 인스턴스 함수에서 해야 하는 작업들은 이 API함수 내부에서 직접 구현해도 상관없지만, 이렇게 메서드들을 만들어서 사용하면 가독성도 좋고 추후 유지 보수를 할 때도 도움이 됩니다.
함수의 마지막 부분에서는 hashedPassword필드가 응답되지 않도록 데이터를 JSON으로 변환한 후 delete를 통해 해당 필드를 지워 주었는데요, 앞으로 비슷한 작업을 자주 하게 될 것입니다. 따라서 이 작업을 serialize라는 인스턴스 함수로 따로 만들어 주겠습니다.
// src/models/user.js
...
UserSchema.methods.serialize = function () {
const data = this.toJSON();
delete data.hashedPassword;
return data;
};
...
// src/api/auth/auth.ctrl.js
...
/**
* POST /api/auth/register
* {
* username: 'velopert',
* password: 'mypass123',
* }
*/
export const register = async (ctx) => {
// 회원가입
// request body 검증하기
const schema = Joi.object().keys({
username: Joi.string().alphanum().min(3).max(20).required(),
password: Joi.string().required(),
});
const result = schema.validate(ctx.request.body);
if (result.error) {
ctx.status = 400;
ctx.body = result.error;
return;
}
const { username, password } = ctx.request.body;
try {
// username이 이미 존재하는지 확인
const exists = await User.findByUsername(username);
if (exists) {
ctx.status = 409; // Conflict
return;
}
const user = new User({
username,
});
await user.setPassword(password); // 비밀번호 설정
await user.save(); // 데이터베이스에 저장
// hashedPassword를 제거한 user문서를 반환
ctx.body = user.serialize();
} catch (e) {
ctx.throw(500, e);
}
};
...
잘 응답하는 것을 볼 수 있다.
로그인 구현하기
// src/api/auth/auth.ctrl.js
...
/**
* POST /api/auth/login
* {
* username: 'velopert',
* password: 'mypass123',
* }
*/
export const login = async (ctx) => {
// 로그인
const { username, password } = ctx.request.body;
// username, password가 없으면 에러 처리
if (!username || !password) {
ctx.status = 401;
return;
}
try {
const user = await User.findByUsername(username);
// 계정이 존재하지 않으면 에러 러리
if (!user) {
ctx.status = 401;
return;
}
const valid = await user.checkPassword(password);
// 잘못된 비밀번호
if (!valid) {
ctx.status = 401;
return;
}
ctx.body = user.serialize();
} catch (e) {
ctx.throw(500, e);
}
};
...
이 API에서는 username, password값이 제대로 전달되지 않으면 에러로 처리합니다. 그리고 findByUsername을 통해 사용자 데이터를 찾고, 만약 사용자 데이터가 없으면 역시 에러로 처리합니다. 계정이 유효하다면 checkPassword를 통해 비밀번호를 검사하고 성공했을 때는 계정 정보를 응답합니다.
잘 작동하는 것을 볼 수 있습니다.
토큰 발급 및 검증하기
이제 클라이언트에서 사용자가 로그인 정보를 지니고 있을 수 있도록 서버에서 토큰을 발급해 주겠습니다.
< 비밀키 설정하기
.env파일을 열어서 JWT토큰을 만들 때 사용할 비밀키를 만듭니다. 이 비밀키는 문자열로 아무거나 입력하면 됩니다. 저는
$openssl rand -hex 64의 값으로 설정하였습니다.
PORT=4000
MONGO_URI=mongodb://hyunseo:gustj486!!@localhost:
JWT_SECRET=98e27564126475eee9f799ca1af979c327803cfecee520989a851b0ff0576965c0e13c51d84c2a8da96aa36b68f17f931c35457543c7edc46f236e754d976ddf
// src/models/user.js
...
UserSchema.methods.generateToken = function () {
const token = jwt.sign(
// 첫 번쨰 파라미터에는 토큰 안에 집어 넣고 싶은 데이터를 넣습니다.
{
_id: this.id,
username: this.username,
},
// 두 번째 파라미터에는 JWT암호를 넣습니다.
process.env.JWT_SECRET,
{
// 7일동안 유효함
expiresIn: '7d',
},
);
};
...
token을 만드는 generateToken인스턴스 메소드를 선언해주고 이제 회원가입과 로그인에 성공했을 때 토큰을 사용자에게 전달해 주겠습니다. 사용자가 브라우저에서 토큰을 사용할 때는 주로 두 가지 방법을 사용합니다. 첫 번째는 브라우저의 localStorage혹은 sessionStorage에 담아서 사용하는 방법이고, 두 번째는 브라우저의 쿠키에 담아서 사용하는 방법입니다.
브라우저의 localStorage혹은 sessionStorage에 토큰을 담으면 사용하기에 매우 편리하고 구현하기도 쉽습니다. 하지만 만약 누군가가 페이지에 악성 스크립트를 삽입한다면 쉽게 토큰을 탈취할 수 있습니다. (이러한 XSS라고 부릅니다).
쿠키에 담아도 같은 문제가 발생할 수 있지만, httpOnly라는 속성을 활성화하면 자바스크립트를 통해 쿠키를 조회할 수 없으므로 악성 스크립트로부터 안전합니다. 그 대신 CSRF라는 공격에 취약해질 수 있습니다. 이 공격은 토큰을 쿠키에 담으면 사용자가 서버로 요청을 할 때마다 무조건 토큰이 함꼐 전달되는 점을 이용해서 사용자가 모르게 원하지 않는 API요청을 하게 만듭니다.
// src/api/auth/auth.ctrl.js
...
/**
* POST /api/auth/register
* {
* username: 'velopert',
* password: 'mypass123',
* }
*/
export const register = async (ctx) => {
// 회원가입
// request body 검증하기
const schema = Joi.object().keys({
username: Joi.string().alphanum().min(3).max(20).required(),
password: Joi.string().required(),
});
const result = schema.validate(ctx.request.body);
if (result.error) {
ctx.status = 400;
ctx.body = result.error;
return;
}
const { username, password } = ctx.request.body;
try {
// username이 이미 존재하는지 확인
const exists = await User.findByUsername(username);
if (exists) {
ctx.status = 409; // Conflict
return;
}
const user = new User({
username,
});
await user.setPassword(password); // 비밀번호 설정
await user.save(); // 데이터베이스에 저장
// hashedPassword를 제거한 user문서를 반환
ctx.body = user.serialize();
const token = user.generateToken();
ctx.cookies.set('access_token', token, {
maxAge: 100 * 60 * 60 * 24 * 7, // 7일
httpOnly: true,
});
} catch (e) {
ctx.throw(500, e);
}
};
/**
* POST /api/auth/login
* {
* username: 'velopert',
* password: 'mypass123',
* }
*/
export const login = async (ctx) => {
// 로그인
const { username, password } = ctx.request.body;
// username, password가 없으면 에러 처리
if (!username || !password) {
ctx.status = 401;
return;
}
try {
const user = await User.findByUsername(username);
// 계정이 존재하지 않으면 에러 러리
if (!user) {
ctx.status = 401;
return;
}
const valid = await user.checkPassword(password);
// 잘못된 비밀번호
if (!valid) {
ctx.status = 401;
return;
}
ctx.body = user.serialize();
const token = user.generateToken();
ctx.cookies.set('access_token', token, {
maxAge: 10000 * 60 * 60 * 24 * 7, // 7일
httpOnly: true,
});
} catch (e) {
ctx.throw(500, e);
}
};
...
Set-Cookie헤더가 보이게 됩니다.
토큰 검증하기
// src/lib/jwtMiddleware.js
/* eslint-disable no-undef */
import jwt from 'jsonwebtoken';
const jwtMiddleware = (ctx, next) => {
const token = ctx.cookies.get('access_token');
if (!token) {
return next();
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
ctx.state.user = {
_id: decoded._id,
username: decoded.username,
};
console.log(decoded);
return next();
} catch (e) {
// 토큰 검증 실패
return next();
}
};
export default jwtMiddleware;
// src/api/auth/auth.ctrl.js
...
export const check = async (ctx) => {
// 로그인 상태 확인
const { user } = ctx.state;
if (!user) {
// 로그인 중 아님
ctx.status = 401; //Unauthorized
return;
}
ctx.body = user;
};
...
토큰 재발급하기
{
_id: '622d8b49345d7127b65c9fda',
username: 'hyunseo20021010',
iat: 1647155748,
exp: 1647760548
}
jwtMiddleware를 통해 해석된 이후에 위와 같은 결과물이 출력되는데 iat의 값은 토큰이 언제 만들어졌는지 알려 주는 값이고, exp 값은 언제 만료되는지 알려주는 값입니다.
exp에 표현된 날짜가 3.5일 미만이라면 토큰을 새로운 토큰으로 재발급해 주는 기능을 구현해 보겠습니다.
// src/lib/jwtMiddleware.js
/* eslint-disable no-undef */
import jwt from 'jsonwebtoken';
import User from '../models/user';
const jwtMiddleware = async (ctx, next) => {
const token = ctx.cookies.get('access_token');
if (!token) {
return next();
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
ctx.state.user = {
_id: decoded._id,
username: decoded.username,
};
const now = Math.floor(Date.now() / 1000);
if (decoded.exp - now < 60 * 60 * 24 * 3.5) {
console.log('access_token 재발급');
const user = await User.findById(decoded._id);
const token = user.generateToken();
ctx.cookies.set('access_token', token, {
maxAge: 1000 * 60 * 60 * 24 * 6, // 7일
httpOnly: true,
});
}
console.log(decoded);
return next();
} catch (e) {
// 토큰 검증 실패
return next();
}
};
export default jwtMiddleware;
// src/models/user.js
...
UserSchema.methods.generateToken = function () {
const token = jwt.sign(
// 첫 번쨰 파라미터에는 토큰 안에 집어 넣고 싶은 데이터를 넣습니다.
{
_id: this.id,
username: this.username,
},
// 두 번째 파라미터에는 JWT암호를 넣습니다.
process.env.JWT_SECRET,
{
// 3일동안 유효함
expiresIn: '3d',
},
);
return token;
};
...
expiresIn을 '3d'로 바꾸어 매번 token이 재발급 되도록 바꾸었더니 토큰이 정상적으로 매번 재발급 되는 것을 확인할 수 있습니다. 이제 다시 '7d'로 바꾸었습니다.
logout 구현
// src/api/auth/auth.ctrl.js
...
/**
* POST /api/auth/logout
*/
export const logout = async (ctx) => {
// 로그 아웃
ctx.cookies.set('access_token');
ctx.status = 204;
};
...
logout은 단순히 위와같이 구현하였습니다.
post API에 회원 인증 시스템 도입
새 포스트는 이제 로그인해야만 작성할 수 있고, 삭제와 수정은 작성자만 할 수 있도록 구현해 보겠습니다. 또한 각 포소트를 어떤 사용자가 작성했는지 알아야 하기 때문에 기존의 Post스키마를 수정하겠습니다.
< 로그인 했을 때만 API를 사용할 수 있게 하기 >
// src/lib/checkLoggedIn.js
const checkLoggedIn = (ctx, next) => {
if (!ctx.state.user) {
ctx.status = 401;
return;
}
return next();
};
export default checkLoggedIn;
// src/api/posts/index.js
import Router from 'koa-router';
import * as postsCtrl from './posts.ctrl';
import checkLoggedIn from '../../lib/checkLoggedIn';
const posts = new Router();
posts.get('/', postsCtrl.list);
posts.post('/', checkLoggedIn, postsCtrl.write);
const post = new Router();
post.get('/', postsCtrl.read);
post.delete('/', checkLoggedIn, postsCtrl.remove);
post.patch('/', checkLoggedIn, postsCtrl.update);
posts.use('/:id', postsCtrl.checkObjectId, post.routes());
export default posts;
간단히 미들웨어를 추가함으로서 수행해 주었다.
< 포스트 작성 시 사용자 정보 넣기 >
우선 포스트 스키마를 바꾸어 주어야 한다.
// src/models/post.js
import mongoose from 'mongoose';
const { Schema } = mongoose;
const PostSchema = new Schema({
title: String,
body: String,
tags: [String],
publishedDate: {
type: Date,
default: Date.now(),
},
user: {
_id: mongoose.Types.ObjectId,
username: String,
},
});
const Post = mongoose.model('Post', PostSchema);
export default Post;
그 후, POST /api/posts에서 작성자 정보까지 같이 저장하게끔 바꾸어 준다.
// src/api/posts/posts.ctrl.js
...
/**
* 포스트 작성
* POST /api/posts
* { title, body }
*/
export const write = async (ctx) => {
// REST API의 Request Body는 ctx.request.body에서 조회할 수 있습니다.
const schema = Joi.object().keys({
// 객체가 다음 필드를 가지고 있음을 검증
title: Joi.string().required(),
body: Joi.string().required(),
tags: Joi.array().items(Joi.string()).required(),
});
const result = schema.validate(ctx.request.body);
if (result.error) {
ctx.status = 400;
ctx.body = result.error;
return;
}
const { title, body, tags } = ctx.request.body;
const post = new Post({
title,
body,
tags,
user: ctx.state.user,
});
try {
await post.save();
ctx.body = post;
} catch (e) {
ctx.throw(500, e);
}
};
...
작성자 정보까지 한번에 들어간 것을 볼 수 있다.
< 포스트 수정 및 삭제 시 권한 확인하기 >
마지막으로 작성자만 포스트를 수정하거나 삭제할 수 있도록 구현해 보겠습니다.
// src/api/posts/posts.ctrl.js
...
export const getPostById = async (ctx, next) => {
const { id } = ctx.params;
if (!ObjectId.isValid(id)) {
ctx.status = 400;
return;
}
try {
const post = await Post.findById(id);
// 포스트가 존재하지 않을 때
if (!post) {
ctx.status = 404;
return;
}
ctx.state.post = post;
} catch (e) {
ctx.throw(500, e);
}
return next();
};
export const checkOwnPost = (ctx, next) => {
const { user, post } = ctx.state;
if (post.user._id.toString() !== user._id) {
ctx.status = 403;
return;
}
return next();
};
/**
* 특정 포스트 조회
* GET /api/posts/:id
*/
export const read = async (ctx) => {
ctx.body = ctx.state.post;
};
...
// src/api/posts/index.js
import Router from 'koa-router';
import * as postsCtrl from './posts.ctrl';
import checkLoggedIn from '../../lib/checkLoggedIn';
const posts = new Router();
posts.get('/', postsCtrl.list);
posts.post('/', checkLoggedIn, postsCtrl.write);
const post = new Router();
post.get('/', postsCtrl.read);
post.delete('/', checkLoggedIn, postsCtrl.checkOwnPost, postsCtrl.remove);
post.patch('/', checkLoggedIn, postsCtrl.checkOwnPost, postsCtrl.update);
posts.use('/:id', postsCtrl.getPostById, post.routes());
export default posts;
getPostById는 그냥 파라미터로 전달된 id에 해당되는 post를 찾아서 ctx.state.post에 저장하는 역할입니다. 또한 checkOwnPost는 해당 포스트가 현제 접속되어 있는 유저의 것인지 확인하는 미들웨어 입니다.
만약 새로운 계정을 만들고, 그 계정을 사용하여 다른 계정으로 작성된 포스트를 삭제하려고 한다면, 403Forbindden에러가 나타나게 될 것입니다.
< username / tags로 포스트 필터링 하기 >
특정 사용자가 작성한 포스트만 조회하거나 특정 태그가 있는 포스트만 조회하는 기능을 만들어 보겠습니다.
// src/api/posts/posts.ctrl.js
...
/**
* 포스트 목록 조회
* GET /api/posts?username=&tag=&page=
*/
export const list = async (ctx) => {
const page = parseInt(ctx.query.page || '1', 10);
if (page < 1) {
ctx.status = 400;
return;
}
const { tag, username } = ctx.query;
// tag, username값이 유효하면 객체 안에 넣고, 그렇지 않으면 넣지 않음
const query = {
...(username ? { 'user.username': username } : {}),
...(tag ? { tags: tag } : {}),
};
console.log('query >> ', query);
try {
const posts = await Post.find({})
.sort({ _id: -1 })
.limit(10)
.skip((page - 1) * 10)
.lean()
.exec();
const postCount = await Post.countDocuments(query).exec();
ctx.set('Last-page', Math.ceil(postCount / 10));
ctx.body = posts.map((post) => ({
...post,
body:
post.body.length < 200 ? post.body : `${post.body.slice(0, 200)}...`,
}));
} catch (e) {
ctx.throw(500, e);
}
};
...
username혹은 tag값이 유효할 때만 객체 안에 해당 값을 넣어야 합니다. 이를 postman에 적절한 쿼리문과 함꼐 날려주면
정상 작동한다.
'React > ReactJs' 카테고리의 다른 글
React - FrontEnd project - 2 (0) | 2022.03.15 |
---|---|
React - FrontEnd Project - 1 (0) | 2022.03.15 |
React - API의 활용을 위한 prototype (0) | 2022.03.13 |
React - mongoDB ( mongoose ) (0) | 2022.03.12 |
React - koa FrameWork (0) | 2022.03.09 |