1. 모듈화 하기
유효성 검사를 통과하지 못했을 때 에러를 처리하는 부분이 코드에서 반복되어 모듈화를 시키고 유효성 검사 부분과 함께 배열로 만들어 get의 첫번째 매개변수로 넣어주었다.
get 의 첫번째 매개변수는 콜백함수가 실행되기 전에 먼저 실행해야할 부분으로 유효성 검사 후
에러가 발생하면 validate 모듈이 실행되어 에러가 처리, 에러가 발생하지 않으면 모듈에서 빠져나와 콜백함수가 실행될 것이라고 생각했는데 그렇지 않았다.
에러가 발생한 경우에는 예상대로 처리되었지만, 에러가 발생하지 않은 경우에는 모듈 코드에서 빠져나오지 못해 request 가 제대로 처리되지 않고 무한 로딩에 빠지는 문제가 발생했다.
-> 에러가 없는 경우에도 validate 가 실행이 되는데, 에러가 없는 경우 return 되는 부분이 없어 다음으로 넘어가지 못하는 문제가 발생
next() 를 사용해서 문제를 해결해야 함!
// 모듈화
const validate = (req, res) => {
const err = validationResult(req)
if (!err.isEmpty()) {
return res.status(400).json(err.array())
}
}
router
.route('/')
.get(
[
body('user_id').notEmpty().isInt().withMessage('숫자 입력 필요'),
validate
]
,(req, res) => {
// ...
}
)
1.1. next()
router 의 매개변수에는 req, res 말고도 next 라는 매개 변수가 존재한다.
next() 는 다음 미들웨어로 넘기는 역할을 수행하는데, next() 를 호출하여 다음 미들웨어 함수로 요청 처리가 넘어가게 할 수 있다.
next 를 사용해 에러가 없는 경우 return next() 를 해 다음 할 일을 찾아가게만 하면 무한 로딩의 문제를 해결할 수 있다.
const validate = (req, res, next) => {
const err = validationResult(req)
if (!err.isEmpty()) {
return res.status(400).json(err.array())
} else {
return next(); // 다음 할 일(미들웨어, 함수) 찾아가라
}
}
※ 추가 - 클린 코드 관점에서 코드 살펴보기 (긍정 >>> 부정)
사람은 부정보다 긍정을 쉽게 인식하기 때문에 "에러가 비지 않았을 때" 보다 "에러가 비었을 때" 를 더 쉽게 인식한다고 한다.
그래서 !err.isEmpty() 일 때 에러를 처리해주기 보다는, err.isEmpty() 일 때 next() 처리를 해주는 것이 사람이 더 이해하기 쉬운 코드라고 한다.
const validate = (req, res, next) => {
const err = validationResult(req)
if (err.isEmpty()) {
return next()
} else {
return res.status(400).json(err.array()) // 다음 할 일(미들웨어, 함수) 찾아가라
}
}
2. 쿠키(Cookie) & 세션(Session)
2.1. 로그인 세션 - 인증 & 인가
인증 (Authentication)
- 로그인
인가 (Authorization)
- 같은 사이트 내에 접근할 수 있는 페이지가 다른 것
- ex) 쇼핑몰 관리자 / 고객은 접근할 수 있는 페이지가 다름
정리)
관리자든 고객이든 인증을 통해서 사이트에 가입된 사용자라는 것을 증명하는 것 -> 인증
인증 후에, 페이지에 접근 권한 여부에 따라 접근할 수 있고 없고 나뉘는 것 -> 인가
2.2. 쿠키 (Cookie)
- 웹 브라우저와 서버 간에 정보를 주고받기 위한 작은 데이터 파일
- 주로 사용자의 상태 정보를 유지하는 데 사용
- 웹 애플리케이션에서 로그인 상태 등을 기억하는 데 매우 유용
쿠키의 동작 과정
1) 로그인을 하면 서버가 쿠키를 구워줌
-> 사용자가 로그인하면, 서버는 사용자 정보(로그인 상태)를 담은 쿠키를 생성하여 브라우저에게 전송. 쿠키는 사용자 컴퓨터의 브라우저에 저장됨.
2) 사용자와 서버가 쿠키를 핑퐁 (사용자: 나 로그인 했었어 <-> 서버: 맞네)
-> 사용자가 다른 페이지로 이동하거나 같은 서버에 요청을 보낼 때, 브라우저는 자동으로 쿠키를 서버로 전송. 즉, 브라우저는 매번 "내가 전에 로그인했어, 이게 내 쿠키야" 라고 서버에 알려줌.
-> 서버는 쿠키에 담긴 정보를 확인하여 사용자가 누구인지 인식하고, 필요한 처리(로그인 상태 유지)를 함.
쿠키의 장점
- 서버에 상태를 저장하지 않음(사용자 브라우저에 저장) == 서버 공간을 아낄 수 있음
- Stateless == RESTful -> 서버에 상태를 저장하지 않고도 통신이 가능하다는 점에서 HTTP stateless 특징과 잘 맞음
쿠키의 단점
- 보안에 취약 -> 클라이언트(사용자 브라우저)측에 저장되기 때문에 도난당할 위험성 있음
2.3. 세션 (Session)
- 세션은 보안에 취약한 쿠키의 단점을 해결하기 위해 나옴 -> 사용자의 중요한 정보를 서버 측에 안전하게 저장하기 위한 방법
- 중요한 정보는 서버에서만 관리되고 클라이언트는 이를 식별할 수 있는 세션 ID 만을 사용
세션의 동작 과정
1) 로그인 & 세션 생성
-> 사용자가 로그인하면, 서버는 세션을 생성. 세션에는 로그인된 사용자의 중요한 정보(ID, 권한)가 저장되고 서버는 이 정보를 세션 저장소에 안전하게 보관함.
2) 사용자 <-> 서버가 번호만 가지고 대화
-> 서버는 생성된 세션과 연결된 세션 ID 를 클라이언트(사용자 브라우저)에 전달. 세션 ID 는 클라이언트 측의 쿠키에 저장되며, 이후 클라이언트가 서버에 요청을 보낼 때마다 세션 ID 를 함께 전송
-> 서버는 세션 ID 를 확인하여 해당 세션에 저장된 정보를 바탕으로 요청을 처리.
세션의 장점
- 보안이 비교적 좋음 -> 중요 정보가 서버에만 저장되기 때문에 클라이언트의 중요한 정보가 노출되지 않음
세션의 단점
- 서버의 저장 공간 사용 -> 서버 측에 정보가 저장되기 때문에, 사용자가 많아질수록 서버에 세션 데이터를 저장하는 공간과 자원 필요
- Stateless X -> 서버가 사용자의 상태를 기억해야 하기 때문에 서버의 부하가 증가
2.4. 쿠키 vs 세션
특성 | 쿠키 (Cookie) | 세션 (Session) |
저장 위치 | 클라이언트 (브라우저) | 서버 |
보안 | 비교적 취약 (데이터를 클라이언트에 저장) | 보안이 비교적 좋음 (서버에 저장) |
서버 부하 | 없음 | 있음 (세션 저장소 필요) |
Stateless | 서버가 상태를 저장하지 않음 (Stateless) | 서버가 상태를 저장 (Stateful) |
데이터 크기 제한 | 4KB | 서버에 저장하므로 제한 X |
3. JWT (JSON Web Token)
- JSON 형태의 데이터를 웹에서 안전하게 전송하기 위해 웹에서 사용하는 토큰 = 토큰을 가진 사용자가 "증명"을 하기 위한 수단
- 인증용(로그인)으로 사용하기도 하고, 인가용(권한)으로 사용하기도 함
장점
- 보안에 강함 == 암호화가 되어있음
- Stateless 함(HTTP 특징을 잘 따름) == 서버가 상태를 저장하지 않음
- 서버에 부담을 줄여줄 수 있음
- 참고로 토큰을 발행하는 서버를 따로 만들어줄 수도 있음
3.1. JWT 구조
오른쪽에 복호화된 내용을 살펴보자.
- HEADER: 토큰을 암호화하는 데 사용한 알고리즘과 토큰 형태(jwt)
- PAYLOAD: 사용자 정보 (이름, 주소, ... + 비밀번호를 담는 일은 거의 X)
- VERIFY SIGNATURE: 서명이 유효한지, 토큰이 변조되지 않았는지, 신회할 수 있는 발급자에 의해 생성됐는지 확인
PAYLOAD 에 담긴 값이 바뀌면 VERIFY SIGNATURE 값이 통채로 바뀐다. 때문에 PAYLOAD 에 위험한 데이터가 들어오면 VERIFY SIGNATURE 값을 보고 걸러낼 수 있다.
3.2. JWT 로 인증/인가 하는 절차
3.3. 토큰 발행 & 검증하기
jsonwebtoken 설치
npm install jsonwebtoken
let jwt = require('jsonwebtoken'); // jwt 모듈 소환
let dotenv = require('dotenv'); // 환경변수파일(.env)을 로드하는 라이브러리
// 환경변수 로딩 -> .env파일에 있는 환경변수들을 process.env에 로딩
dotenv.config();
// 서명 = 토큰 발행
// 첫번째 인자: 사용자정보(페이로드), 두번째 인자: 비밀키
let token = jwt.sign({ foo: 'bar' }, process.env.PRIVATE_KEY);
console.log(token);
// JWT 검증 -> 검증에 성공하면, 페이로드 값을 확인할 수 있음!
let decoded = jwt.verify(token, process.env.PRIVATE_KEY);
console.log(decoded);
콘솔창에 출력한 토큰을 JWT 홈페이지에 가서 붙여넣으면 이런 페이로드 안에 iat 라는 필드가 존재하는 것을 확인할 수 있다. iat 는 토큰 발행된 시간을 나타내며, 이 값은 토큰이 생성된 시점에 따라 달라진다. 토큰의 전체 값이 달라지는 이유는, 토큰을 서명할 때 페이로드(iat 등)를 포함하여 서명하므로, 발행시간이 다르면 서명 값(VERIFY SIGNATURE) 역시 변경된다.
3.4. .env 파일 (environment: 환경 변수 설정 파일)
- 파일 확장자가: .env
- 외부에 유출되면 안 되는(깃허브에 올리면 안되는) 중요한 환경 변수들을 따로 관리하기 위한 파일 (포트넘버, 데이터베이스 계정, 암호키 등)
- .gitignore 파일에 .env 파일을 추가하여 버전 관리에서 제외해야 함
- .env 파일은 프로젝트 최상위 디렉토리에 위치해야함
3.5. JWT 적용해보기 - 쿠키에 토큰 담아 보내기
- cookie-parser 설치
npm install cookie-parser
- 로그인 api 토큰 발급 코드
로그인에 실패하면 403 상태코드를 반환 -> 403 클라이언트가 요청한 리소스에 액세스할 권한이 없음
// jwt 모듈
const jwt = require('jsonwebtoken');
// dotenv 모듈
const dotenv = require('dotenv');
dotenv.config();
// ...
if (loginUser && loginUser.password === password) {
// token 발급
const token = jwt.sign({
email: loginUser.email,
name: loginUser.name
}, process.env.PRIVATE_KEY);
res.cookie("token", token, {
httpOnly: true
}); // 쿠키에 토큰 담음
res.status(200).json({
message: `${loginUser.name}님 로그인이 성공하였습니다.`
})
} else {
res.status(403).json({
message: '이메일 또는 비밀번호가 틀렸습니다.'
})
}
로그인 성공 시 포스트맨 쿠키 탭에서 잘 담겨있는 토큰을 확인할 수 있다. 이때 Secure 과 httpOnly 필드는 보안을 강화하기 위해 사용하는 옵션이다.
Secure (HTTP / HTTPS)
-> Secure 필드가 true 이면 쿠키가 오직 HTTPS 연결을 통해서만 전송되도록 설정한다. 즉, 쿠키가 암호화된 연결에서만 서버로 전송되어 네트워크 상에서 중간에 탈취되는 것을 방지한다.
HttpOnly
-> HttpOnly 필드가 true 이면 자바스크립트로 쿠키에 접근할 수 없게 막아준다. 즉, 쿠키에 접근할 수 있는 방법은 HTTP 요청뿐이며, 브라우저에서 쿠키로 접근하거나 조작하는 것을 차단한다.
(XSS: 크로스 사이트 스크립팅 공격을 방지하기 위해 사용됨 -> 자바스크립트에서 쿠키를 훔치지 못하게 하여 쿠키가 악성 스크립트에 의해 노출되지 않도록 보호)
- 토큰에 유효기간 설정하기 (timeout)
토큰에 expiresIn 과 issuer 를 추가해 유효기간은 5분, 토큰 발행자는 test 로 설정을 해주었다.
발급받은 토큰을 확인해보니, 사용자 정보 + 유효기간 + 토큰 발행자정보까지 확인할 수 있었다.
// token 발급
const token = jwt.sign({
email: loginUser.email,
name: loginUser.name
}, process.env.PRIVATE_KEY, {
expiresIn: '5m',
issuer: 'test'
});
'TIL with Programmers' 카테고리의 다른 글
[TIL] 9/27 API 설계 명세서 작성하기 (0) | 2024.09.27 |
---|---|
[TIL] 9/26 와이어프레임 보고 API 설계해보기 (0) | 2024.09.26 |
[TIL] 9/24 express-validator, 유효성 검사, sql 에러, API 우선순위 (1) | 2024.09.24 |
[TIL] 9/23 node.js, db 연결, db 모듈화, SQL 쿼리문, affectedRows, 단축평가(short-circuit evaluation) (0) | 2024.09.23 |
[회고록] 풀사이클 개발 데브코스 5주차 회고 (1) | 2024.09.14 |