본문 바로가기

블록체인_9기/💚 Node.js

39강_230526_Node.js(로그인, 회원가입, 게시판 페이지 구현_html로 front 작업)

728x90

 

 

 


cors

cors (Cross-Origin Resource Sharing)란?

  • 자신이 속하지 않은 도메인이나 다른 프로토콜, 다른 포트에 있는 리소스 요청을 요청하는 cross-origin HTTP요청 방식이다.

출처: https://surprisecomputer.tistory.com/32

  • 좌측의 웹 사이트는 domain-a.com으로 구동되고 있다. 파란색 이미지는 웹사이트가 구동 중인 domain-a.com 으로부터 자료를 요청하고 수신한다.
  • 하지만 아래의 빨간색 이미지는 domain-b.com라는 다른 도메인에 자료를 요청하고 수신한다.
    • 이러한 방식이 cross-origin HTTP요청 방식이다.
  • 중요한 부분은 서버는 기본적으로 cors 방식을 제한해둔다.
    • 특정 서버 리소스에 다른 임의의 웹 사이트들으 request를 보낼 수 있다면 악의적으로 특정 서버의 세션을 탈취하거나 서버에 무리가 가는 행위 등의 문제가 발생할 수 있기 때문이다.
    • 따라서 다른 사이트에서 나의 서버에 요청하는 것을 허가하고 싶은 방법에는 모두에게 제한 없이 제공하는 방식과 본인이 직접 특정 도메인들만 허용하는 방식이 있다.

node.js_모두에게 허용하는 방식

  • 별도의 값 없이 아래의 내용대로 입력을 하면 모든 도메인에서 제한 없이 서버에 요청을 보내고 응답을 받을 수 있다.
const cors = require('cors');

app.use(cors());

 

node.js_특정 도메인에게 허용하는 방식

  • 자신이 허용할 도메인을 추가하면 해당 도메인은 제한없이 해당 서버에 요청과 응답을 보낼 수 있다.
app.use(cors({
    // 도메인 허용 옵션
    // 1. 접근을 허용할 도메인을 작성
        // 여러개의 도메인을 허용하고 싶다면 배열의 형태로 넣어주면 된다.
    origin : "http://127.0.0.1:5500",
    // 클라이언트의 요청에 쿠키를 포함할지의 속성
    credentials : true,
}))

 

 

 

 


회원가입, 로그인, 게시판 페이지 구현

#. 폴더의 경로는 다음과 같다.

1. app.js_ 사용할 모듈을 불러오고 웹 서버를 열어준다.

  • 사용할 모듈을 저장 후 불러온다.
    • npm i bcrypt cors dotenv express express-session jsonwebtoken mysql2 sequelize
const express = require("express");
const cors = require("cors");
const dot = require("dotenv").config();
const session = require("express-session");
  • 서버 인스턴스를 생성한다.
const app = express();
  • body 객체를 가져온다.
app.use(express.urlencoded({extended:false}));
  • session을 사용하기 위한 설정을 한다.
app.use(session({
    secret : process.env.SESSION_TOKEN_KEY,
    resave : false,
    saveUninitialized : false
}))
  • sequelize 연결 매핑을 설정한다.
//forse: 데이터 베이스에 있으면 해당 값을 초기화 시킬지 유무를 설정
sequelize.sync({forse : false}).then(()=>{
    console.log("연결성공")
}).catch((err)=>{
    console.log(err)
})
  • 서버를 대기시킨다.
app.listen(8080, ()=>{
    console.log("서버열림")
})
  • cors 모듈을 사용하여 도메인을 허용한다.
    • 지금의 경우, Live server의 경로를 허용한다.
// 다른 도메인에서 악의적으로 접근할 수 없도록
// 도메인 접근 시 발생하는 보안 정책

// 다른 도메인과 통신을 안전하게 유지 시키기 위해서 보안 정책이 있다.
// cors 모듈을 가지고 도메인을 허용 해주자
// Access-control-Allow-origin을 헤더에 포함해서
// 접근을 허용하고 응답하고 브라우저에서 응답을 받은 뒤
// 헤어 값을 확인해서 접근을 허용 또는 차단한다.
app.use(cors({
    // 도메인 허용 옵션
    // 1. 접근을 허용할 도메인을 작성
        // 여러개의 도메인을 허용하고 싶다면 배열의 형태로 넣어주면 된다.
    origin : "http://127.0.0.1:5500",
    // 클라이언트의 요청에 쿠키를 포함할지의 속성
    credentials : true,
}))

 

2.  .env_DATABASE의 정보키를 작성하고, SESSION_KEY와 ACCESS_TOKEN_KEY를 작성한다.

DATABASE_PASSWORD = 000000
DATABASE_USERNAME = root
DATABASE_NAME = test11
DATABASE_HOST = 127.0.0.1
ACCESS_TOKEN_KEY = myaccessToken
SESSION_TOKEN_KEY = mysessionToken

 

3. config/index.js_sequelize 객체 생성 시 들어갈 데이터를 설정한다.

const config = {
    dev: {
        username: process.env.DATABASE_USERNAME,
        password: process.env.DATABASE_PASSWORD,
        database: process.env.DATABASE_NAME,
        host: process.env.DATABASE_HOST,
        dialect: "mysql"
    }
}

module.exports = config;

 

4. models/users.js_sequelize의 User의 모델을 설정한다.

  • hasMany 메서드를 이용하여 Post 테이블의 참조키 user_id에 User테이블의 id 칼럼을 참조시킨다.
const Sequelize = require("sequelize");

class User extends Sequelize.Model{
    static init(seq){
        return super.init({
            name: {
                type: Sequelize.STRING(20),
                allowNull: false
            },
            age : {
                type: Sequelize.INTEGER,
                allowNull: false
            },
            user_id : {
                type: Sequelize.STRING(20),
                allowNull: true
            },
            user_pw : {
                type: Sequelize.STRING(64),
                allowNull: true
            }
        }, {
            sequelize : seq,
            timestamps : true,      // 추가 시간, 수정 시간 자동 생성
            underscored : false,    // 카멜 케이스 할건지 유무 설정
            modelName : "User",     // 노드 프로젝트에서 사용할 이름(조회했을 때 보임)
            tableName : "users",    // 데이터베이스 테이블의 이름
            paranoid : false,       // 삭제 시간 표기 유무
            charset : "utf8",       // 인코딩 방식
            collate: "utf8_general_ci"
        })
    }

    static associate(db){
        db.User.hasMany(db.Post, {foreignKey : "user_id", sourceKey: "id"})
    }

}

module.exports = User;

 

5. models/posts.js_sequelize의 Post의 모델을 설정한다.

  • belongsTo 메서드를 이용하여 참조키 user_id에 User테이블의 id 칼럼을 참조한다.
const Sequelize = require("sequelize");

class Post extends Sequelize.Model{
    static init(seq){
        return super.init({
            title : {
                type: Sequelize.STRING(20),
                allowNull: false
            },
            content : {
                type : Sequelize.STRING(400),
                allowNull : false
            }
        }, {
            sequelize : seq,
            timestamps : true,
            underscored : false,
            modelName : "Post",
            tableName: "posts",
            paranoid: false,
            charset: "utf8",
            collate: "utf8_general_ci"
        })
    }
    static associate(db){
        db.Post.belongsTo(db.User, {foreignKey: "user_id", targetKey:"id"})
    }
}

module.exports = Post;

 

6. models/index.js_Sequelize 객체를 생성하고, Post와 User의 테이블을 담아 내보낸다.

const Sequelize = require("sequelize");

const config = require("../config");
const User = require("./users");
const Post = require("./posts")

const sequelize = new Sequelize(
    config.dev.database,
    config.dev.username,
    config.dev.password,
    config.dev
)

const db = {};

db.sequelize = sequelize;
db.User = User;
db.Post = Post;

// 테이블 초기화
User.init(sequelize);
Post.init(sequelize);

User.associate(db)
Post.associate(db)

module.exports = db;

 

7. signUpController.js_회원가입시 db의 User테이블에 데이터를 추가한다.

  • 중복회원 가입을 막기위해 유저의 아이디는 고유값이어야 한다.
    • findOne 메소드를 이용해 유저가 가입하고자 하는 아이디가 존재하는지 확인한다.
    • 같은 값이 있으면 찾아진 값이 반환되므로, 같은 값이 없다면 null이 뜬다.
const { User } = require("../models");

const bcrypt = require("bcrypt");

exports.SignUp = async(req,res)=>{
    try {
        const { name, age, user_id, user_pw } = req.body;
        const user = await User.findOne({where : {user_id}});
        if(user != null){
            // 유저가 조회된 것. 중복회원 가입을 막는다.
            return res.send("이미 사용중인 아이디 입니다.");
        }

        const hash = bcrypt.hashSync(user_pw, 10);
        await User.create({
            name,
            age,
            user_id,
            user_pw : hash
        })
        res.redirect("http://127.0.0.1:5500/frontEnd/login.html")
    } catch (error) {
        console.log(error);
    }
}

res.redirect("http://127.0.0.1:5500/frontEnd/login.html")

  • 프론트단에서 ejs 사용하면 res.redirect("/login") 형식으로 작성을 하게 되는데 html로 작업을 할 때와, ejs로 작업을 할 때 경로 설정에서 왜 이런 차이가 발생하는지 궁금해졌다. 
결론적으로,
ejs는 서버에서 동적으로 생성되는 HTML페이지이고,
HTML은 정적인 클라이언트 측 리소스이므로, 완전한 URL 주소를 제공해주어야 한다는 것이다.

ejs의 경우

  • ejs는 서버단의 코드와 함께 사용되며, 서버에서 렌더링 되어 클라이언트에게 전해진다.

html의 경우

  • 서버의 경로와 상관없이, 서버에서 클라이언트로 직접 제공된다.
  • 따라서 클라이언트 측에서 해당 파일을 찾을 수 있는 정확한 경로를 제공해야 하므로, 완전한 주소를 입력해야한다.

 

8. loginController.js_로그인 시, db에 저장되어 있는 id, pw를 유저가 입력한 내용과 비교한다.

  • 사용자가 입력한 아이디 값과, User테이블에 저장된 내용 중 user_id 컬럼에서 같은 값을 가지고 있는 row가 있는지 확인한다.
  • bcrypt의 compareSync를 사용하여, 유저가 입력한 패스워드와 User테이블에서 같은 id값으로 가진 비밀번호가 같은 값인지 비교한다.
  • 만약 아이디와 비밀번호가 동일하다면, 세션에 토큰을 발급해주며, 로그인을 성공시킨다.
const { User }  = require("../models");

//로그인 bcrypt jsonwebtoken

const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");

exports.Login = async(req,res)=>{
    try {
        const { user_id, user_pw } = req.body;
        console.log(user_id,user_pw);
        const user = await User.findOne({where : {user_id}});
        if(user == null){
            return res.send("가입 안한 아이디임");
        }

        const same = bcrypt.compareSync(user_pw, user.user_pw);

        const { name, age, id } = user;
        if(same){
            let token = jwt.sign({
                name,
                age,
                id
            }, process.env.ACCESS_TOKEN_KEY, {
                expiresIn : "20m"
            });
            req.session.access_token = token;
            // ' / ': 여기서 경로는 백엔드의 도메인 경로 루트이다.
            // 때문에프론트의 경로를 작성해주자

            // 이런 경우는 배포된 프론트의 경로이다.
            return res.redirect("http://127.0.0.1:5500/frontEnd/main.html")
           
            // 이렇게 리다이렉트를 할 게 아니면 프론트에서 응답 받은 값으로 
            // 조건 분기 처리해서 페이지를 전환시켜주면 된다.
            // return res.send("로그인 완료");
        }
        else{
            return res.send("비밀번호 틀림");
        }
    } catch (error) {
        // console.log("난 이키보드 좋던뎅 ㅎ");
        // console.log("안녕하세요 쑤잌쑤잌 아직안옴 판씨가 가지고있음 ㅎ");
        console.log(error)
    }
}

 

  • 미들웨어로 받은 현재 로그인한 유저의 값을 가져와 User테이블에서 findOne메소드를 사용하여 해당 유저의 정보를 res.json으로 클라이언트 단으로 보내준다.
// 유저 정보를 확인할 수 있게함
exports.viewUser = async(req,res)=>{
    const { acc_decoded } = req;
    console.log("변경????????????????????",acc_decoded);
    const user = await User.findOne({
        // where : {name : acc_decoded.name} => where : {id : acc_decoded.id}
        // userUpdate를 실행하면 User테이블의 name값이 변경되어 로그인 시 지장?생성?저장?된 acc_decoded의 name값과
        // 새로 저장한 User테이블의 name값과 달라져 찾을 수 없으므로 고정값인 id로 변경하여 오류를 해결! 아현성생님
        where : {id : acc_decoded.id}
    })
    // json 형태로 데이터를 응답
    res.json({user})
}

 

  • 현재 로그인한 유저의 정보를 변경한다.
    • 미들웨어로 현재 로그인한 유저의 값을 가져와 where로 User테이블에서 해당 유저의 정보를 찾고 클라이언트 단에서 수정을 위해 입력한 유저의 이름과 나이의 값을 User.update 메소드를 사용하여 수정한다. 
exports.userUpdate = async(req,res)=>{
    const {acc_decoded} = req;
    const {user_name, user_age} = req.body;
    const user = await User.update(
        {name: user_name, age: user_age},
        {where : {id: acc_decoded.id}}
    )
    // console.log("----------------",user);


    res.redirect("http://127.0.0.1:5500/frontEnd/mypage.html")

}

9. postController.js_모든 게시글을 조회, 내가 쓴 게시글 조회, 게시글 등록, 게시글 수정, 게시글 삭제에 관한 함수를 설정한다.

const { Post, User } = require("../models");

exports.PostAll = async(req,res)=>{
    const post = await Post.findAll({})
    res.json(post)
}

exports.myPost = async(req,res)=>{
    const { acc_decoded } = req;
    const post = await Post.findAll({where : {user_id : acc_decoded.id}})

    res.json(post)
}

exports.PostCreate = async(req,res)=>{
    const { acc_decoded } = req;
    // console.log("acc_decoded",acc_decoded);
    const { title, content } = req.body;
    // console.log("%%%%%%%%%%%%%%%",title, content);
    await Post.create({
        title: title,
        content: content,
        user_id: acc_decoded.id
    })
    return res.redirect('http://127.0.0.1:5500/frontEnd/main.html')
}

exports.PostUpdate = async (req,res)=>{  
    const { id } = req.params;
    const { postT, postC } = req.body;

    await Post.update({title:postT, content: postC}, {where:{id : id}})
    res.redirect('http://127.0.0.1:5500/frontEnd/main.html')
}

exports.PostDel = async (req,res)=>{
    const { id } = req.params;
    await Post.destroy({where: {id:id}})

    res.redirect('http://127.0.0.1:5500/frontEnd/main.html')
}

10. loginMiddleware.js_로그린이 정상적으로 이루어지고 있는지 확인하는 미들웨어를 설정한다.

const jwt = require("jsonwebtoken");

exports.isLogin = (req,res,next) =>{
    const {access_token} = req.session;
    console.log("미들웨어", access_token);

    jwt.verify(access_token, process.env.ACCESS_TOKEN_KEY, (err, acc_decoded)=>{
        if(err){
            res.send("다시 로그인 해주세요");
        }
        else{
            req.acc_decoded = acc_decoded;
            // 다음 미들웨어 실행
            next();
        }
    })
}

 

11. app.js_각 라우터별로 기본 경로를 설정한다.

  • cors로 허용해준 http://127.0.0.1:5500 경로는 기본경로이며, 도메인이다. 
    • http://127.0.0.1:5500/signUp : 회원가입 페이지로 이동된다.
    • http://127.0.0.1:5500/login: 로그인 페이지로 이동된다.
    • http://127.0.0.1:5500/Post: 게시판 페이지로 이동된다.
const loginRouter = require("./routers/login");
const signUpRouter = require("./routers/signUp");
const postRouter = require("./routers/Post");


app.use("/signUp", signUpRouter);
app.use("/login", loginRouter);
app.use("/Post", postRouter);

 

12. routers/signUp.js_http://127.0.0.1:5500/signUp ~ 접속 시, 컨트롤러를 연결한다.

  • http://127.0.0.1:5500/signUp 으로 접속 시, signUpController의 Signup 컨트롤러가 실행된다.
const router = require("express").Router();
const { SignUp } = require("../controllers/signUpController");

router.post("/", SignUp);

module.exports = router;

 

13. routers/signUp.js_http://127.0.0.1:5500/login ~ 접속 시, 컨트롤러를 연결한다.

  • http://127.0.0.1:5500/login으로 접속 시, loginController의 Login 컨트롤러가 실행된다.
  • http://127.0.0.1:5500/login/viewhttp://127.0.0.1:5500/login/update 를 접속 시, isLogin 미들웨어를 실행 후, 각 컨트롤러의 함수를 실행한다.
const router = require("express").Router();
const { Login, viewUser, userUpdate } = require("../controllers/loginController");
const { isLogin } = require("../middleware/loginMiddleware")

router.post('/',Login);

router.get('/view', isLogin, viewUser);

router.post('/update', isLogin, userUpdate);


module.exports = router;

 

14. routers/signUp.js_http://127.0.0.1:5500/Post ~ 접속 시, 컨트롤러를 연결한다.

const router = require("express").Router();

const { PostAll, PostCreate, myPost, PostUpdate, PostDel } = require("../controllers/postController");
const { isLogin } = require("../middleware/loginMiddleware");

router.get('/posts', isLogin, PostAll);

router.get('/myposts', isLogin, myPost);

router.post('/', isLogin, PostCreate)

router.post('/postupdate/:id', isLogin, PostUpdate);

router.post('/postDelete/:id', isLogin, PostDel);

module.exports = router;

 

15. frontEnd/signUp.html_회원가입 페이지를 작성한다.

  • 프론트 단에서 데이터를 get / post 할 때, 요청 경로를 서버를 열어주었던 주소로 설정하면 된다.
    • 지금의 경우, app.js에서 8080으로 서버를 열었으므로,  http://127.0.0.1:8080 ~ 가 되는 것이다.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>회원가입</h1>
    <!-- 프론트랑 백을 나누었기 때문에 백엔드의 경로를 적어주어야 한다. -->
    <!-- 백엔드를 배포해놨다고 가정할 때 백엔드의 요청 경로를 작성 해주면 된다. -->
    <!-- action 속성에 요청 url을 작성한다. -->
    <!-- 예를 들어, http://do.con/login -->
    <form action="http://127.0.0.1:8080/signUp" method="post">
        <input type="text" name="name" placeholder="이름"> <br>
        <input type="text" name="age" placeholder="나이"> <br>
        <input type="text" name="user_id" placeholder="아이디"> <br>
        <input type="password" name="user_pw" placeholder="비밀번호"> <br> <br>
        <button>회원가입</button>
    </form> <br>
    <a href="http://127.0.0.1:5500/frontEnd/login.html">로그인</a>
</body>
</html>

 

16. frontEnd/main.html_axios로 전체 게시글 내용과 로그인한 유저의 정보를 가져온다.

    async function getAPI(){
        try {
            const {data} = await axios.get('http://127.0.0.1:8080/login/view',{
                // 브라우저가 쿠키를 서버로 전달할 수 있는 옵션
                withCredentials : true
            })
            const data2 = await axios.get('http://127.0.0.1:8080/Post/posts',{
                // 브라우저가 쿠키를 서버로 전달할 수 있는 옵션
                withCredentials : true
            })

            user_name.innerHTML = data.user.name;
            user_age.innerHTML = data.user.age;
            
            isLoading.classList.add("disable");
            console.log("정보",data2.data);

            let postArr = Object.entries(data2.data);
            console.log("#################",postArr)
            
            postArr.forEach((el, index) => {
                console.log("sfsdfsdfsdf",el)
                posts.innerHTML += `
                <div>
                    제목: ${el[1].title} <br>
                    내용: ${el[1].content}
                    <br><br><br>
                </div>
                `
            });
            // console.log("정보",data.user.name);

        } catch (error) {
            console.log(error)
        }
    }
    getAPI();

 

 

 

 

 

 

 

 

728x90