2024. 12. 3. 01:21ㆍ프로젝트
우선 로그인 기능을 이용해서 접근 제어를 할 수있는 방법은 세션 기능과 JWT토큰을 이용하는 방법들이 있습니다.
1. 세션 기반 인증이란?
작동 방식
사용자가 로그인하면 서버는 세션을 생성하고, 세션 ID를 클라이언트의 쿠키에 저장합니다.
이후 요청마다 클라이언트는 이 세션 ID를 서버에 보냅니다.
서버는 세션 ID를 확인해 사용자를 인증합니다.
흐름
- 로그인 요청
사용자가 ID와 비밀번호를 서버에 보냅니다. - 서버에서 세션 생성
서버는 사용자가 맞는지 확인한 후, 고유한 세션 ID를 생성하고 이를 저장합니다.
(예: Session ID: abc123) - 세션 ID를 클라이언트에 전달
클라이언트는 서버로부터 받은 세션 ID를 쿠키에 저장합니다. - 인증된 요청
클라이언트는 요청을 보낼 때마다 쿠키에 저장된 세션 ID를 서버에 보냅니다. - 서버에서 세션 확인
서버는 세션 ID를 확인하고, 저장된 세션 정보(사용자 ID 등)로 인증을 처리합니다. - 로그아웃
서버에서 해당 세션을 삭제하면 사용자는 로그아웃됩니다.
2. JWT 기반 인증이란?
작동 방식
사용자가 로그인하면 서버는 JWT를 생성해 클라이언트에 전달합니다.
클라이언트는 JWT를 쿠키나 로컬스토리지에 저장합니다.
이후 요청마다 클라이언트는 이 JWT를 서버로 전송합니다.
서버는 JWT의 서명을 확인해 사용자를 인증합니다.
흐름
- 로그인 요청
사용자가 ID와 비밀번호를 서버에 보냅니다. - 서버에서 JWT 생성
서버는 사용자가 맞는지 확인한 후, 사용자 정보를 포함한 JWT 토큰을 생성합니다. - JWT를 클라이언트에 전달
클라이언트는 서버로부터 받은 JWT를 로컬스토리지나 쿠키에 저장합니다. - 인증된 요청
클라이언트는 요청을 보낼 때마다 JWT를 HTTP 헤더에 포함시켜 서버에 보냅니다. - 서버에서 JWT 확인
서버는 JWT의 서명을 확인하고, 유효하면 사용자 정보를 인증합니다. - 로그아웃
클라이언트에서 JWT를 삭제하면 로그아웃됩니다.
(서버는 JWT 자체를 저장하지 않기 때문에 강제 만료가 어려움.)
우선 회원가입부터 살펴보겠습니다.
SOLAPI - 알림톡과 문자메시지 발송, CRM 자동화 솔루션
CRM을 통해 고객에게 더 나은 서비스를 제공하고 비즈니스의 성장을 이룰 수 있는 방법을 소개합니다. 대량문자, 카카오 알림톡 친구톡, 문자 API연동, 앱발송, 웹발송, CRM자동화, 보이스톡
solapi.com
우리는 회원가입에서 전화번호 인증을 위해서 솔라피api를 이용하여 전화번호인증을하면 랜덤숫자를 통해 인증을 해주는걸로 하였습니다.
npm install solapi
를 설치를 해줍니다.
solapi를 통한 인증번호
import pkg from "solapi"; // solapi 모듈 전체를 임포트
import dotenv from "dotenv";
dotenv.config();
const { SolapiMessageService } = pkg; // 필요한 서비스만 가져옴
// SOLAPI 초기화
const messageService = new SolapiMessageService(
process.env.SOLAPI_API_KEY, // API Key
process.env.SOLAPI_API_SECRET // Secret Key
);
// 전화번호 인증코드 발송
const sendVerificationCode = async (req, res) => {
const { phone_number } = req.body;
// 전화번호 확인
if (!phone_number) {
return res.status(400).json({
resultCode: "F-1",
msg: "전화번호가 누락되었습니다.",
});
}
// 6자리 인증코드 생성
const verificationCode = Math.floor(
100000 + Math.random() * 900000
).toString();
try {
const response = await messageService.send({
to: phone_number,
from: "010-2848-5397",
text: `인증코드: ${verificationCode}`,
});
console.log("CoolSMS API Response:", response); // CoolSMS 응답 출력
if (response.error_list && response.error_list.length > 0) {
console.error("CoolSMS API Error:", response.error_list); // 에러 리스트 출력
return res.status(500).json({
resultCode: "F-2",
msg: "SMS 전송 중 에러가 발생했습니다.",
error: response.error_list,
});
}
return res.status(200).json({
resultCode: "S-1",
msg: "인증코드가 성공적으로 전송되었습니다.",
verificationCode,
});
} catch (error) {
console.error("Unexpected Error:", error); // 예외 처리
return res.status(500).json({
resultCode: "F-1",
msg: "서버 에러 발생",
error: error.message,
});
}
};
클라이언트에서 전화번호를 입력하면
const { phone_number } = req.body;
통해서 전화번호가 받아와지고
const messageService = new SolapiMessageService(
process.env.SOLAPI_API_KEY, // API Key
process.env.SOLAPI_API_SECRET // Secret Key
);
를 통해서 api 를 통해서
try {
const response = await messageService.send({
to: phone_number,
from: "발송인",
text: `인증코드: ${verificationCode}`,
});
// 6자리 인증코드 생성
const verificationCode = Math.floor(
100000 + Math.random() * 900000
).toString();
인증 코드가 랜덤으로 전송되고
// 인증코드 검증
const verifyCode = (req, res) => {
const { verificationCode, inputCode } = req.body;
console.log("Stored code:", verificationCode); // 서버에서 저장된 인증코드
console.log("User input code:", inputCode); // 사용자가 입력한 인증코드
if (verificationCode !== inputCode) {
return res.status(400).json({
resultCode: "F-1",
msg: "인증코드가 일치하지 않습니다.",
});
}
return res.status(200).json({
resultCode: "S-1",
msg: "인증이 완료되었습니다.",
});
};
위에 코드를 통해서
인증코드 검증을 해줄수 있습니다.
회원가입 기능
/* 사용자 회원가입 */
const register = async (req, res) => {
try {
const { username, password, email, full_name, phone_number } = req.body;
// 입력 값 검증
if (!username || !password || !email) {
return res.status(400).json({
resultCode: "F-1",
msg: "필수 입력 값이 누락되었습니다.",
});
}
// 사용자 이름 중복 체크
const { rows: existingUsers } = await pool.query(
`SELECT id FROM users WHERE username = $1 OR email = $2`,
[username, email]
);
if (existingUsers.length > 0) {
return res.status(400).json({
resultCode: "F-2",
msg: "이미 사용 중인 사용자 이름 또는 이메일입니다.",
});
}
console.log("Received Data:", req.body); // 입력 값 확인을 위한 로그
// 비밀번호 해싱
const hashedPassword = await bcrypt.hash(password, 10);
// 사용자 생성
const { rows } = await pool.query(
`
INSERT INTO users (username, password, email, full_name, phone_number)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, username, email, full_name, phone_number, created_at
`,
[username, hashedPassword, email, full_name, phone_number]
);
const newUser = rows[0];
res.status(201).json({
resultCode: "S-1",
msg: "사용자 등록 성공",
data: newUser,
});
} catch (error) {
console.error("Error registering user:", error);
res.status(500).json({
resultCode: "F-1",
msg: "서버 에러 발생",
error: error.message,
});
}
};
위에 코드는 클라이언트쪽에서
const { username, password, email, full_name, phone_number } = req.body;
를 받아오고
// 입력 값 검증
if (!username || !password || !email) {
return res.status(400).json({
resultCode: "F-1",
msg: "필수 입력 값이 누락되었습니다.",
});
}
// 사용자 이름 중복 체크
const { rows: existingUsers } = await pool.query(
`SELECT id FROM users WHERE username = $1 OR email = $2`,
[username, email]
);
필수로 입력되어야 하는 값들과 이름과 이메일을 중복체크를 해줍니다.
// 비밀번호 해싱
const hashedPassword = await bcrypt.hash(password, 10);
비밀번호는 안정적이게 bcrypt를 통해 해싱암호화를 해줍니다.
const { rows } = await pool.query(
`
INSERT INTO users (username, password, email, full_name, phone_number)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, username, email, full_name, phone_number, created_at
`,
[username, hashedPassword, email, full_name, phone_number]
);
쿼리문을 통해 테이블에 넣어주도록 합니다.
로그인 기능
const login = async (req, res) => {
try {
const { username, password } = req.body;
const { rows } = await pool.query(
`
SELECT id, username, password, email, full_name, phone_number, created_at
FROM users
WHERE username = $1
`,
[username]
);
if (rows.length === 0) {
return res.status(401).json({
resultCode: "F-2",
msg: "존재하지 않는 아이디입니다.",
});
}
const user = rows[0];
const match = await bcrypt.compare(password, user.password);
if (!match) {
return res.status(401).json({
resultCode: "F-2",
msg: "아이디 또는 비밀번호가 일치하지 않습니다.",
});
}
req.session.userId = user.id;
req.session.username = user.username;
req.session.save((err) => {
if (err) {
console.error("세션 저장 중 에러 발생:", err);
return res.status(500).json({
resultCode: "F-1",
msg: "세션 저장 중 에러 발생",
});
}
// 세션 저장 성공 후 응답
res.json({
resultCode: "S-1",
msg: "로그인 성공",
data: {
id: user.id,
username: user.username,
email: user.email,
fullName: user.full_name,
phone_number: user.phone_number,
created_at: user.created_at,
},
sessionId: req.sessionID,
});
});
} catch (error) {
res.status(500).json({
resultCode: "F-1",
msg: "서버 에러 발생",
error: error.message,
});
}
};
로그인 기능은
const { username, password } = req.body;
을 통해 사용자가 입력한 유저 아이디와 비밀번호를 받아와서
const { rows } = await pool.query(
`
SELECT id, username, password, email, full_name, phone_number, created_at
FROM users
WHERE username = $1
`,
[username]
);
if (rows.length === 0) {
return res.status(401).json({
resultCode: "F-2",
msg: "존재하지 않는 아이디입니다.",
});
}
쿼리를 통해서 유저아이디를 찾도록 탐색합니다.
const user = rows[0];
const match = await bcrypt.compare(password, user.password);
if (!match) {
return res.status(401).json({
resultCode: "F-2",
msg: "아이디 또는 비밀번호가 일치하지 않습니다.",
});
}
데이터베이스에서 가져온 사용자 정보(user)에서 password 필드를 확인하고
bcrypt.compare: 사용자가 입력한 비밀번호와 데이터베이스에 저장된 해싱된 비밀번호를 비교.
결과: 비밀번호가 일치하지 않을 경우, HTTP 상태 코드 401과 에러 메시지 반환.
req.session.userId = user.id;
req.session.username = user.username;
req.session.save((err) => {
if (err) {
console.error("세션 저장 중 에러 발생:", err);
return res.status(500).json({
resultCode: "F-1",
msg: "세션 저장 중 에러 발생",
});
}
// 세션 저장 성공 후 응답
res.json({
resultCode: "S-1",
msg: "로그인 성공",
data: {
id: user.id,
username: user.username,
email: user.email,
fullName: user.full_name,
phone_number: user.phone_number,
created_at: user.created_at,
},
sessionId: req.sessionID,
});
});
} catch (error) {
res.status(500).json({
resultCode: "F-1",
msg: "서버 에러 발생",
error: error.message,
});
}
};
req.session:Express.js에서 제공하는 세션 객체로, 현재 사용자에 대한 정보를 저장
저장 내용:userId: 데이터베이스에서 가져온 사용자 고유
req.session.save():세션 정보를 저장하고, 저장이 성공했는지 확인합니다.세션 저장은 비동기로 이루어지며,
콜백 함수로 성공/실패를 처리.
오류 처리:저장 중 에러가 발생하면 서버는 HTTP 상태 코드 500을 반환.
오류 메시지는 resultCode와 함께 클라이언트로 전송.
로그아웃 기능
const logout = (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({
resultCode: "F-1",
msg: "로그아웃 중 에러 발생",
});
}
res.clearCookie("connect.sid", {
path: "/",
secure: process.env.NODE_ENV === "production", // HTTPS 환경에서만 secure 쿠키 허용
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", // 개발 환경에서는 lax, 배포 환경에서는 none
maxAge: 0, // 쿠키 즉시 만료
});
console.log("세션이 성공적으로 파괴되었습니다.");
res.json({
resultCode: "S-1",
msg: "로그아웃 성공",
});
});
};
req.session.destroy((err) => {
if (err) {
return res.status(500).json({
resultCode: "F-1",
msg: "로그아웃 중 에러 발생",
});
}
req.session.destroy():
- 현재 사용자의 세션 데이터를 삭제.
- 세션 스토리지에 저장된 해당 세션 데이터를 제거.
res.clearCookie("connect.sid", {
path: "/",
secure: process.env.NODE_ENV === "production", // HTTPS 환경에서만 secure 쿠키 허용
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", // 개발 환경에서는 none, 개발 중에는 lax
maxAge: 0, // 즉시 만료
});
res.clearCookie():
- 클라이언트에 저장된 특정 쿠키를 제거.
- 여기서는 connect.sid라는 이름의 쿠키를 삭제.
옵션 설명:
- path:
- 쿠키를 삭제할 경로. 기본적으로 세션 쿠키가 설정된 경로(/)를 지정.
- secure:
- HTTPS 환경에서만 쿠키를 전송하도록 설정.
- NODE_ENV 환경 변수를 이용해 배포 환경과 개발 환경을 구분.
- sameSite:
- 쿠키가 동일한 사이트 내 요청에서만 전송되도록 제어.
- none은 배포 환경, lax는 개발 환경에서 설정.
- maxAge:
- 쿠키의 유효 시간을 0으로 설정하여 즉시 만료.
세션 상태 확인
const checkSession = async (req, res) => {
console.log("세션 데이터 확인:", req.session); // 세션 데이터 확인
if (req.session && req.session.userId) {
try {
// 데이터베이스에서 사용자 정보 조회
const { rows } = await pool.query(
`
SELECT id, username, email, full_name, phone_number, created_at
FROM users
WHERE id = $1
`,
[req.session.userId]
);
if (rows.length === 0) {
return res.status(401).json({
resultCode: "F-2",
msg: "세션이 만료되었거나 유효하지 않습니다.",
isAuthenticated: false,
});
}
const user = rows[0];
console.log("세션 유효함. 사용자 ID:", req.session.userId);
return res.status(200).json({
resultCode: "S-1",
msg: "세션이 유효합니다.",
isAuthenticated: true,
user: {
id: user.id,
username: user.username,
email: user.email,
full_name: user.full_name,
phone_number: user.phone_number,
created_at: user.created_at,
},
});
} catch (error) {
console.error("세션 확인 중 에러 발생:", error);
return res.status(500).json({
resultCode: "F-1",
msg: "서버 에러 발생",
isAuthenticated: false,
});
}
} else {
return res.status(401).json({
resultCode: "F-2",
msg: "세션이 만료되었거나 유효하지 않습니다.",
isAuthenticated: false,
});
}
};
console.log("세션 데이터 확인:", req.session); // 세션 데이터 확인
if (req.session && req.session.userId) {
req.session:
- Express.js 세션 객체로, 현재 사용자와 관련된 세션 데이터를 포함.
eq.session.userId:
- 로그인 시 저장된 사용자 ID. 이 값이 존재하면 세션이 유효한 것으로 간주.
처리:
- req.session 또는 req.session.userId가 없으면 클라이언트에 401 상태 코드와 오류 메시지를 반환
const { rows } = await pool.query(
`
SELECT id, username, email, full_name, phone_number, created_at
FROM users
WHERE id = $1
`,
[req.session.userId]
);
목적:
- 세션에 저장된 userId를 이용해 데이터베이스에서 사용자의 상세 정보를 조회.
- 쿼리 결과:
- rows 배열에 조회된 데이터가 저장되며, rows[0]은 사용자 객체를 나타냄.
- 조건:
- rows.length === 0일 경우: 세션의 userId가 더 이상 유효하지 않은 경우로 간주하고, 401 응답 반환.
'프로젝트' 카테고리의 다른 글
캡스톤 디자인 프로젝트 -맛집 정보 사이트 만들기- (5) 프론트 회원 기능 (4) | 2024.12.09 |
---|---|
캡스톤 디자인 프로젝트 -맛집 정보 사이트 만들기- (5) 프론트 회원 기능 (1) | 2024.12.03 |
캡스톤 디자인 프로젝트 -맛집 정보 사이트 만들기- (3) 데이터 베이스 및 Postgresql (2) | 2024.12.02 |
캡스톤 디자인 프로젝트 -맛집 정보 사이트 만들기- (2) (2) | 2024.12.02 |
캡스톤 디자인 프로젝트 -맛집 정보 사이트 만들기- (1) (2) | 2024.12.02 |