지난번 작업 이후 진행된 작업
1. register시 중복확인 기능
2. DB와 연결
3. MySQL2 라이브러리 사용
4. DB의 쿼리문을 Promise 방식으로 값 반환
5. env 환경변수 간단하게 적용
6. JWT 토큰 발급, 인증, 재발급 기능 구현
7. 로그아웃 기능 추가
위와 같은 작업을 했다.
와성된 코드를 보며 작업 당시 경험했던 어려움을 적어보겠다.
코드
modules/user.js
import db from './db.js';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
// 비밀번호 생성
export const setPassword = async (password) => {
const hash = await bcrypt.hash(password, 10);
// .then(hash => console.log(hash));
// console.log(hash);
return hash;
};
export const checkPassword = async (username, password) => {
try {
const saved_hashed_pw = await db.query(`SELECT password FROM account_info WHERE name="${username}";`);
// console.log(typeof saved_hashed_pw[0][0].password, typeof password)
const result = await bcrypt.compare(password, saved_hashed_pw[0][0].password);
return result;
}
catch (e) {
throw e;
}
}
export const checkExistName = async (username) => {
const isExist_username = await db.query(`SELECT name FROM account_info WHERE name="${username}";`);
if (isExist_username[0].length === 0) {
return false;
}
else {
return isExist_username[0][0].name;
}
}
export const serialize = async (username) => {
const hspw = await db.query(`SELECT password FROM account_info WHERE name="${username}";`);
const data = JSON.stringify(
{
_id: hspw[0][0].password,
username: username
}
);
console.log(data, "user");
return data;
}
export const generateToken = (username, password) => {
const token = jwt.sign(
// 첫번째 파라미터는 토큰 안에 넣고싶은 데이터를 넣는다.
{
username: username,
password: password
},
process.env.JWT_SECRET, // 두 번째 파라미터에는 JWT 암호를 넣는다.
{
expiresIn: '7d' // 유효기간을 정함 (7일)
},
);
return token;
}
이 코드에서는 꽤 어려움이 많았다.
우선 query문을 이용하는 것에서 콜백에서 나오는 결과는 상위 스코프로 내보내기 힘들기에
mysql2를 이용해 Promise 방식으로 값을 가져올 수 있었다.
쿼리문으로 가져온 값은 여러 값을 갖은 객체로 전달되기에 그중 원하는 데이터를 찾아 사용해야한다.
(예시 : hspw[0][0].password)
auth/auth.ctrl.js
import Joi from 'joi';
import db from '../models/db.js';
import { checkExistName, checkPassword, setPassword, generateToken, serialize } from '../models/user.js';
// 여기서 각 동작을 위한 api 작성
// 회원가입(완)
export const register = async (req, res) => {
const { username, password } = req.body;
const schema = Joi.object().keys({
// 객체가 다음 필드를 갖음을 검증
username: Joi.string().alphanum().min(3).max(20).required(),
// 문자열 타입, 알파벳과 0~9의 범위, 최소3 최대 20의 길이, 필수적인
password: Joi.string().required()
});
// validate() 현제 스키마와 옵션을 사용하여 값을 검증한다.
const result = schema.validate(req.body);
if (result.error) {
res.status(400);
res.body = result.error;
return;
}
try {
const exist_username = await checkExistName(username);
// username이 이미 있는지 확인
// db에 같은 이름을 갖은 요청이 들어오면 INSERT하지 않게 끔 함
if (exist_username) {
// exist_username의 0번 인덱스에는 RowDataPacket이 존재하고
// 이 안에 반환값이 있다면 length는 0 이상인 것을 확인
res.status(409);
res.send("already exist username")
return;
}
else {
const hspw = await setPassword(password);
await db.query(`INSERT INTO account_info (name,password) VALUES ("${username}","${hspw}");`);
// 토큰 발급 및 검증에서 username과 _id(hashedpw)를 JSON형식의 객체로 보내주기
const token = generateToken(username, password);
const user_data = await serialize(username);
res.cookie('access_token', token, {
maxAge: 1000 * 60 * 60 * 24 * 7, // 7일
httpOnly: true,
});
res.send(user_data)
}
} catch (e) {
throw e // 에러
}
}
// 로그인
export const login = async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
res.status(401);
res.send("id 혹은 pw 없음"); // Unauthorized
return;
}
try {
const exist_username = await checkExistName(username);
if (!exist_username) {
res.send("db에 유저 없음");
return
}
const valid = await checkPassword(username, password);
if (!valid) {
res.status(401);
res.send("잘못된 비밀번호입니다.");
return
}
else if (valid) {
// 토큰 발급 및 검증에서 username과 _id(hashedpw)를 JSON형식의 객체로 보내주기
const token = generateToken(username, password);
res.cookie('access_token', token, {
maxAge: 1000 * 60 * 60 * 24 * 7, // 7일
httpOnly: true,
});
res.send(`${serialize(username)}`);
}
}
catch (e) {
throw e;
}
}
// 로그인 상태 확인
export const check = async (req, res) => {
const user = res.locals.user;
if (!user) {
res.status(401);
return;
}
else {
res.send(user);
}
}
// 로그아웃
export const logout = (req, res) => {
// 쿠키삭제
res.clearCookie('access_token');
console.log("쿠키 삭제됨")
res.send("쿠키 삭제?")
}
이 코드에서는 user.js에서 만들어낸 메서드들을 사용하고 실질적인 api동작을 구성한다.
우선 JOI를 사용해 스키마의 일치 여부를 확인하는 것을 처음 접했기에 구글링해서 사용했다.
모든 메서드에서 db에서 데이터를 가져오는 동작은 await로 비동기 형식으로 동작하도록 했다.
어짜피 데이터가 없으면 원활하게 동작할 수 없기때문에 비동기 형식으로 사용했다.
express에서 cookie를 설정하는 법도 구글링해서 적용시켰고 이를 전달하고자 하는
데이터와 유효기간등 옵션을 설정할 수 있었다.
lib/jwtMiddleware.js
import jwt from 'jsonwebtoken';
import { checkExistName, generateToken, serialize } from '../models/user.js';
// 이 미들웨어를 사용하는 곳은 무조건 get,post,use 하는 곳에 추가해줘야 한다.
const jwtMiddlware = async (req, res, next) => {
const { username, password } = req.body;
const token = await req.headers.cookie.replace('access_token=', '');
// console.log(token, "token 확인")
if (!token) {
console.log("토큰 없음")
return next(); // 토큰 없음
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// console.log(decoded, "middle decoded");
const data = await serialize(decoded.username);
// console.log(decoded, data, "decoded, data");
// // 토큰의 유효기간이 3.5일 미만이면 재발급하는 코드
const now = Math.floor(Date.now() / 1000);
if (decoded.exp - now < 1000 * 60 * 60 * 24 * 3.5) {
const token = generateToken(decoded.username, password);
res.cookie('access_token', token, {
maxAge: 1000 * 60 * 60 * 24 * 7, // 7일
httpOnly: true,
});
}
res.locals.user = data;
next();
}
catch (e) {
// 토큰 검증 실패
return next();
}
}
export default jwtMiddlware;
이 코드는 토큰의 유효성과 재발급을 도와주는 코드이며
jsonwebtoken을 이용해 토큰과 비밀키를 비교대조해서 유효성을 검사하고 이후 유효하다면
데이터를 전달해주고,유효기간이 3일 이하로 남게되면 재발급 해주는 코드이다.이때, 데이터 전달은 res.locals.user로 전달했으며 다음 미들웨어 함수에 값을 전달하며,값을 불러올 때는 똑같이 res.locals.user로 불러와야한다.
후기
중요한 코드 작업을 위와 같으며,api 작업을 했으니 이제는 컴포넌트들을 만들고 연결까지 해보는 작업을 진행할 예정이다.
처음 로그인기능을 만드려 했을 때는 앞이 막막했다...로그인 이라는 기능이 어떤 방식으로 동작하고, 중요한 트리거가 되는 기능들을 몰랐지만지금은 데이터 입력 > 데이터 검토 > 유효성 검사(토큰확인) > 결과 와 같이 간단하게 나마이해를 했다고 느꼈다.앞으로 가야할 길이 멀지만 끝까지 열심히 계속 해보자 화이팅