풀스택 맛보기8-Join & Login
User
Model 생성
src/models/User.js
import mongoose from "mongoose";
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
});
const User = mongoose.model("User", userSchema);
export default User;
init.js에 User 모델 import
import "./models/User.js";
=> DB에게 알려주는 과정
form 생성
회원가입 form 작성 src/views/join.pug
form(method="POST")
input(name="username" type="text" placeholder="아이디" required)
input(name="password" type="password" placeholder="비밀번호" required)
input(name="nickname" type="text" placeholder="닉네임" required)
input(name="email" type="email" placeholder="이메일" required)
input(type="submit" value="가입하기")
form에서 받은 정보 post하기 src/controllers/userController.js
export const postJoin = async (req, res) => {
const { username, password, nickname, email } = req.body;
await User.create({
username,
password,
nickname,
email,
});
return res.redirect("login");
};
Password
Hashing
- password를 그대로 저장하면 쉽게 노출되므로 hashing 해준다.
- Hashing은 일방향 함수이다.
- 입력값을 입력하면 출력값이 나오지만 출력값으로 입력값을 알아낼 수 없다.
bcrypt
- rainbow table 공격을 막아준다. 🔧
npm install bcrypt
해싱하기
userSchema.pre("save", async function () {
this.password = await bcrypt.hash(this.password, 5);
});
💡숫자 5: salt 횟수이다.
bcrypt.compare
- 로그인 할 때 유저가 form에 입력한 숫자를 해싱한 값과
회원가입시 해싱된 값을 비교하여 로그인 성공여부를 결정한다.const ok = await bcrypt.compare(password, user.password);
Login
로그인 오류 메시지 출력
const usernameExists = await User.exists({ username: username });
if (usernameExists) {
return res.render("join", { pageTitle: "Join", errorMessage: "이미 존재하는 아이디입니다." });
}
회원가입 성공여부 알려주기
조건에 의해 회원가입이 실패하면 에러 메시지는 뜨지만
브라우저에서 성공한줄 알고 아이디와 암호를 저장하려고 한다.
그래서 상태 코드를 이용하여 성공 여부를 알려줌으로 해결한다.
status code
- HTTP 응답 상태를 알려주는 코드이다.
2xx : success 4xx : client errors
=> 200번대 상태코드를 받는다면 URL을 History에 저장하지만
400번대 상태코드를 받으면 URL을 저장하지 않는다.
if (password !== password2) {
return res.status(400).render("join", { pageTitle, errorMessage: "패스워드가 일치하지 않습니다." });
}
쿠키
- 정보를 주고받는 방식이며 session ID를 저장하고 전송한다.
세션
- backend와 browser간에 어떤 활동을 했는지 기억한다.
express-session
- express에서 세션을 처리할 수 있게 해주는 미들웨어.
🔧 npm install express-session
server.js //Router 위에 추가
app.use(
session({
secret: "Hello!",
resave: true,
saveUninitialized: true,
})
);
=> 위의 미들웨어가 사이트로 들어오는 것을 기억하게 된다.
💡 resave : 변경 사항이 없어도 저장 💡 saveUninitialized : 세션 초기화 전에도 저장
MongoDB에 세션 저장
export const postLogin = async (req, res) => {
const { pageTitle } = "로그인";
const { username, password } = req.body;
const user = await User.findOne({ username });
if (!user) {
return res.status(400).render("login", { pageTitle, errorMessage: "아이디가 존재하지 않습니다." });
}
const ok = await bcrypt.compare(password, user.password);
if (!ok) {
return res.status(400).render("login", { pageTitle, errorMessage: "비밀번호가 일치하지 않습니다." });
}
req.session.loggedIn = true;
req.session.user = user;
res.redirect("/");
};
locals 전역변수
- res.locals object를 이용하면 template에 전역적으로 변수를 보낼 수 있다.
middlewares.js 생성
export const localsMiddleware = (req, res, next) => {
res.locals.loggedIn = Boolean(req.session.loggedIn);
res.locals.siteName = "Ἀγορά";
res.locals.loggedInUser = req.session.user;
next();
};
server.js에서 middlewares.js 사용하기
app.set("view engine", "pug");
app.set("views", process.cwd() + "/src/views");
app.use(logger);
app.use(express.urlencoded({ extended: true }));
app.use(
session({
secret: "Hello!",
resave: true,
saveUninitialized: true,
})
);
app.use(localsMiddleware);
app.use("/", globalRouter);
app.use("/users", userRouter);
app.use("/posts", postRouter);
base.pug에서 locals에 있는 전역변수 loggedIn 사용예시
if loggedIn
li
a(href="/users/logout") 로그아웃
li
a(href="/posts/upload") 글쓰기
else
li
a(href="/login") 로그인
li
a(href="/join") 회원가입
⭐ 정리
- 브라우저(클라이언트)가 서버에 접근
- 서버가 브라우저에게 쿠키를 줌
- 세션에 쿠키와 관련된 정보를 저장
- 브라우저가 서버에 다시 접근할 때 쿠키를 보여줌
- 서버는 쿠키를 통해 브라우저를 구분
Mongo Store
- ssession data를 저장하기 위해 사용한다.
💡💡💡
- 쿠키 안에는 session ID만 저장되고 session data는 저장되지 않는다.
- session data는 서버쪽에 저장된다.
🔧 npm install connect-mongo
app.use(
session({
secret: "Hello!",
resave: true,
saveUninitialized: true,
store: mongoStore.create({ mongoUrl: "mongodb://127.0.0.1:27017/agora" }),
})
);
Mongo DB
> show collections
posts
sessions
users
=> Mongo DB에서 sessions가 추가된 것을 확인할 수 있다.
생성된 세션 확인
> db.sessions.find()
{ "_id" : "Qa6Yf2mv3WBnm-PSS92vhHMSJITcz02G", "expires" : ISODate("2022-09-13T04:25:12.466Z"), "session" : "{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"}}" }
Session authentication(세션 인증) 문제
- 모든 방문자에 대해 쿠키를 저장하는 문제
resave & saveUninitialized false로 변경
- server.js
app.use( session({ secret: "Hello!", resave: false, saveUninitialized: false, cookie: { maxAge: 20000, }, store: MongoStore.create({ mongoUrl: "mongodb://127.0.0.1:27017/agora" }), }) );💡 secret : 쿠키에 sign할 때 사용하는 string //session hijack을 방지하기 위함이다. 💡 resave : 변경 사항이 없어도 저장 💡 saveUninitialized : 세션 초기화 전에도 저장 💡 maxAge: 쿠키를 유지할 시간을 정함 // 20000 -> 20초
Token authentication
- IOS or 안드로이드는 쿠키를 갖지 않기 때문에 token을 사용함
Url 비공개
environment file(환경변수) .env 생성
코드에 들어가면 안되는 값들을 지정
/.envCOOKIE_SECRET = [YOUR Cookie Secret] DB_URL = [YOUR DB URL]사용 방법
server.jsapp.use( session({ secret: process.env.COOKIE_SECRET, resave: false, saveUninitialized: false, store: MongoStore.create({ mongoUrl: process.env.DB_URL }), }) );=> 그러나 지금은 COOKIE_SECRET과 DB_URL을 출력하면 undefined를 반환받는다.
⚠️ gitignore에도 .env 추가하기!!
dotenv
🔧 npm install dotenv
가능한 제일 먼저 실행하게 해준다.
- init.js
import "dotenv/config";
깃허브로 로그인하기
- Github-Settings-Oauth Apps-new OAuth App 클릭 2.
- login.pug에 github 로그인 주소 추가
form(method="POST") input(name="username" type="text" placeholder="아이디" required) input(name="password" type="password" placeholder="비밀번호" required) input(type="submit" value="로그인") br a(href="https:/github.com/login/oauth/authorize?[Client ID]") Github로 로그인하기 →
scope
- 유저에게 어떤 정보를 가져올 것인지 설정
email 정보만 가져오기
a(href="https:/github.com/login/oauth/authorize?[Client ID]&scope=user:email")
결과 => oauth-scope-email.png
URL 정리하기
github에서 준 code를 Access Token으로 바꿔준다.
⚠️ client_secret은 무조건 backend에 존재해야한다.
code는 10분이면 만료한다.
npm install node-fetch@2.6.1
로그아웃
export const logout = (req, res) => {
req.session.destroy();
res.redirect("/");
};