리액트에서 인증(로그인) 상태에 따라 진입여부를
Auth를 HOC(Higher-order component)를 이용하여 구현하고
페이지가 작동하는 것을 확인하고 다시 실행해보니 프록시 에러가 발생합니다
이것저것 해보다가 갑자기 에러가 사라지고 모든 기능이 제대로 작동해서 서버, 클라이언트, 텍스트에디터, 크롬 등등.. 을 종료하고 다시 실행하니 프록시 에러가 다시 발생했습니다.
[브라우저 콘솔 확인]]
콘솔창의api/hello OK!
을 통해 서버와 클라이언트가 실행되고 LandingPage에서의 네트워크 요청은 정상적으로 이루어 졌고,
콘솔창의GET http://localhost:3000/api/users/auth
부분에서의 요청이 제대로 이루어지지 않았다는 것을 알 수 있었습니다
[터미널 확인]
JsonWebTokenError: jwt must be provided
JWT가 제공되어야 하는데 제공되지 않았다는 것을 알 수 있었습니다
JsonWebTokenError: jwt must be provided
이 오류는 JWT 토큰이 제공되지 않았을 때 발생합니다. 즉, JWT 라이브러리가 토큰을 해석하려고 시도했지만, 전달된 토큰이 없거나 정의되지 않았기 때문에 이런 오류가 발생한 것입니다.
const { User } = require('../models/User')
let auth = async (req, res, next) => {
try {
// 클라이언트 쿠키에서 토큰 가져옴
let token = req.cookies.user_auth
// 토큰을 복호화 후 유저 찾음
const user = await User.findByToken(token);
if (!user) {
return res.json({ isAuth: false, error: true });
}
req.token = token;
req.user = user;
next()
} catch (err) {
throw err
}
};
module.exports = { auth }
해당 문제는 auth.js 파일에서 발생했습니다.
여기서 User.findByToken(token)
메소드를 호출하면서
token
을 인자로 전달하는데, 이 token
은 req.cookies.user_auth
에서 가져옵니다.
위 코드에서는 req.cookies.user_auth
값의 존재 여부를 검증하지 않고 바로 사용하였습니다. 따라서 클라이언트가 user_auth 쿠키를 보내지 않았다면
undefined
값을 token
으로 사용하게 되고, 그 결과 "jwt must be provided"라는 에러가 발생한 것입니다.
let token = req.cookies.user_auth // undefined일 수 있음
const user = await User.findByToken(token); // 여기서 에러 발생
const { User } = require('../models/User');
const auth = async (req, res, next) => {
try {
// 클라이언트 쿠키에서 토큰 가져옴
const token = req.cookies.user_auth;
if (!token) {
return res.json({ isAuth: false, error: true });
}
// 토큰을 검증하고 사용자를 찾음
const user = await User.findByToken(token);
if (!user) {
return res.json({ isAuth: false, error: true });
}
req.token = token;
req.user = user;
next();
} catch (err) {
throw err;
}
};
module.exports = { auth };
문제를 해결하기 위해 수정된 코드에서는 다음과 같이 user_auth 쿠키의 존재 여부를 먼저 확인합니다
const token = req.cookies.user_auth;
if (!token) {
return res.json({ isAuth: false, error: true });
}
위 코드에 의해 클라이언트가 user_auth 쿠키
를 제공하지 않았을 경우 요청 처리를 중단하고 { isAuth: false, error: true }
응답을 보내게 됩니다.
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const saltRounds = 10;
const jwt = require('jsonwebtoken');
const userSchema = mongoose.Schema({
...
})
userSchema.pre('save', function (next) {
let user = this;
if (user.isModified('password')) {
// 비밀번호를 암호화 시킨다.
bcrypt.genSalt(saltRounds, function (err, salt) {
if (err) return next(err)
bcrypt.hash(user.password, salt, function (err, hash) {
if (err) return next(err);
user.password = hash;
next()
})
})
} else {
next()
}
})
userSchema.methods.comparePassword = function (plainPassword) {
console.log(plainPassword)
return new Promise((resolve, reject) => {
bcrypt.compare(plainPassword, this.password, (err, isMatch) => {
if (err) {
reject(err)
} else {
resolve(isMatch)
}
})
})
}
userSchema.methods.generateToken = function () {
const user = this
const token = jwt.sign({ userId: user._id.toHexString() }, "secretToken")
user.token = token
return user.save()
.then(() => token)
}
userSchema.statics.findByToken = function (token) {
const user = this
return new Promise((resolve, reject) => {
jwt.verify(token, 'secretToken', (err, decoded) => {
if (err) {
reject(err)
}
user.findOne({ '_id': decoded.userId, 'token': token })
.then(user => {
resolve(user)
})
.catch(err => {
reject(err)
})
})
})
}
const User = mongoose.model('User', userSchema)
module.exports = { User }
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const saltRounds = 10;
const jwt = require('jsonwebtoken');
// 'secretToken' 값 설정 (임의로 선택한 시크릿 토큰)
const secretToken = 'yourSecretToken';
// 사용자 스키마 정의
const userSchema = mongoose.Schema({
name: {
type: String,
maxlength: 50,
},
email: {
type: String,
trim: true, // 공백 제거, 예: "ckd gml@naver.com" -> "ckdgml@naver.com"
},
password: {
type: String,
minlength: 5
},
lastname: {
type: String,
maxlength: 50
},
role: {
type: Number,
default: 0
},
image: String,
token: {
type: String
},
tokenExp: {
type: Number
}
})
// 'save' 이벤트 전(pre)에 비밀번호 암호화
userSchema.pre('save', function (next) {
let user = this;
if (user.isModified('password')) {
// 비밀번호를 암호화
bcrypt.genSalt(saltRounds, function (err, salt) {
if (err) return next(err)
bcrypt.hash(user.password, salt, function (err, hash) {
if (err) return next(err);
user.password = hash;
next()
})
})
} else {
next()
}
})
// 비밀번호 비교 메서드
userSchema.methods.comparePassword = function (plainPassword) {
console.log(plainPassword)
return new Promise((resolve, reject) => {
bcrypt.compare(plainPassword, this.password, (err, isMatch) => {
if (err) {
reject(err)
} else {
resolve(isMatch)
}
})
})
}
// JWT 토큰 생성 메서드
userSchema.methods.generateToken = function () {
const user = this
const token = jwt.sign({ userId: user._id.toHexString() }, secretToken)
user.token = token
return user.save()
.then(() => token)
}
// JWT 토큰 검증 및 사용자 찾기 메서드
userSchema.statics.findByToken = function (token) {
const user = this
return new Promise((resolve, reject) => {
jwt.verify(token, secretToken, (err, decoded) => {
if (err) {
reject(err)
}
User.findOne({ '_id': decoded.userId, 'token': token }) // 'User'로 수정
.then(user => {
resolve(user)
})
.catch(err => {
reject(err)
})
})
})
}
// User 모델 정의
const User = mongoose.model('User', userSchema)
module.exports = { User }
User.js의 변경점은 다음과 같습니다
1. secret key를 변수로 선언
이전코드
const secretToken = 'yourSecretToken';
수정 전 코드에서는 JWT 토큰을 생성할 때 "secretToken"이라는 하드 코딩된 문자열을 사용하여 시크릿 토큰을 정의했습니다.
수정된 코드
const secretToken = 'yourSecretToken'; const token = jwt.sign({ userId: user._id.toHexString() }, secretToken);
수정된 코드에서는 const secretToken = 'yourSecretToken';을 사용하여 시크릿 토큰을 변수로 정의했습니다. 이 변수를 사용하여 JWT 토큰을 생성합니다.
2. 'User' 모델 직접 참조
User.findOne({ '_id': decoded.userId, 'token': token })
이전 코드에서는
user
를 모델 이름으로 사용하고user.findOne
으로 사용자를 찾았습니다. 수정된 코드에서는User
모델을 정확히 사용하고User.findOne
으로 사용자를 찾습니다. 이렇게 수정함으로써 모델과 메서드를 일관되게 사용하게 되며, 오타나 오류를 방지할 수 있습니다.
이제 잘 작동한다 ㅠㅠ