2023.12.25(월)
stateful
stateless
점 (.
)으로 구분된 세 부분으로 구성: xxxxx.yyyyy.zzzzz
Signature or encryption algorithm (HMAC SHA256 or RSA) = Signature 값을 만드는데 사용되는 알고리즘/암호화 방식
Type of token = JWT
{
"alg": "HS256",
"typ": "JWT"
}
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
⚠️ 변조로부터 보호되지만 이 정보는 Base64Url로 인코딩되어 있어 누구나 읽을 수 있기 때문에 암호화되지 않은 경우에는 JWT의 payload나 header 요소에 비밀 정보를 넣으면 안됨
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
Authorization: Bearer <token>
1. The application or client requests authorization to the authorization server. This is performed through one of the different authorization flows. For example, a typical OpenID Connect compliant web application will go through the /oauth/authorize
endpoint using the authorization code flow.
2. When the authorization is granted, the authorization server returns an access token to the application.
3. The application uses the access token to access a protected resource (like an API).
💡 JWT와 같은 token 방식의 경우 stateless하기 때문에 토큰이 만료될 때까지 사용자의 상태를 제어할 수 없다. 그래서 만료시간을 가깝게 잡아서 토큰의 수명을 아주 짧게 한다. 물론 사용자가 로그인을 자주해야 하므로 로그인을 하고 나면 토큰을 두 개 발급한다.
1. 수명이 몇 시간이나 몇 분 이하로 짧은 access token (매번 인가를 받을 때 사용)
2. 보통 2주 정도로 수명이 꽤 긴 refresh token (DB에도 저장)
client는 access token의 수명이 다하면 refresh token을 보내고 서버는 DB에 저장된 값과 client가 보낸 refresh token을 대조해서 맞으면 새로운 access token을 발급한다. 따라서 사용자의 상태를 제어하려면 refresh token을 조작하면 되지만 access token이 짧게나마 살아 있는 동안에는 여전히 제어가 불가능하기 때문에 한계가 있다.
jsonwebtoken 설치 : npm install jsonwebtoken
Usage
Basic
const jwt = require('jsonwebtoken'); // jwt 모듈 import
const privateKey = 'secret-key';
/**
* jwt.sign(payload, secretOrPrivateKey, [options, callback])
*/
let token = jwt.sign({ user: 'do0ori', mail: 'do0ori@mail.com' }, privateKey); // token 발행 (default: HS256)
console.log(token); // noTimestamp라는 option이 지정되지 않은 경우 생성된 jwt는 iat (issued at) claim을 포함
/**
* jwt.verify(token, secretOrPublicKey, [options, callback])
*/
let decoded = jwt.verify(token, privateKey);
console.log(decoded); // { user: 'do0ori', mail: 'do0ori@mail.com', iat: 1703489109 }
jwt.verify(token, 'wrong-key', function(err, decoded) {
if (err) console.log(err); // JsonWebTokenError: invalid signature
else console.log(decoded);
});
❓ token 발행 및 검증 시에 사용할 private key와 같이 외부에 유출되면 안되는 중요한 정보는 어떻게 안전하게 보관할까? ⇒
.env
사용
.env
= Node.js 내에서 필요한 값들을 정리해놓은 환경변수 파일(프로젝트 최상위 경로, root 폴더에 위치)⇒ 추후PRIVATE_KEY = "secret-key" # JWT 암호키
.env
파일은.gitignore
파일에 추가해 github에 올라가지 않도록 하면 됨!dotenv
모듈로.env
파일에 저장한 환경 변수 사용 가능
- dotenv 설치 :
npm install dotenv --save
- Usage
const dotenv = require('dotenv') dotenv.config() console.log(process.env.PRIVATE_KEY) // secret-key
const jwt = require('jsonwebtoken'); // jwt 모듈 import
const dotenv = require('dotenv'); // dotenv 모듈 import
dotenv.config();
/**
* jwt.sign(payload, secretOrPrivateKey, [options, callback])
*/
let token = jwt.sign({ user: 'do0ori', mail: 'do0ori@mail.com' }, process.env.PRIVATE_KEY); // token 발행 (default: HS256)
console.log(token); // noTimestamp라는 option이 지정되지 않은 경우 생성된 jwt는 iat (issued at) claim을 포함
/**
* jwt.verify(token, secretOrPublicKey, [options, callback])
*/
let decoded = jwt.verify(token, process.env.PRIVATE_KEY);
console.log(decoded); // { user: 'do0ori', mail: 'do0ori@mail.com', iat: 1703489109 }
jwt.verify(token, 'wrong-key', function(err, decoded) {
if (err) console.log(err); // JsonWebTokenError: invalid signature
else console.log(decoded);
});
npm install cookie-parser
폴더 구조
.
│ .env
│ app.js
│ mariadb.js
│ package-lock.json
│ package.json
│
├─node_modules
│
└─routes
channels.js
users.js
.env
PORT = "8888" # express port number
PRIVATE_KEY = "secret-key" # JWT secret key
mariadb.js (변경 없음)
// get the client
const mysql = require('mysql2');
// create the connection to database
const connection = mysql.createConnection({
// host: 'localhost',
// port: 3306,
user: 'root',
password: 'root',
// timezone: 'Asia/Seoul',
database: 'Youtube',
dateStrings: true
});
module.exports = connection;
app.js
const express = require('express'); // npm install express
const app = express();
const dotenv = require('dotenv');
dotenv.config();
let port = process.env.PORT || '3000';
app.set('port', port);
app.listen(port, () => console.log(`> Server is running on http://localhost:${port}/`));
const userRouter = require('./routes/users');
const channelRouter = require('./routes/channels');
app.use(express.json());
app.use('/', userRouter);
app.use('/channels', channelRouter);
routes/users.js
const express = require('express');
const router = express.Router();
const conn = require('../mariadb');
const { body, param, validationResult } = require('express-validator');
const channelNotFound = (res) => res.status(404).json({ message: "채널이 존재하지 않습니다." });
const validate = (req, res, next) => {
const err = validationResult(req)
if (err.isEmpty()) {
next();
} else {
return res.status(400).json(err.array());
}
};
router.route('/')
.get( // 채널 전체 조회
[
body('userId').notEmpty().isInt().withMessage('숫자를 입력해주세요.'),
validate
],
(req, res) => {
const { userId } = req.body;
let sql = 'SELECT * FROM `channels` WHERE user_id = ?';
conn.query(
sql, userId,
function (err, results) {
if (err) {
return res.status(400).json(err);
}
if (results.length) {
res.status(200).json(results);
} else {
channelNotFound(res);
}
}
);
})
.post( // 채널 개별 생성
[
body('userId').notEmpty().isInt().withMessage('숫자를 입력해주세요.'),
body('name').notEmpty().isString().withMessage('문자를 입력해주세요.'),
validate
],
(req, res) => {
const { name, userId } = req.body;
let sql = 'INSERT INTO `channels` (name, user_id) VALUES (?, ?)';
let values = [name, userId];
conn.query(
sql, values,
function (err, results) {
if (err) {
return res.status(400).json(err);
}
res.status(201).json({ message: `${name} 채널을 응원합니다.`, results });
}
);
});
router.route('/:id')
.get( // 채널 개별 조회
[
param('id').notEmpty().isInt().withMessage('숫자를 입력해주세요.'),
validate
],
(req, res) => {
let { id } = req.params;
id = parseInt(id);
let sql = 'SELECT * FROM `channels` WHERE id = ?';
conn.query(
sql, id,
function (err, results) {
if (err) {
return res.status(400).json(err);
}
if (results.length) {
res.status(200).json(results);
} else {
channelNotFound(res);
}
}
);
})
.put( // 채널 개별 수정
[
param('id').notEmpty().isInt().withMessage('숫자를 입력해주세요.'),
body('name').notEmpty().isString().withMessage('채널명 오류'),
validate
],
(req, res) => {
let { id } = req.params;
id = parseInt(id);
let { name } = req.body;
let sql = 'UPDATE `channels` SET name = ? WHERE id = ?';
let values = [name, id];
conn.query(
sql, values,
function (err, results) {
if (err) {
return res.status(400).json(err);
}
if (results.affectedRows != 0) {
res.status(200).json({ message: `채널명이 ${name}(으)로 성공적으로 수정되었습니다.`, results });
} else {
channelNotFound(res);
}
}
);
})
.delete( // 채널 개별 삭제
[
param('id').notEmpty().isInt().withMessage('숫자를 입력해주세요.'),
validate
],
(req, res) => {
let { id } = req.params;
id = parseInt(id);
let sql = 'DELETE FROM `channels` WHERE id = ?';
conn.query(
sql, id,
function (err, results) {
if (err) {
return res.status(400).json(err);
}
if (results.affectedRows != 0) {
res.status(200).json({ message: `채널이 삭제되었습니다.`, results });
} else {
channelNotFound(res);
}
}
);
});
module.exports = router;
routes/channels.js (변경 없음)
const express = require('express')
const router = express.Router()
const conn = require('../mariadb')
const { body, param, validationResult } = require('express-validator')
const channelNotFound = (res) => res.status(404).json({ message: "채널이 존재하지 않습니다." })
const validate = (req, res, next) => {
const err = validationResult(req)
if (err.isEmpty()) {
next()
} else {
return res.status(400).json(err.array())
}
}
router.route('/')
.get( // 채널 전체 조회
[
body('userId').notEmpty().isInt().withMessage('숫자를 입력해주세요.'),
validate
],
(req, res) => {
const { userId } = req.body
let sql = 'SELECT * FROM `channels` WHERE user_id = ?'
conn.query(
sql, userId,
function (err, results) {
if (err) {
return res.status(400).json(err);
}
if (results.length) {
res.status(200).json(results)
} else {
channelNotFound(res)
}
}
)
})
.post( // 채널 개별 생성
[
body('userId').notEmpty().isInt().withMessage('숫자를 입력해주세요.'),
body('name').notEmpty().isString().withMessage('문자를 입력해주세요.'),
validate
],
(req, res) => {
const { name, userId } = req.body
let sql = 'INSERT INTO `channels` (name, user_id) VALUES (?, ?)'
let values = [name, userId]
conn.query(
sql, values,
function (err, results) {
if (err) {
return res.status(400).json(err);
}
res.status(201).json({ message: `${name} 채널을 응원합니다.`, results })
}
)
})
router.route('/:id')
.get( // 채널 개별 조회
[
param('id').notEmpty().isInt().withMessage('숫자를 입력해주세요.'),
validate
],
(req, res) => {
let { id } = req.params
id = parseInt(id)
let sql = 'SELECT * FROM `channels` WHERE id = ?'
conn.query(
sql, id,
function (err, results) {
if (err) {
return res.status(400).json(err);
}
if (results.length) {
res.status(200).json(results)
} else {
channelNotFound(res)
}
}
)
})
.put( // 채널 개별 수정
[
param('id').notEmpty().isInt().withMessage('숫자를 입력해주세요.'),
body('name').notEmpty().isString().withMessage('채널명 오류'),
validate
],
(req, res) => {
let { id } = req.params
id = parseInt(id)
let { name } = req.body
let sql = 'UPDATE `channels` SET name = ? WHERE id = ?'
let values = [name, id]
conn.query(
sql, values,
function (err, results) {
if (err) {
return res.status(400).json(err);
}
if (results.affectedRows != 0) {
res.status(200).json({ message: `채널명이 ${name}(으)로 성공적으로 수정되었습니다.`, results })
} else {
channelNotFound(res)
}
}
)
})
.delete( // 채널 개별 삭제
[
param('id').notEmpty().isInt().withMessage('숫자를 입력해주세요.'),
validate
],
(req, res) => {
let { id } = req.params
id = parseInt(id)
let sql = 'DELETE FROM `channels` WHERE id = ?'
conn.query(
sql, id,
function (err, results) {
if (err) {
return res.status(400).json(err);
}
if (results.affectedRows != 0) {
res.status(200).json({ message: `채널이 삭제되었습니다.`, results })
} else {
channelNotFound(res)
}
}
)
})
module.exports = router
강의 마지막에 송아 강사님께서 언급하신 데이터베이스 페이징(Paging)에 대해 간단히 알아봤다. DB에서 데이터를 읽어와서 화면에 출력할 때 한꺼번에 모든 데이터를 가져오지 않고 출력될 페이지의 데이터만 나눠서 가져오는 것을 페이징이라고 한다고 한다. 일반적으로 쿼리문에서
LIMIT
(가져올 데이터 양),OFFSET
(시작 위치)을 이용해서 구현하는 것 같다.