DB에 있는 캐릭터 정보를 가져와야 하는 API를 작성해야 했다.
요구조건은 다음과 같다.
- 조회 시 다음의 정보를 가져와야 한다.
- 이름, 체력, 공격력 - 조회하는 캐릭터가 자신의 캐릭터일 경우 돈도 포함해서 조회
- 자신의 캐릭터가 아니거나 비로그인일 경우 돈은 조회하지 않음
인증되지 않은 사용자도 API를 사용할 수 있어야 하기 때문에 고민이 생겼다.
기존에 토큰 인증을 위해 사용하던 ``authenticateToken`` 미들웨어를 사용할 수가 없는 상태였다.
export const authenticateToken = (req, res, next) => {
const authHeader = req.headers["authorization"];
if (!authHeader) return res.status(401).json({ error: "Authorization header missing" });
const token = authHeader.split(" ");
if (token[0] !== "Bearer") return res.status(403).json({ error: "Invalid token structure" });
jwt.verify(token[1], process.env.JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: "Invalid token" });
req.user = user;
next();
});
};
인증 실패 시 바로 응답을 보내버리기에 비로그인은 넘어갈 수 조차 없다.
그럼 API 처리 함수 내에서 이를 처리해야 하는데...
이게 과연 보안의 관점에서 옳은 일인지는 모르겠으나 일단 다음과 같이 작성해 봤다.
// 미인증 조회는 ID를 -1로 설정
const authHeader = req.headers["authorization"];
let currentUserId = -1;
// 헤더가 존재 하는지
// 토큰의 형식은 갖춰져 있는지 확인
if (!authHeader || authHeader.slice(0, 7) !== "Bearer ") currentUserId = -1;
else {
// 인증 절차 진행
// currentUserId에 인증된 Id 저장
const token = authHeader.slice(7);
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) currentUserId = -1;
else currentUserId = user.userId;
});
}
먼저 인증된 사용자와 미인증 된 사용자를 구분해야 한다.
인증된 사용자는 모두 ``1 이상의 자연수``를 그 ID로 갖고 있기 때문에 미인증자는 ``-1``로 설정했다.
그리고 TCP 통신에서 해당 패킷이 최소한 헤더는 갖고 있는지 판단하듯이,
``authorization`` 헤더가 존재하는지, 그 데이터가 형식을 갖추고는 있는지 판단한다.
여기서 문제가 있다면 미인증으로 판단하고 ``currentUserId``를 -1로 설정한다.
마지막으로 토큰을 검증한다.
문제가 없다면 ``userId``를 ``currentUserId``에 넘겨줘 인증을 마친다.
역시 여기서 문제가 있다면 -1로 설정한다.
일단 인증은 이렇게 처리했다.
다음엔 필요한 데이터를 가져와야 한다.
``money``는 인증된 사용자가 자기 캐릭터의 정보를 조회할 때만 가져와야 한다.
그럼 여기서 ``currentUserId``를 통해 추가 검증을 거쳐야 한다.
그렇다면 일단 캐릭터 정보는 가져와야 한다는 것이다.
해당 캐릭터 ID에 맞는 정보를 일단 가져온다.
const character = await prisma.character.findUnique({
where: { id: parseInt(characterId) },
select: {
userId: true,
name: true,
health: true,
power: true,
money: true,
},
});
if (!character) {
return res.status(404).json({ error: "Character not found" });
}
``userId``는 검증을 위해, ``money``도 2번 쿼리 하는 일을 막기 위해 가져오기로 했다.
검증과 ``money``에 대한 판단은 다음과 같이 이루어진다.
// 현재 로그인한 유저와 캐릭터 소유자의 ID가 다른 경우 money 필드 제거
if (character.userId !== currentUserId) {
delete character.money;
}
// userId는 필요가 없으므로 어찌됐든 지운다.
delete character.userId;
// 정상적으로 데이터를 가져왔으므로 200 코드와 함께 결과 반환
res.status(200).json(character);
쿼리 한 ``character``엔 ``userId``도 포함돼 있었다.
이를 ``currentUserId``와 비교하여 사용자의 캐릭터가 맞는지 검증한다.
만약 틀리다면 ``money`` 필드를 제거한다.
그리고 ``userId``는 원래 반환 결과에 포함되지 않으므로 제거한다.
이러면 정상적으로 요청이 이루어졌기 때문에 코드 ``200 OK``와 함께 결과를 전송한다.
실행 결과를 확인해 보자.
현재 로그인 한 계정은 ``user2``이며, 이 계정이 소유한 캐릭터의 ID는 6이다.
조회할 타 계정의 캐릭터 ID는 1이다.
``money``가 포함되지 않은 것을 확인할 수 있다.
6번 캐릭터를 확인해 보자.
정상적으로 ``money``도 결과에 포함되는 것을 확인할 수 있다.
토큰이 없는 요청에 대해서 ``money``를 제외한 결과를 주는 것도 확인할 수 있다.
존재하지 않는 캐릭터에 대한 조회 요청도 404를 보내며 제대로 처리한다.
일단 의도한 대로 기능은 한다.
하지만 코드가 정말 효율적인지,
이 안에서 토큰 인증이 진행돼도 되는 건지에 대해선
좀 더 고민이 필요할 것 같다.
'Camp > T.I.L.' 카테고리의 다른 글
[TIL #11] 행렬의 곱셈 (0) | 2024.09.12 |
---|---|
[TIL #10] n^2 배열 자르기 (0) | 2024.09.11 |
[TIL #8] H-Index 구하기 (0) | 2024.09.09 |
[TIL #7] 괄호 회전하기 (0) | 2024.09.06 |
[TIL #6] 햄버거 만들기 (0) | 2024.08.23 |