저번에 만들었던 SNS 서비스인 NodeBird 앱을 사용하여 웹 API 서버를 만들어보겠습니다. API 는 Application Programming Interface 로 다른 애플리케이션에서 현재 프로그램의 기능을 사용할 수 있게 허용하는 접점을 의미합니다. 따라서 웹 API는 다른 웹 서비스의 기능을 사용하거나 자원을 가져올 수 있는 창구입니다. 제공하고 싶은 부분만 API를 열어 둘 수 있고 인증된 사람만 일정 횟수 내에서 가져가게 제한을 둘 수도 있습니다.
1. 웹 API 서버 만들기
package.json 파일을 생성한 후 "npm install" 을 콘솔에 입력하여 패키지를 설치합니다.
{
"name": "nodebird-api",
"version": "0.0.1",
"description": "NodeBird API 서버",
"main": "app.js",
"scripts": {
"start": "nodemon app",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Zero Cho",
"license": "ISC",
"dependencies": {
"bcrypt": "^4.0.1",
"cookie-parser": "^1.4.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-session": "^1.17.1",
"jsonwebtoken": "^8.5.1",
"morgan": "^1.10.0",
"mysql2": "^2.1.0",
"nunjucks": "^3.2.1",
"passport": "^0.4.1",
"passport-kakao": "1.0.0",
"passport-local": "^1.0.0",
"sequelize": "^5.21.7",
"uuid": "^7.0.3"
},
"devDependencies": {
"nodemon": "^2.0.3"
}
}
저번에 만들었던 NodeBird 앱의 파일들을 재사용하겠습니다. 필요한 파일들을 압축파일로 첨부해뒀습니다. config.json 파일은 자신의 데이터베이스에 맞게 수정해주세요!!
도메인을 등록하는 기능이 생겼으므로 도메인 모델을 추가하겠습니다. models 폴더 안에 domain.js 파일을 만듭니다. type 컬럼을 보면 ENUM 이라는 속성을 갖고 잇는데 이것은 넣을 수 있는 값을 제한하는 데이터 형식입니다. type 컬럼에는 'free' 나 'premium' 중 하나의 값만 가질 수 있습니다. 클라이언트 비밀키는 NodeBird의 API를 사용할 때 필요한 비밀키로 유츌되지 않도록 주의해야 합니다. UUID는 충돌 가능성이 매우 적은 랜덤한 문자열입니다.
// models/domain.js
const Sequelize = require('sequelize');
module.exports = class Domain extends Sequelize.Model {
static init(sequelize) {
return super.init({
host: { // 인터넷 주소
type: Sequelize.STRING(80),
allowNull: false,
},
type: { // 도메인 종류
type: Sequelize.ENUM('free', 'premium'),
allowNull: false,
},
clientSecret: { // 클라이언트 비밀키
type: Sequelize.UUID,
allowNull: false,
},
}, {
sequelize,
timestamps: true,
paranoid: true,
modelName: 'Domain',
tableName: 'domains',
});
}
static associate(db) {
db.Domain.belongsTo(db.User); // User 모델과 1:N 관계
}
};
도메인 모델을 시퀄라이즈, 사용자 모델과 연결하겠습니다. "추가" 라고 주석이 있는 부분을 추가해주면 됩니다.
// models/index.js
const Sequelize = require('sequelize');
const env = process.env.NODE_ENV || 'development';
const config = require('../config/config')[env];
const User = require('./user');
const Post = require('./post');
const Hashtag = require('./hashtag');
const Domain = require('./domain'); // 추가
const db = {};
const sequelize = new Sequelize(
config.database, config.username, config.password, config,
);
db.sequelize = sequelize;
db.User = User;
db.Post = Post;
db.Hashtag = Hashtag;
db.Domain = Domain; // 추가
User.init(sequelize);
Post.init(sequelize);
Hashtag.init(sequelize);
Domain.init(sequelize); // 추가
User.associate(db);
Post.associate(db);
Hashtag.associate(db);
Domain.associate(db); // 추가
module.exports = db;
// models/user.js
const Sequelize = require('sequelize');
module.exports = class User extends Sequelize.Model {
// .. 생략
static associate(db) {
db.User.hasMany(db.Post);
db.User.belongsToMany(db.User, {
foreignKey: 'followingId',
as: 'Followers',
through: 'Follow',
});
db.User.belongsToMany(db.User, {
foreignKey: 'followerId',
as: 'Followings',
through: 'Follow',
});
db.User.hasMany(db.Domain); // 추가
}
};
로그인 화면을 만들겠습니다. 프론트 부분이라서 파일로 첨부하겠습니다.
로그인 화면의 라우터를 만들겠습니다. routes 폴더에 index.js 파일을 생성합니다. GET / 라우터는 접속시 로그인 화면을 보여줍니다. 사용자가 로그인을 하면 도메인 등록 화면으로 바뀝니다. GET /domain은 도메인 등록 라우터입니다. clientSecret 값은 uuid 패키지를 통해 생성합니다.
// routes/index.js
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const { User, Domain } = require('../models');
const router = express.Router();
router.get('/', async (req, res, next) => {
try {
const user = await User.findOne({
where: { id: req.user && req.user.id || null },
include: { model: Domain },
});
res.render('login', {
user,
domains: user && user.Domains,
});
} catch (err) {
console.error(err);
next(err);
}
});
// 도메인 등록 라우터
router.post('/domain', async (req, res, next) => {
try {
await Domain.create({
UserId: req.user.id,
host: req.body.host,
type: req.body.type,
clientSecret: uuidv4(),
});
res.redirect('/');
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
여기까지 한 후 콘솔에 npm start 를 입력하여 로컬에 접속하면 다음과 같은 화면이 뜹니다. 로그인 화면에서 로그인을 한 후 도메인을 등록하면 클라이언트 비밀키를 발급받을 수 있습니다. 추후에 클라이언트 비밀키를 사용해야하므로 저장해두세요!!


2. JWT 토큰으로 인증하기
JWT 토큰이란?
JWT는 JSON Web Token 으로 JSON 형식의 데이터를 저장하는 토큰입니다. JWT는 다음과 같이 세부분으로 이루어져 있습니다.
- 헤더(HEADER) - 토큰 종류와 해시 알고리즘 정보가 들어 있음
- 페이로드(PAYLOAD) - 토큰의 내용물이 인코딩된 부분
- 시그니처(SIGNATURE) - 일련의 문자열이며 시그니처를 통해 토큰이 변조되었는지 여부를 확인할 수 있음
JWT 모듈을 설치한 후 .env 파일에 토큰의 비밀키를 추가합니다.
npm install jsonwebtoken
COOKIE_SECRET=nodebirdsecret
KAKAO_ID=5d4daf57becfd72fd9c919882552c4a6
JWT_SECRET=jwtSecret // 추가
토큰을 인증하는 미들웨어를 만듭니다. middlewares.js 파일을 수정합니다. jwt.verify 는 토큰을 검증하는 메서드입니다. 첫번재 인수는 토큰이고, 두번재 인수는 토큰의 비밀키입니다. 토큰 검증에 성공하면 req.decoded 에 저장되는데 토큰의 내용은 사용자의 아이디, 닉네임, 발급자 등 입니다.
const jwt = require('jsonwebtoken');
//.. 생략
exports.verifyToken = (req, res, next) => {
try {
req.decoded = jwt.verify(req.headers.authorization, process.env.JWT_SECRET);
return next();
} catch (error) {
if (error.name === 'TokenExpiredError') { // 유효기간 초과
return res.status(419).json({
code: 419,
message: '토큰이 만료되었습니다',
});
}
return res.status(401).json({
code: 401,
message: '유효하지 않은 토큰입니다',
});
}
};
토큰을 발급하는 라우터를 만들겠습니다. routes 폴더 안에 v1.js 파일을 만듭니다. POST /v1/token은 토큰을 발급하는 라우터이고, GET /v1/text는 토큰을 테스트하는 라우터입니다.
// routes/v1.js
const express = require('express');
const jwt = require('jsonwebtoken');
const { verifyToken } = require('./middlewares');
const { Domain, User } = require('../models');
const router = express.Router();
// 토큰 발급 라우터
router.post('/token', async (req, res) => {
const { clientSecret } = req.body;
try {
const domain = await Domain.findOne({ // 클라이언트 비밀키로 도메인 조회
where: { clientSecret },
include: {
model: User,
attribute: ['nick', 'id'],
},
});
if (!domain) { // 등록된 도메인이 아니라면
return res.status(401).json({
code: 401,
message: '등록되지 않은 도메인입니다. 먼저 도메인을 등록하세요',
});
}
// 등록된 도메인이라면
const token = jwt.sign({ // 토큰 발급 - jwt.sign(토큰 내용, 토큰 비밀키, 토큰 설정)
id: domain.User.id,
nick: domain.User.nick,
}, process.env.JWT_SECRET, {
expiresIn: '1m', // 유효기간 - 1분
issuer: 'nodebird', // 발급자
});
return res.json({
code: 200,
message: '토큰이 발급되었습니다',
token,
});
} catch (error) {
console.error(error);
return res.status(500).json({
code: 500,
message: '서버 에러',
});
}
});
// 토큰 테스트 라우터
router.get('/test', verifyToken, (req, res) => {
res.json(req.decoded);
});
module.exports = router;
app.js 에 토큰 발급 라우터를 연결합니다. "추가" 라고 주석친 부분을 추가하면 됩니다. 여기까지 하면 웹 API 서버가 완성됩니다.
// .. 생략
const v1 = require('./routes/v1'); // 추가
const authRouter = require('./routes/auth');
const indexRouter = require('./routes');
const { sequelize } = require('./models');
const passportConfig = require('./passport');
// .. 생략
app.use('/v1', v1); // 추가
app.use('/auth', authRouter);
app.use('/', indexRouter);
// .. 생략
3. 다른 서비스에서 웹 API 호출하기
위에서 만든 웹 API 호출하는 다른 서비스를 만들어보겠습니다. 다른 서비스의 이름은 NodeCat 이라고 하겠습니다. 이 서비스를 만드는데 기본적으로 필요한 파일들은 첨부하겠습니다.
.env 파일을 만들어 아까 발급받은 클라이언트 비밀키를 저장해둡니다.
COOKIE_SECRET=nodecat
CLIENT_SECRET=8193a477-1160-4610-ad44-af7b39834c27
토큰 테스트 라우터를 만들겠습니다. routes 폴더 안에 index.js 파일을 생성합니다.
const express = require('express');
const axios = require('axios');
const router = express.Router();
// 토큰 테스트 라우터
router.get('/test', async (req, res, next) => {
try {
if (!req.session.jwt) { // 세션에 토큰이 없으면 토큰 발급 시도
const tokenResult = await axios.post('http://localhost:3000/v1/token', {
clientSecret: process.env.CLIENT_SECRET,
});
if (tokenResult.data && tokenResult.data.code === 200) { // 토큰 발급 성공
req.session.jwt = tokenResult.data.token; // 세션에 토큰 저장
} else { // 토큰 발급 실패
return res.json(tokenResult.data); // 발급 실패 사유 응답
}
}
// 발급받은 토큰 테스트
const result = await axios.get('http://localhost:3000/v1/test', {
headers: { authorization: req.session.jwt },
});
return res.json(result.data);
} catch (error) {
console.error(error);
if (error.response.status === 419) { // 토큰 만료 시
return res.json(error.response.data);
}
return next(error);
}
});
module.exports = router;
웹 API 서버와 다른 서비스인 Nodecat 콘솔에 모두 npm start 를 한 후 "http://localhost:4000/test" 에 접속하면 다음과 같은 화면을 볼 수 있습니다. 1분 후에 다시 접속하면 토큰이 만료됐다는 메시지가 뜰 것입니다.


'Back-end > Node.js' 카테고리의 다른 글
[Node.js] 노드 서비스 테스트 하기 (0) | 2021.08.03 |
---|---|
[Node.js] 웹 API 서버 만들기 (2) (0) | 2021.08.01 |
[Node.js] 익스프레스로 SNS 서비스 만들기 (3) (0) | 2021.07.27 |
[Node.js] 익스프레스로 SNS 서비스 만들기 (2) (0) | 2021.07.27 |
[Node.js] 익스프레스로 SNS 서비스 만들기 (1) (0) | 2021.07.25 |