관리 메뉴

평행우주 : world 1

[실습 | 인증 보안] sprint-auth-session 본문

텃밭 3 : BE/인증 | 보안

[실습 | 인증 보안] sprint-auth-session

parallelworlds 2022. 2. 17. 05:19

* 하이라이트 :  문제 조건

Part I - Session

 

Achievement Goals

  • 세션의 개념 이해
  • 쿠키와 세션은 서로 어떤 관계이며, 각각이 인증에 있어서 어떤 목적으로 존재하는지 이해
  • 세션의 한계 이해

 

Getting Started

쿠키를 학습하고, 브라우저에 상태를 저장할 수 있다는 사실을 알게 된 주니어 개발자 김코딩은, 쿠키에 인증 정보를 넣어 로그인 상태를 유지할 수 있음을 알게 되었습니다.

그러나, 선배 개발자 박코딩은, 쿠키만으로 인증을 구현하는 것은 언제든 쿠키가 클라이언트에 의해 변조가 가능하기 때문에 위험하다는 사실을 알려줍니다. 이에 따라 서버에도 쿠키를 검증할 수 있는 수단을 마련하는 세션 인증 방식을 제안합니다.

 

1. 환경 변수 설정

  • 시작하기에 앞서 .env.example 파일이름을 .env로 바꾼다음 데이터베이스 관련 환경변수를 설정해준다
  • 스트링 형식으로 넣는 것 잊지 말기 ! 

 


2. 데이터베이스 마이그레이션

 

  • 서버 폴더로 들어가서 시퀄라이즈 마이그레이션 및 시드를 적용하거나 직접 데이터베이스를 수정해야 한다 
  • 시퀄라이즈 마이그레이션 및 시드를 적용하는 방법은 공식 문서를 참고한다

 

Installing the CLI

To install the Sequelize CLI:

# using npm
npm install --save-dev sequelize-cli
# using yarn
yarn add sequelize-cli --dev

Running Migrations

Until this step, we haven't inserted anything into the database. We have just created the required model and migration files for our first model, User. Now to actually create that table in the database you need to run db:migrate command.

# using npm
npx sequelize-cli db:migrate
# using yarn
yarn sequelize-cli db:migrate

Running Seeds

In last step you created a seed file; however, it has not been committed to the database. To do that we run a simple command.

# using npm
npx sequelize-cli db:seed:all
# using yarn
yarn sequelize-cli db:seed:all

 

모델 파일과 시드 파일은 제공되어 있으므로 러닝 명령어를 바로 입력해준다

 

 

회원 가입 기능은 제공되지 않으며, 구현하지도 않습니다. 다만 스프린트에서 제공한 유저정보를 활용해야 합니다. servers/seeders 폴더를 확인하면 스프린트 및 테스트에서 사용하는 유저정보를 확인할 수 있습니다.

 

 


 

3. 서버 구현

 

index.js

 

  • 발급받은 https 인증서를 해당 스프린트 디렉토리에 복사합니다. 쿠키 설정은 HTTPS를 사용하는 것을 고려하여 설정하세요.

 

  1.  cert.pem, key.pem파일을 서버세션에 옮겨준다
  2.  쿠키 옵션에서 도메인은 포트 및 서브 도메인 정보, 세부 경로를 포함하지 않기 때문에 "localhost" 까지 적어준다
  3. 쿠키 옵션에서 경로는 서버가 라우팅 할 때 사용하는 경로이다. 명시하지 않은 경우 "/"으로 기본 설정된다
  4. cross-origin 요청을 받은 경우, 요청에서 사용한 메소드와 해당하는 옵션의 조합으로 서버의 쿠키 전송 여부를 결정한다.이때 이용할 수 있는 same site의 옵션은 Lax, Strict, None이다.
  5. httpOnly는 자바스크립트에서 브라우저의 쿠키 접근 여부를 결정한다. 자바스크립트에서 쿠키에 접근이 가능하게 되면 보안에 취약해진다(false옵션). 따라서 보다 안전한 true로 설정해준다
  6. secure 옵션이 true인 경우 HTTPS를 통해서만 쿠키 전송이 가능해 보다 안전하다

 

index.js

// TODO: express-session 라이브러리를 이용해 쿠키 설정을 해줄 수 있습니다.
app.use(
  session({
    secret: '@codestates',
    resave: false,
    saveUninitialized: true,
    cookie: {
      domain: "localhost",
      path: "/",
      maxAge: 24 * 6 * 60 * 10000,
      sameSite: "None",
      httpOnly: true,
      secure: true,
    },
  })
);

 

 

  • index.js에서 CORS 및 세션 옵션을 설정합니다.

 

  1. 요청이 오면 cors 요청 헤더의 origin 값을 확인하여, 별도로 관리하는 whitelist에 포함되고 있는지 체크한다. whitelist를 직접 처리할 수도 있지만, cors 미들웨어가 있기 때문에 app.use(cors({origin: "http://localhost:3000"}))처럼 작성해준다.
  2. 허용되는 method를 적어 해당 메소드만 수락하게 한다
  3. 헤더에 'Access-Contral-Allow-Credentials'를 설정해주고, credentials: true로 설정해준다 

+3) 추가 학습

CORS는 기본적으로 보안상의 이유로 쿠키를 요청으로 보낼 수 없도록 막는다. 다른 도메인을 가진 API 서버에 자신을 인증해야 정상적인 응답을 받을 수 있는 상황에서는 쿠키를 통한 인증이 필요하다

이를 위해 두 가지 작업이 필요하다

  • 요청을 credentials 모드로 설정하기
  • 서버에서 응답 헤더로 Access-Control-Allow-Crendentials: true 설정하기
// TODO: CORS 설정이 필요합니다. 클라이언트가 어떤 origin인지에 따라 달리 설정할 수 있습니다.
// 메서드는 GET, POST, OPTIONS를 허용합니다.
app.use(
  cors({
    origin: "https://localhost:3000",
    method: "GET, POST, OPTIONS",
    credentials: true
  })
  );

 

 


 

controller/login.js (POST /users/login)

  • request로부터 받은 userId, password와 일치하는 유저가 DB에 존재하는지 확인합니다.
  • 일치하는 유저가 없을 경우: 로그인 요청을 거절합니다.
  • 일치하는 유저가 있을 경우: 세션에 userId를 저장합니다.

 

1. 비동기 처리 패턴으로 동작할 수 있도록 async 와 await 문법을 적용해준다

2. 유저 정보인 userInfo 를 찾을 수 있도록 findOne 을 사용해준다

3. userInfo의 존재 여부에 따라  status와 메세지 값을 넣어준다

4. 존재할 경우 추가로 정보를 저장해준다

 

 

+2) 추가 학습

데이터 조회를 위해 finder 메서드를 사용한다. DB로부터 데이터를 쿼리하는 데 사용된다.

크게 find, findOrCreate, findAndCountAll, findAll 등이 있다.

그 중 find-단일 요소를 db에서 검색하는 메소드 예제를 살펴보자

// ID로 검색
Project.findById(123).then(project => {
  // project는 Project의 인스턴스로, ID가 123인 테이블 항목의 데이터를 담고 있다.
  // 이러한 항목이 존재하지 않는다면, null을 반환받는다.
})

// 특정 컬럼으로 검색
Project.findOne({ where: {title: 'aProject'} }).then(project => {
  // project는 Project 테이블에서 title이 'aProject'인 첫번째 항목 || 또는 null
})


Project.findOne({
  where: {title: 'aProject'},
  attributes: ['id', ['name', 'title']]
}).then(project => {
  // project는 Project 테이블에서 title이 'aProject'인 첫번째 항목 || 또는 null
  // project.title은 프로젝트의 name 값을 가진다 (SQL AS)
})

controller/login.js (POST /users/login)

module.exports = {
  post: async (req, res) => {
    // userInfo는 유저정보가 데이터베이스에 존재하고, 완벽히 일치하는 경우에만 데이터가 존재합니다.
    // 만약 userInfo가 NULL 혹은 빈 객체라면 전달받은 유저정보가 데이터베이스에 존재하는지 확인해 보세요
    const userInfo = await Users.findOne({
      where: { userId: req.body.userId, password: req.body.password },
    });
    result = userInfo;
    // TODO: userInfo 결과 존재 여부에 따라 응답을 구현하세요.
    // 결과가 존재하는 경우 세션 객체에 userId가 저장되어야 합니다.
    if (!result) {
      // your code here
      res.status(400).send({message:"not authorized"})
    } else {
      req.session.userId = userInfo.userId
      res.status(200).send({message:"ok"})
      // your code here
      // HINT: req.session을 사용하세요.
    }
  }
}

 

 

 


 

controller/logout.js (POST /users/logout)

  1. 세션 객체에 저장한 값이 존재하면, 세션을 삭제합니다. (자동으로 클라이언트 쿠키는 갱신됩니다)

 

1. 앞서 로그인시 세션 객체에 저장했던 값이 존재할 경우를 판단해준다

2. 저장한 값이 존재한다면, 이미 로그인한 상태이므로 세션을 삭제해준다 

3. 세션 삭제를 위해 express=sessoin의 destroy()함수를 사용한다.

 

controller/logout.js 

module.exports = {
  post: (req, res) => {

    // TODO: 세션 아이디를 통해 고유한 세션 객체에 접근할 수 있습니다.
    // 앞서 로그인시 세션 객체에 저장했던 값이 존재할 경우, 이미 로그인한 상태로 판단할 수 있습니다.
    // 세션 객체에 담긴 값의 존재 여부에 따라 응답을 구현하세요.

    if (!req.session.userId) {
      // your code here
      res.status(400).send({data : null, message : "not authorized"})
    } else {
      // your code here
      res.status(200).send({data : null, message : "ok"})
      // TODO: 로그아웃 요청은 세션을 삭제하는 과정을 포함해야 합니다.
      req.session.destroy();
    }
  },
};

 

 


 

controller/userinfo.js (POST /users/userinfo)

  1. 세션 객체에 저장한 값이 존재하면, 사용자 정보를 데이터베이스에서 조회한 후 응답으로 전달합니다.
  2. 세션 객체에 저장한 값이 존재하지 않으면 요청을 거절합니다.

 

1. 비동기 형식으로 수정해준다

2. 세션 객체에 담긴 정보를 확인한 후 조건으로 넣어준다

3. 객체에 담긴 정보와 로그인한 사용자의 정보가 같은지 확인해준다

 

controller/userinfo.js

onst { Users } = require('../../models');

module.exports = {
  get: async (req, res) => {

    // TODO: 세션 객체에 담긴 값의 존재 여부에 따라 응답을 구현하세요.
    // HINT: 세션 객체에 담긴 정보가 궁금하다면 req.session을 콘솔로 출력해보세요

    if (!req.session.userId) {
      // your code here
      res.status(400).send({data : null, message : "not authorized"})
    } else {
      // TODO: 데이터베이스에서 로그인한 사용자의 정보를 조회한 후 응답합니다.
      const userInfo = await Users.findOne({
        where : {userId : req.session.userId}
      }).catch((err)=> res.send(err));
      res.status(200).send({data : userInfo, message: "ok"})
    }
  },
};

 


4. 클라이언트 구현

  1. components/Mypage.js


1.props에 userData 객체를 넣어준다
2. 로그아웃 버튼이 클릭된 경우, POST `/users/logout` 요청을 보내준다
3. 로그아웃 요청이 성공한 이후, logoutHandler 함수를 호출한다

  1. components/Login.js

4) 로그인 버튼이 클릭된경우, POST `/login` 요청을 보내준다
5) 로그인에 성공한 경우, `loginHandler` 함수를 호출한다
6) 로그인 이후 GET `/users/userinfo` 요청을 통해 유저정보를 받아온다
7) 성공적으로 GET `/users/userinfo`요청이 완료된 이후, `setUserInfo` 함수가 실행한다

 

Comments