일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 참조형변수
- EnhancedFor
- 추상메서드
- GRANT VIEW
- 컬렉션 타입
- exception
- 사용자예외클래스생성
- 한국건설관리시스템
- Java
- 예외미루기
- 예외처리
- 인터페이스
- cursor문
- oracle
- 집합_SET
- 어윈 사용법
- 컬렉션프레임워크
- 자바
- NestedFor
- 생성자오버로드
- 오라클
- 정수형타입
- 객체 비교
- 환경설정
- 제네릭
- 다형성
- abstract
- 자동차수리시스템
- 대덕인재개발원
- 메소드오버로딩
- Today
- Total
거니의 velog
(25) JWT를 통한 회원 인증 시스템 구현하기 5 본문
5. posts API에 회원 인증 시스템 도입하기
* 이번에는 기존에 구현했던 posts API에 회원 인증 시스템을 도입해 보겠다. 새 포스트는 이제 로그인해야만 작성할 수 있고, 삭제와 수정은 작성자만 할 수 있도록 구현할 것이다.
* 각각의 함수를 직접 수정해서 이 기능을 구현해도 되지만, 여기서는 미들웨어를 만들어서 관리해 보겠다. 또한, 각 포스트를 어떤 사용자가 작성했는지 알아야 하기 때문에 기존의 Post 스키마를 수정해 주어야 한다.
(1) 스키마 수정하기
* 스키마에 사용자 정보를 넣어 주자. 보통 MariaDB, PostgreSQL 같은 관계형 데이터베이스에서는 데이터의 id만 관계 있는 데이터에 넣어 주는 반면, MongoDB에서는 필요한 데이터를 통째로 집어 넣는다.
* 여기서는 Post 스키마 안에 사용자의 id와 username을 전부 넣어 주어야 한다.
* post 모델 파일을 열어서 다음과 같이 수정해 주자.
[src/models/post.js]
import mongoose, { Schema } from '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;
(2) posts 컬렉션 비우기
* 이제 포스트 데이터에는 사용자 정보가 필요하다. 우리가 이전에 생성한 데이터들은 더 이상 유효하지 않으므로 모두 삭제해 주자. Compass를 열어서 좌측 컬렉션 리스트를 보면 posts 컬렉션이 있다. 오른쪽의 휴지통 아이콘을 누르자. 컬렉션을 삭제하려면 컬렉션 이름을 한번 더 입력해야 한다.
(3) 로그인했을 때만 API를 사용할 수 있게 하기
* checkLoggedIn이라는 미들웨어를 만들어서 로그인해야만 글쓰기, 수정, 삭제를 할 수 있도록 구현해 보자.
* lib 디렉터리에 checkLoggedIn.js 파일을 생성하고 다음 미들웨어를 작성해 보자.
* 이 미들웨어를 lib 디렉터리에 저장하는 이유는 다른 라우트에서도 사용될 가능성이 있기 때문이다. 물론 이 프로젝트에서 auth를 제외한 라우트는 posts가 유일하기 때문에 auth.ctrl.js에서 구현해도 상관 없지만, 로그인 상태 확인 작업은 자주 사용하는 기능이므로 더 쉽게 재사용할 수 있도록 lib 디렉터리에 작성하는 것이다.
const checkLoggedIn = (ctx, next) => {
if (!ctx.state.user) {
ctx.status = 401; // Unauthorized
return;
}
return next();
};
export default checkLoggedIn;
* 정말 짧고 간단하다. 이 미들웨어는 로그인 상태가 아니라면 401 HTTP Status를 반환하고, 그렇지 않으면 그다음 미들웨어들을 실행한다.
* 이제 이 미들웨어를 posts 라우터에서 사용해 보자.
[src/api/posts/index.js]
import Router from 'koa-router';
import * as postsCtrl from './posts.ctrl.js';
import checkLoggedIn from '../../lib/checkLoggedIn';
const posts = new Router();
posts.get('/', postsCtrl.list);
posts.post('/', checkLoggedIn, postsCtrl.write);
const post = new Router(); // /api/posts/:id
post.get('/', postsCtrl.read);
post.delete('/', checkLoggedIn, postsCtrl.remove);
post.patch('/', checkLoggedIn, postsCtrl.update);
posts.use('/:id', postsCtrl.checkObjectId, post.routes());
export default posts;
* 어떤가? 미들웨어를 만드니 로직을 재사용하기 참 편리해 졌다.
(4) 포스트 작성 시 사용자 정보 넣기
* 로그인된 사용자만 포스트를 작성할 수 있게 했으니, 지금부터는 포스트를 작성할 때 사용자 정보를 넣어서 데이터베이스에 저장하도록 구현해 보겠다.
* posts.ctrl.js의 write 함수를 다음과 같이 수정해 보자.
[src/api/posts/posts.ctrl.js]
export const write = async (ctx) => {
(...)
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);
}
};
* 이제 Postman으로 포스트 작성 API를 요청해 보자. 조금 전에 로그아웃 했다면, 다시 로그인하고 API를 요청하면 된다.
* 다음과 같이 API를 요청했을 때,
http://localhost:4000/api/posts
{
"title" : "제목",
"body" : "내용",
"tags" : ["태그1", "태그2"]
}
* 이렇게 사용자 정보가 들어간 상태로 잘 등록되는지 응답을 확인해 보자.
{
"tags": [
"태그1",
"태그2"
],
"_id": "65d5b2182060101e88f5a80e",
"title": "제목",
"body": "내용",
"user": {
"_id": "65d59292f176c733b8038e00",
"username": "bbbb7788"
},
"publishedDate": "2024-02-21T08:19:36.267Z",
"__v": 0
}
(5) 포스트 수정 및 삭제 시 권한 확인하기
* 마지막으로 작성자만 포스트를 수정하거나 삭제할 수 있도록 구현해 보자. 이 작업을 미들웨어에서 처리하고 싶다면 id로 포스트를 조회하는 작업도 미들웨어로 해 주어야 한다. 따라서 기존에 만들었던 checkObjectId를 getPostById로 바꾸고, 해당 미들웨어에서 id로 포스트를 찾은 후 ctx.state에 담아 줄 것이다.
[src/api/posts/posts.ctrl.js] => 기존 checkObjectId 함수명을 getPostById로 수정
export const getPostById = async (ctx, next) => {
const { id } = ctx.params;
if (!ObjectId.isValid(id)) {
ctx.status = 400; // Bad Request
return;
}
try {
const post = await Post.findById(id);
// 포스트가 존재하지 않을 때
if (!post) {
ctx.status = 404; // Not Found
return;
}
ctx.state.post = post;
return next();
} catch (e) {
ctx.throw(500, e);
}
};
* 미들웨어 이름과 코드를 수정한 뒤 posts 라우터에도 반영해 주자.
[src/api/posts/index.js]
(...)
posts.use('/:id', postsCtrl.getPostById, post.routes());
export default posts;
* 그 다음에는 read 함수 내부에서 id로 포스트를 찾는 코드를 간소화해 준다.
[src/api/posts/posts.ctrl.js]
/*
GET /api/posts/:id
*/
export const read = async (ctx) => {
ctx.body = ctx.state.post;
};
* 코드가 정말 짧아졌다.
* getPostById를 구현하고 적용했다면 이번에는 checkOwnPost라는 미들웨어를 만든다. 이 미들웨어는 id로 찾은 포스트가 로그인 중인 사용자가 작성한 포스트인지 확인해 준다. 만약 사용자의 포스트가 아니라면 403에러를 발생시킨다.
[src/api/posts/posts.ctrl.js]
export const checkOwnPost = (ctx, next) => {
const { user, post } = ctx.state;
if (post.user._id.toString() !== user._id) {
ctx.status = 403;
return;
}
return next();
};
* MongoDB에서 조회한 데이터와 id 값을 문자열과 비교할 때는 반드시 .toString()을 해 주어야 한다.
* 이어서 이 미들웨어를 수정 및 삭제 API에 적용하자. checkLoggedIn 다음 미들웨어로 등록해 주어야 한다.
[src/api/posts/index.js]
import Router from 'koa-router';
import * as postsCtrl from './posts.ctrl.js';
import checkLoggedIn from '../../lib/checkLoggedIn';
const posts = new Router();
posts.get('/', postsCtrl.list);
posts.post('/', checkLoggedIn, postsCtrl.write);
const post = new Router(); // /api/posts/:id
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;
* 이제 새로운 계정을 만든 다음, 그 계정을 사용하여 다른 계정으로 작성된 포스트를 삭제해 보자. 회원가입할 때 계정 정보는 마음대로 입력해도 된다.
* 403 Forbidden 에러가 잘 나타났는가?
* 이제 posts API에 회원 인증 시스템을 도입하는 과정을 모두 마쳤다!
6. username/tags로 포스트 필터링하기
* 이번에는 특정 사용자가 작성한 포스트만 조회하거나 특정 태그만 있는 포스트만 조회하는 기능을 만들어 보자.
* 먼저 조금 전에 새로 만든 계정으로 포스트를 작성한다. GET /api/posts에 요청을 해서 두 명의 사용자가 쓴 포스트가 있는지 확인한 뒤, 포스트 목록 조회 API를 다음과 같이 수정하자.
[src/api/posts/posts.ctrl.js]
/*
GET /api/posts
*/
export const list = async (ctx) => {
// query 는 문자열이기 때문에 숫자로 변환해주어야합니다.
// 값이 주어지지 않았다면 1 을 기본으로 사용합니다.
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 } : {}),
};
try {
const posts = await Post.find(query)
.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);
}
};
* 위 코드에서 query를 선언하는 방법이 조금 생소할 것이다.
const query = {
...(username ? { 'user.username': username } : {}),
...(tag ? { tags: tag } : {}),
};
* 이 코드는 username 혹은 tag 값이 유효할 때만 객체 안에 해당 값을 넣겠다는 것을 의미한다. 다음과 같은 형식으로 query 객체를 만들면 어떨까?
{
username,
tags: tag
}
* 이런 객체를 query로 사용한다면 요청을 받을 때 username이나 tag 값이 주어지지 않는다. 이 경우에는 undefined 값이 들어가게 된다. mongoose는 특정 필드가 undefined인 데이터를 찾게 되고, 결국 데이터를 조회할 수 없게 된다.
* 코드를 다 작성했으면 다음과 같이 username, tag 쿼리 파라미터를 URL에 포함시켜서 요청을 해 보자.
GET http://localhost:4000/api/posts?username=bbbb1234
GET http://localhost:4000/api/posts?tag=태그
* username과 tag에는 여러분이 테스트용으로 작성한 포스트에서 사용하는 값을 넣으면 된다.
7. 정리
* 이 장에서는 회원 인증 시스템을 구현하는 방법을 알아보고, 기존의 포스트 관련 API에 회원 인증 시스템을 도입했다. 도입하는 과정에서 반복되는 코드는 대부분 미들웨어로 처리해 주었는데, 앞으로 Koa를 통해 백엔드 개발을 할 때는 이렇게 미들웨어를 자주 만들어 가면서 개발하는 방법을 추천한다. 이로써 코드의 가독성과 재사용성이 모두 높아져서 유지 보수가 쉬워질 것이다.
* 다음 장에서는 이번에 만든 서버를 기반으로 리액트를 사용하여 블로그 웹 애플리케이션을 개발해 보면서 실제 프로젝트 개발 흐름을 학습해 보겠다.
'React_백엔드 프로그래밍' 카테고리의 다른 글
(24) JWT를 통한 회원 인증 시스템 구현하기 4 (0) | 2024.02.21 |
---|---|
(23) JWT를 통한 회원 인증 시스템 구현하기 3 (0) | 2024.02.21 |
(22) JWT를 통한 회원 인증 시스템 구현하기 2 (0) | 2024.02.21 |
(21) JWT를 통한 회원 인증 시스템 구현하기 1 (0) | 2024.02.21 |
(20) mongoose를 이용한 MongoDB 연동 실습 9 (0) | 2024.02.21 |