인증/보안 - session 기반 인증 구현하기 실습

인증/보안 - session 기반 인증 구현하기 실습

session 기반 인증(session-based authentication) 구현 실습

학습 목표

* 세션의 개념을 이해 -> 접속 상태를 서버가 가지는(stateful) 인증 방식 * 쿠키와 세션은 서로 어떤 관계이며, 각각이 인증에 있어서 어떤 목적으로 존재하는지 이해하기 -> 쿠키는 단순히 무상태성을 보완해주는 도구일 뿐, 인증이 아니다 -> 세션은 인증 과정에 쿠키를 이용해서 인증 작업을 하고 세션을 열어준다고 보면 된다 -> 접속 상태와 권한 부여를 위해 세션 아이디를 쿠키로 전송하므로... * 세션의 한계를 이해 -> 서버에서 인증 정보를 가지므로 서버가 부담을 가짐(성능 저하 이슈) -> 분산에 불리 -> 여전히 쿠키를 사용하는 방식이므로 XSS공격에 취약점 존재

getting started

본격적인 시작에 앞서 환경 변수 설정 및 데이터 마이그레이션 작업을 선행해야 코드 작업이 가능하다

환경 변수 설정: .env 파일 생성 및 관련 환경 변수 설정하기(각 환경에 맞게 변경)

데이터 마이그레이션 작업: sequelize 를 이용하여 데이터 마이그레이션 및 시드 작업을 진행하자

https://sequelize.org/master/manual/migrations.html

서버 작업

index.js 작업

발급 받은 인증서 및 키를 해당 디렉토리로 옮겨 넣거나, 알맞는 경로로 설정해 주자

쿠키 관련 설정은 https를 사용할 것이므로 해당 관련 옵션을 설정하는 것이 필요(secure: true)

cors 설정도 필요함(cors는 어떤 origin인지에 따라 달리 설정할 수 있으나, 여기서는 단일 오리진에 대해서만 설정)

// express-session 라이브러리를 이용해 쿠키 설정을 해줄 수 있으므로 이를 이용 app.use( session({ secret: '주어진시크릿값', resave: false, saveUninitialized: true, cookie: { // 쿠키 관련 설정 domain: 'localhost', // 해당 도메인명 path: '/', // 세부 경로 maxAge: 24 * 6 * 60 * 10000, sameSite: 'none', // 교차 오리진에 대한 설정을 할 것이라 none으로 함 httpOnly: true, // 브라우저에서만 접근가능(자바스크립트 변조x) secure: true, //https 프로토콜 사용 }, })

cors 설정 작업도 해주자

// TODO: CORS 설정이 필요합니다. 클라이언트가 어떤 origin인지에 따라 달리 설정할 수 있습니다. // 메서드는 GET, POST, OPTIONS를 허용합니다. // app.use(cors()); //! 기본값으로 포함하고 있긴하나 해당 메서드만 허용하려면 다음과 같이 쓰자 // ! 추가된 부분 app.use(cors({ origin: 'https://localhost:4000', methods: ['POST', 'GET', 'OPTIONS'], credentials: true // 암호화 부분(기밀성 on) }))

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

기본적으로 만들어야 하는 로직은 다음과 같다

request로부터 받은 userId, password와 일치하는 유저가 DB에 존재하는지 확인

-> 일치하는 유저가 없을 경우:로그인 요청을 거절

-> 일치하는 유저가 있을 경우: 세션에 userId를 저장

// TODO: userInfo 결과 존재 여부에 따라 응답을 구현하세요. // 결과가 존재하는 경우 세션 객체에 userId가 저장되어야 합니다. // 위에서 userInfo 라는 변수 명으로 DB에 존재하는 User 테이블에서 해당 조건에 맞는 사용자를 가져옴(await Users.findOne()) if (!userInfo) { // 일치하는 유저가 없을 경우 res.status(401).send({message : '알맞은 에러 메시지 쓸것' }); } else { // console.log(req.body); //! res.session.save 를 이용 // save 객체 안에서 res도 처리해야 한다 req.session.save(function() { // session saved // 자세한 용법은 req.session에 대한 내용을 찾아보면 된다(인자 등) req.session.userId = userInfo.userId; // 세션에 userId를 저장 res.status(201).json({data: 응답 결과값, message : '잘 처리 되었다는 응답 메시지' }); //!주의점: res(응답)를 반드시 save 구문 안에 넣어야 한다 }) }

express-session 모듈과 관련된 설명은 공식 문서를 참고하자

https://www.npmjs.com/package/express-session

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

기본적인 로직은 세션 객체에 저장한 값이 존재하면 세션을 삭제하도록 구현 (자동으로 클라이언트 쿠키는 갱신되도록)

// TODO: 세션 아이디를 통해 고유한 세션 객체에 접근할 수 있습니다. // 앞서 로그인시 세션 객체에 저장했던 값이 존재할 경우, 이미 로그인한 상태로 판단할 수 있습니다. // 세션 객체에 담긴 값의 존재 여부에 따라 응답을 구현하세요. if (!req.session.userId) { // 세션 객체에 저장된 값이 존재하지 않는 경우 // res.send에 알맞은 메시지를 담아서 전달하면 됨 // your code here } else { // 세션 객체에 저장된 값이 존재하는 경우 // your code here // TODO: 로그아웃 요청은 세션을 삭제하는 과정을 포함해야 합니다. //! res.session.destroy를 활용 req.session.destroy(); // ! 그냥 이렇게 하면 현재 세션이 삭제됨 공식문서 참고 res.sendStatus(200) // res 부분은 디스트로이 안에 넣으나 안넣으나 상관없는듯 하다(차이점)? }

controller/userinfo.js (GET /users/userinfo) 작업

기본적인 로직은 다음과 같다

세션 객체에 저장한 값이 존재하면

사용자 정보를 데이터베이스에서 조회한 후 응답으로 전달

세션 객체에 저장한 값이 존재하지 않으면

요청을 거절함

// TODO: 세션 객체에 담긴 값의 존재 여부에 따라 응답을 구현하세요. // HINT: 세션 객체에 담긴 정보가 궁금하다면 req.session을 콘솔로 출력해보세요 // console.log(req.session) 찍어보기! if (!req.session.userId) { //세션 객체에 저장된 값이 존재하지 않는 경우 // your code here // res.send 로 요청 거절 메시지를 작성하자 } else { // 세션 객체에 저장된 값이 존재하는 경우 // TODO: 데이터베이스에서 로그인한 사용자의 정보를 조회한 후 응답합니다. //! sequelize의 findOne 메서드를 활용하여 구현 가능! const userInfo = await Users.findOne({ // DB의 Users 테이블에서 조회하여 응답 where: { userId: req.session.userId}, }).catch(err => res.json(err)); //에러이면 에러 res.status(200).json({data: userInfo, message: '잘되었다는 메시지'}) // 정상적으로 진행되면, res로 응답 }

클라이언트 작업

클라이언트 작업 구현 전에 package.json 의 scripts.start 부분을 알맞게 수정해 줘야 한다(환경 변수 경로 지정)

components/Mypage.js 작업

app.js에서 Mypage로 props를 전달하는데, logoutHandler 함수와 userData 상태(state)값을 전달해 주고 있다

// Mypage.js의 해당 함수 부분에서 코드 작업 const handleLogout = () => { // TODO: 서버에 로그아웃 요청을 보낸다음 요청이 성공하면 props.logoutHandler를 호출하여 로그인 상태를 업데이트 해야 합니다. //! axios 모듈을 이용하여 요청을 처리함 //axios.post(첫번째인자: url, 두번째인자:data, 세번째 인자: config) 인데, 자세한 용법은 찾아볼 것 axios .post('https://해당url', null, { //config 부분 'Content-type': 'application/json', withCredentials: true, //암호화 부분? }) .then((re) => props.logoutHandler()) // 성공시 props.logoutHandler를 호출하여 로그인 상태를 업데이트 .catch(e => alert(e)) };

components/Login.js 작업

마찬가지로 app.js에서 Login.js로 props를 전달하는데, loginHandler 함수와 setUserInfo 함수를 전달해 주고 있다

//Login.js의 해당 함수 부분에서 작업 loginRequestHandler() { // TODO: 로그인 요청을 보내세요. // 로그인에 성공하면 // - props로 전달받은 함수를 호출해, 로그인 상태를 변경하세요. // - GET /users/userinfo 를 통해 사용자 정보를 요청하세요 // // 사용자 정보를 받아온 후 // - props로 전달받은 함수를 호출해, 사용자 정보를 변경하세요. //! 마찬가지로 axios를 통해 처리 axios .post('https://localhost:4000/users/login',{ // 로그인 정보를 전달 //data 부분 username: this.state.name, password: this.state.password, }, { // config 부분 'Content-type': 'application/json', withCredentials: true, //암호화 부분? }) .then(res => { // 로그인에 성공하면, 로그인 상태를 변경 this.props.loginHandler(true) //app.js의 함수를 호출해서 상태를 변경 return axios.get('https://localhost:4000/users/userinfo', { // get 을 통해 사용자 정보를 요청 //config 설정 withCredentials: true, }) }) .then(res => { // 받아온 정보를 통해서 let {userId, email} = res.data.data; // 구조분해 this.props.setUserInfo({ // 구조분해를 이용해 사용자 정보 변경 userId, email }) }) .catch(err => alert(err)); // 에러있으면 에러 알람 }

from http://lesil.tistory.com/78 by ccl(A) rewrite - 2021-11-25 16:27:23