Youtube Clone Coding (5. USER AUTHENTICATION)

LeeJaeHoon·2021년 10월 25일
0
post-thumbnail
  1. hashing

    1. 입력값→ 출력값 0
    2. 출력값 → 입력값 x
    • 사용법
      • npm i bcrypt

        import bcrypt from "bcrypt";
        import mongoose  from "mongoose";
        
        userSchema.pre("save", async function() {
          //this는 create되는 User을 가리킴
          this.password = await bcrypt.hash(this.password, 5);
        })
      • bcrypt.hash(this.password, 5)

        → 1번째 인자에는 hash할 비밀번호가 들어감.

        → 2번째 인자에는 hash를 몇번 할 것인지 Number으로 들어감.

      • hashing한 비밀번호 login할때 맞는지 비교하기

        • bcrypt.compare(password, user.password)
          - 첫번째 인자는 사용자가 login할때 적은 password
          - 2번째 인자는 사용자가 가입할때 적은 비밀번호(hash된 비밀번호)

          const match = await bcrypt.compare(password, user.password);
            if(!match) {
              return res.status(400).render("login",{ 
                pageTitle,
                errorMessage: "Worng Password"
              });
            }
  2. 이미 create한 user인지 확인하는 법

    • $or
      • username과 email 둘중에 하나만 있어도 true를 return한다.

        const exits = await User.exists({$or: [{ username }, { email }]});
  3. confirm password

    • req.body에서 password와 password2를 가져와 비교한다.
      if(password !== password2){
          return res.render("join", {
            pageTitle: "join",
            errorMessage : "Password confirmation does not match"
          });
        }
  4. status 400/404

    • status를 알맞는 번호로 브라우저에 안보내주면 이미있는 username으로 join을해서 errorMessage가 뜸에도 불구하고 브라우저는 모르기때문에 비밀번호를 저장할려는지 물어본다. 이를 방지하기위해 errorMessage를 보낼때 status도 알맞은 번호로 브라우저에 보내주면 된다.
    • 400
      • Bad Request로써, 요청 실패-문법상 오류가 있어서 서버가 요청 사항을 이해하지 못함
    • 404
      • Not Found, 문서를 찾을 수 없음->클라이언트가 요청한 문서를 찾지 못한 경우에 발생함 (URL을 잘 살펴보기)
    • Example
    if(password !== password2){
        return res.status(400).render("join", {
          pageTitle: "join",
          errorMessage : "Password confirmation does not match"
        });
      }
  5. session

    • npm i express-session
      • server.js에 session을 middleware로 설정

        app.use(session({
          secret: "Hello!",
          resave: true,
          saveUninitialized: true,
        }))
        1. 브라우저 서버에 접근
        1. 서버가 브라우저에게 session id를 준다.
        1. 브라우저는 서버에서 받은 session id를 Cookie에 저장한다./ express도 session id를 session db에 저장한다.
        1. 브라우저가 localhost:4000의 모든 url에 요청을 보낼 때마다 session id를 요청과 함께 보낸다.
        1. 서버는 session id를 통해 어떤 유저가, 어떤 브라우저에서 요청을 보냈는지 알 수 있다.
    • req.session
      • 위에 작성된 session 미들웨어를 통해 req.session을 사용 할 수 있다.

      • user가 로그인 할때 req.session에 user내용을 저장하게 한다.

        export const postLogin = async(req, res) => {
          const { username, password } = req.body;
          const pageTitle = "Login"
          const user = await User.findOne({username});
          if(!user){
            return res.status(400).render("login", {pageTitle, errorMessage:"존재하지 않는 사용자입니다."})
          }
          const match = await bcrypt.compare(password, user.password);
          if(!match) {
            return res.status(400).render("login",{ 
              pageTitle,
              errorMessage: "Worng Password"
            });
          }
          req.session.loggedIn = true;
          req.session.user = user;
          return res.redirect("/");
        }
    • mongo에 session 저장하기
      • 위의코드로만 ssesion을 쓴다면 서버가 재시작 할 때 마다 session이 사라진다. 따라서 session을 mongodb에 저장 해 주어야 한다.

      • npm i connect-mongo

        import session from "express-session";
        import MongoStore from "connect-mongo";;
        
        app.use(session({
          secret: "Hello!",
          resave: true,
          saveUninitialized: true,
          store: MongoStore.create({
            mongoUrl: "mongodb://127.0.0.1:27017/wetube",
          })
        }))
      • 위의 코드로 session을 mongodb에 저장하면 로그인을 안한 유저까지 session이 저장되어 용량을 어마어마 하게 차지 할 수 있다.

      • 이를 방지하기위해 밑의 코드와 같이 session설정을 해주면 된다.
        - 아래와 같은 코드로 작성시 session을 초기화 할때마다 session을 mongodb에 저장한다.

            → 우리의 코드에서는 login할때 세션이 초기화됨! req.session에 값을 넣어주기 때문.
            
        app.use(
          session({
            secret: "Hello!",
            resave: false,
            saveUninitialized: false,
            store: MongoStore.create({
              mongoUrl: "mongodb://127.0.0.1:27017/wetube",
            }),
          })
    • secret과 MONGODB_URL 보호하기
      • .env파일 만들어서 secrit와 MONGODB_URL 변수로 두기
        - .env파일은 .gitignore에 들어가게한다.
        - 관습적으로 .env파일에 들어가는 변수는 대문자로 하는게 원칙.

        COOKIE_SECRET=dawdawdhkauwdhkuasdjldsgijdilsglosdgdskflaksd;
        DB_URL=mongodb://127.0.0.1:27017/wetube
      • 사용할때는 process.env.변수명으로 사용하기

        app.use(
          session({
            secret: process.env.COOKIE_SECRET,
            resave: false,
            saveUninitialized: false,
            store: MongoStore.create({
              mongoUrl: process.env.DB_URL,
            }),
          })
        );
      • npm i dotenv

      • 서버가 시작되는 첫 지점인 init.js에 import "dotenv/config"해주기

        • require로 불러오면 오류발생 → 이유 import는 선언문이라 Hoisiting이 되고 require는 표현식이라 import보다 늦게 호출되기때문.
  6. Github Login

    1. https://github.com/settings/developers로 접속하여 새로운 앱을 만든다.

      • Homepage URL 은 내 웹 애플리케이션의 URL
      • Callback URL은 사용자가 깃허브 로그인을 하면 redirect 될 페이지의 주소
    2. pug에 github로그인 창 만들기

      • a(href="https://github.com/login/oauth/authorize?client_id=b247bfa518854515df95&allow_signup=false") Continue with Github
      • url을 더 깔끔하게 만드는 방법
        • a(href="/users/github/start") Continue with Github

        • /users/github/start로 갔을때 controller를 만든다.

        • scope: "read:user user:email"을 설정 안해주면 나중에 user정보를 받을때 user정보를 얻을 수 없음.

          export const startGithubLogin = (req, res) => {
            const baseUrl = "https://github.com/login/oauth/authorize";
            const config = {
              client_id: "b247bfa518854515df95",
              allow_signup: false,
              scope: "read:user user:email",
            };
            const params = new URLSearchParams(config).toString();
            const finalUrl = `${baseUrl}?${params}`;
            return res.redirect(finalUrl);
          };
    3. return res.redirect(finalUrl)으로 finalUrl로 가면 github에 로그인하는 창이나온다. 로그인하면 생성한 앱을 만들때 쓴 callback url로 페이지가 이동한다.

      • callback url 로 페이지가 이동하면 페이지 url에 code가 나타난다
        • /users/github/finish?code=05105cc9dffadd998582
    4. 받은 code를 이용해 github에게 token요청하기

      • npm install node-fetch@2.6.1
        • 설치해야만 node에서 fetch를 쓸 수 있음
        • 2.6.1버전으로 설치해야 에러가 안뜸!
      • client_secret은 생성한 앱에서 확인 가능하다. (노출 하면 안되어 .env파일에 변수로 저장 해야함)
      export const finishGithubLogin = async (req, res) => {
        const baseUrl = "https://github.com/login/oauth/access_token";
        const config = {
          client_id: process.env.GH_CLIENT,
          client_secret: process.env.GH_SECRET,
          code: req.query.code,
        };
        const params = new URLSearchParams(config).toString();
        const finalUrl = `${baseUrl}?${params}`;
        const tokenRequest = await (
          await fetch(finalUrl, {
            method: "POST",
            headers: {
              Accept: "application/json",
            },
          })
        ).json();
      };
      • finalUrl에 POST요청을 주면 밑과 같은 json파일을 얻을 수 있음.
      {
        access_token: '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',
        token_type: 'bearer',
        scope: 'read:user,user:email'
      }
      export const finishGithubLogin = async (req, res) => {
        const baseUrl = "https://github.com/login/oauth/access_token";
        const config = {
          client_id: process.env.GH_CLIENT,
          client_secret: process.env.GH_SECRET,
          code: req.query.code,
        };
        const params = new URLSearchParams(config).toString();
        const finalUrl = `${baseUrl}?${params}`;
        const tokenRequest = await (
          await fetch(finalUrl, {
            method: "POST",
            headers: {
              Accept: "application/json",
            },
          })
        ).json();
        if ("access_token" in tokenRequest) {
          const { access_token } = tokenRequest;
          const userData = await (
            await fetch("https://api.github.com/user", {
              headers: {
                Authorization: `token ${access_token}`,
              },
            })
          ).json();
          console.log(userData);
        } else {
          return res.redirect("/login");
        }
      };
      • 얻은 user정보
      • scope: "read:user user:email을 설정해 주어서 가능한 것임.
      {
        login: 'abc5259',
        id: 62169861,
        node_id: 'MDQ6VXNlcjYyMTY5ODYx',
        avatar_url: 'https://avatars.githubusercontent.com/u/62169861?v=4',
        gravatar_id: '',
        url: 'https://api.github.com/users/abc5259',
        html_url: 'https://github.com/abc5259',
        followers_url: 'https://api.github.com/users/abc5259/followers',
        following_url: 'https://api.github.com/users/abc5259/following{/other_user}',
        gists_url: 'https://api.github.com/users/abc5259/gists{/gist_id}',
        starred_url: 'https://api.github.com/users/abc5259/starred{/owner}{/repo}',
        subscriptions_url: 'https://api.github.com/users/abc5259/subscriptions',
        organizations_url: 'https://api.github.com/users/abc5259/orgs',
        repos_url: 'https://api.github.com/users/abc5259/repos',
        events_url: 'https://api.github.com/users/abc5259/events{/privacy}',
        received_events_url: 'https://api.github.com/users/abc5259/received_events',
        type: 'User',
        site_admin: false,
        name: 'LeeJaeHoon',
        company: null,
        blog: '',
        location: null,
        email: 'dlwogns3413@naver.com',
        hireable: null,
        bio: null,
        twitter_username: null,
        public_repos: 18,
        public_gists: 0,
        followers: 0,
        following: 0,
        created_at: '2020-03-14T07:58:41Z',
        updated_at: '2021-10-19T23:28:59Z',
        private_gists: 0,
        total_private_repos: 1,
        owned_private_repos: 1,
        disk_usage: 73130,
        collaborators: 0,
        two_factor_authentication: false,
        plan: {
          name: 'free',
          space: 976562499,
          collaborators: 0,
          private_repos: 10000
        }
      }
      • 위의 정보에서 email이 유저가 primary로 설정을 안하면 못 가져 올 수도 있는데 그때를 위해 email을 따로 가져와야함
      • email은 "https://api.github.com/user/emails"을 통해 얻을 수 있음.
      export const finishGithubLogin = async (req, res) => {
        const baseUrl = "https://github.com/login/oauth/access_token";
        const config = {
          client_id: process.env.GH_CLIENT,
          client_secret: process.env.GH_SECRET,
          code: req.query.code,
        };
        const params = new URLSearchParams(config).toString();
        const finalUrl = `${baseUrl}?${params}`;
        const tokenRequest = await (
          await fetch(finalUrl, {
            method: "POST",
            headers: {
              Accept: "application/json",
            },
          })
        ).json();
        if ("access_token" in tokenRequest) {
          const { access_token } = tokenRequest;
          const apiUrl = "https://api.github.com";
          const userData = await (
            await fetch(`${apiUrl}/user`, {
              headers: {
                Authorization: `token ${access_token}`,
              },
            })
          ).json();
          const emailData = await (
            await fetch(`${apiUrl}/user/emails`, {
              headers: {
                Authorization: `token ${access_token}`,
              },
            })
          ).json();
          const email = emailData.find(
            email => email.primary === true && email.verified === true
          );
          if (!email) {
            return res.redirect("/login");
          }
        } else {
          return res.redirect("/login");
        }
      };
      • emailData는 밑과 같다
      [
        {
          email: 'dlwogns3413@naver.com',
          primary: true,
          verified: true,
          visibility: 'public'
        },
        {
          email: '62169861+abc5259@users.noreply.github.com',
          primary: false,
          verified: true,
          visibility: null
        }
      ]
      • 가져온 email이 현재 usersdb에 저장된 사람인지 확인 후 저장되어 있지 않은 user이면 userdb에 user을 create히고 이미 저장된 user이면 바로 로그인 시켜준다.
      export const finishGithubLogin = async (req, res) => {
        const baseUrl = "https://github.com/login/oauth/access_token";
        const config = {
          client_id: process.env.GH_CLIENT,
          client_secret: process.env.GH_SECRET,
          code: req.query.code,
        };
        const params = new URLSearchParams(config).toString();
        const finalUrl = `${baseUrl}?${params}`;
        const tokenRequest = await (
          await fetch(finalUrl, {
            method: "POST",
            headers: {
              Accept: "application/json",
            },
          })
        ).json();
        if ("access_token" in tokenRequest) {
          const { access_token } = tokenRequest;
          const apiUrl = "https://api.github.com";
          const userData = await (
            await fetch(`${apiUrl}/user`, {
              headers: {
                Authorization: `token ${access_token}`,
              },
            })
          ).json();
          const emailData = await (
            await fetch(`${apiUrl}/user/emails`, {
              headers: {
                Authorization: `token ${access_token}`,
              },
            })
          ).json();
          const emailObj = emailData.find(
            email => email.primary === true && email.verified === true
          );
          if (!emailObj) {
            return res.redirect("/login");
          }
          let user = await User.findOne({ email: emailObj.email });
          if (!user) {
            user = await User.create({
              avatarUrl: userData.avatar_url,
              name: userData.name,
              email: emailObj.email,
              username: userData.login,
              password: "",
              location: userData.location,
              socialOnly: true, 
            });
          }
          req.session.loggedIn = true;
          req.session.user = user;
          return res.redirect("/");
        } else {
          return res.redirect("/login");
        }
  7. LogOut

    • req.session.destroy()을 통해 sessionsDB에 저장되어있던 해당 브라우저의 session을 삭제한다.
      export const logout = (req, res) => {
        req.session.destroy();
        return res.redirect("/");
      };
  8. protector/public middleware

    • 로그인 안한 사람이 /edit/profile을 url로 쳐서 갈려할때 막아줘야함
      • protector middleware

        export const protectedMiddleware = (req, res, next) => {
          if (req.session.loggedIn) {
            next();
          } else {
            return res.redirect("/login");
          }
        };
    • 로그인 한 사람이 /login으로 가려할때 막아줘야함.
      • public middleware

        export const publicOnlyMiddleware = (req, res, next) => {
          if (!req.session.loggedIn) {
            next();
          } else {
            return res.redirect("/");
          }
        };
    • middleware 사용하기
      userRouter.get("/logout", protectedMiddleware, logout);
      userRouter.route("/edit").all(protectedMiddleware).get(getEdit).post(postEdit);
      • userRouter.route("/edit")에서 middleware 사용시 all 매소드를 이용하여 사용 할 수 있음.
      • userRouter.route("/edit").get(protectedMiddleware, getEdit).post(protectedMiddleware, postEdit); 이렇게 써도 무관함.
  9. edit profile

    • 밑과 같이 코드를 작성 한다면 usersdb에 있는 user은 update되지만 sessionsdb에 있는 user은 update가 되지 않음.(로그인 할때 req.session.user = user이란 코드를 직성하기 때문)
    export const postEdit = async (req, res) => {
      const {
        session: {
          user: { _id },
        },
        body: { name, email, username, location },
      } = req;
      const user = await User.findByIdAndUpdate(_id, {
        name,
        email,
        username,
        location,
      });
      return res.render("edit-profile", { pageTitle: "Edit Profile" });
    };
    • 따라서 req.session.user의 내용을 새로 업데이트한 user로 바꿔줘야함.
    • User.findByIdAndUpdate을 쓸때 3번째 인자로 { new: true }을 넣어줘야 update된 user을 리턴함 3번째 인자를 안넣으면 업데이트 하기전 user을 리턴함.
    export const getEdit = (req, res) => {
      return res.render("edit-profile", { pageTitle: "Edit Profile" });
    };
    
    export const postEdit = async (req, res) => {
      const pagetitle = "Edit Profile";
      const {
        session: {
          user: { _id },
        },
        body: { name, email, username, location },
      } = req;
      const updateUser = await User.findByIdAndUpdate(
        _id,
        {
          name,
          email,
          username,
          location,
        },
        { new: true }
      );
      req.session.user = updateUser;
      return res.redirect("/users/edit");
    };
    • 로그인한 유저가 edit profile을 할때 username과 email을 바꿨을 때 이미 존재하는지 확인하고 이미 존재하면 errorMessage를 보내줘야함
    export const getEdit = (req, res) => {
      return res.render("edit-profile", { pageTitle: "Edit Profile" });
    };
    
    export const postEdit = async (req, res) => {
      const pagetitle = "Edit Profile";
      const {
        session: {
          user: { _id },
        },
        body: { name, email, username, location },
      } = req;
      if (req.session.user.email !== email) {
        const existEmail = await User.exists({ email });
        if (existEmail) {
          return res.status(400).render("edit-profile", {
            pagetitle,
            errorMessage: "이미 있는 이메일 입니다.",
          });
        }
      }
      if (req.session.user.username !== username) {
        const existUsername = await User.exists({ username });
        if (existUsername) {
          return res.status(400).render("edit-profile", {
            pagetitle,
            errorMessage: "이미 있는 username 입니다.",
          });
        }
      }
      const updateUser = await User.findByIdAndUpdate(
        _id,
        {
          name,
          email,
          username,
          location,
        },
        { new: true }
      );
      req.session.user = updateUser;
      return res.redirect("/users/edit");
    };
  10. Change Password

    • oldPassword가 user의 password인지 확인후 맞으면 newPassword와 newPasswordConfirmation을 비교하고 둘이 같으면 change Password한다.
    • newPassword로 password를 바꿀때 해쉬를해서 userdb에 넣어줘야하므로 이전에 만들어둔 mongodb middleware을 사용하여야 한다. mongodb middleware가 사용될려면 create 메소드를 쓰거나 save 메소드를 써야하는데 create를 사용할 상황이 아니므로 save 메소드를 사용한다.
    • userdb에 user내용이 바뀌더라도 sessiondb에 user내용은 바뀌지 않으므로 둘다 바꿔 줘야 한다.
    export const postChangePassword = async (req, res) => {
      const {
        session: {
          user: { _id, password },
        },
        body: { OldPassword, newPassword, newPasswordConfirmation },
      } = req;
      const match = await bcrypt.compare(OldPassword, password);
      if (!match) {
        return res.status(400).render("change-password", {
          pageTitle: "Change Password",
          errorMessage: "현재 비밀번호가 일치하지 않습니다",
        });
      }
      if (newPassword !== newPasswordConfirmation) {
        return res.status(400).render("change-password", {
          pageTitle: "Change Password",
          errorMessage: "새로운 비밀번호가 일치하지 않습니다.",
        });
      }
      const user = await User.findById(_id);
      user.password = newPassword;
      await user.save();
      req.session.user.password = user.password;
      return res.redirect("/users/logout");
    }; 

0개의 댓글