로컬에서는 잘 되다가 AWS로 배포하고 나니 로그인이 안되는 문제가 생겼다.

 

내 로그인 기능은 쿠키를 사용하기 때문에 쿠키를 확인해봤더니 쿠키가 생성이 안되는 문제가 있었다.

 

구글링해보니 배포 후 쿠키 생성이 안된다는 글이 꽤 있었고 읽어보니 CORS, sameSite, secure 설정 등 보안 설정을 해주라고 나와있었고 다 해봤지만 소용이 없었다.

 

도메인 연결도 해보래서 도메인도 돈 주고 샀는데 역시나 안됐다.

 

결국엔 해결했지만 늘 그렇듯이 어이 없는 이유였다.

 

 

 

 

도메인 연결하기

 

참고한 곳

 

[AWS]리액트 개인 프로젝트 배포하기(nextjs,nestjs)[7]-Cookie 문제 해결 & 도메인 설정

지난번 글에서는 Cookie 문제까지 발생한 글을 적었다. 이번 글에서는 그 Cookie를 어떻게 공유하는지 설명할 예정이다. 먼저 원인은 도메인이 다르기 때문인데 도메인을 일치시켜주려면 도메인을

2ham-s.tistory.com

 

 

마찬가지로 나도 .shop을 1년에 550원주고 샀다.

 

해결은 안됐지만...

 

 

 

해결(+12.11)

 

원인 찾는 데 거의 2주나 걸렸다.

 

원인은 늘 그렇듯이 내가 작성한 코드 때문이었다...

 

cookie로 로그인을 구현하기 때문에 보안이 너무 취약하다고 생각했던 나는 쿠키를 생성하는 부분에 Secure=true 라는 코드를 작성했다.

 

그리고 로컬에서도 아무 문제 없이 돌아가길래 신경 쓰지 않았다.

 

하지만 배포 후 쿠키가 브라우저에 저장되지 않았다.

 

그리고 구글링을 2주 가까이 하고 여러 코드를 적어보다가 우연히 읽게된 포스팅에서 브라우저 헤더 읽는 법을 알게 됐다.

 

f12-애플리케이션에서 쿠키 생기는 목록만 봤지 헤더 읽는 법은 몰랐는데 f12-네트워크를 클릭하면 메인 페이지, 서브 페이지 별로 헤더를 볼 수 있고 헤더에는 Set-Cookie라고 쿠키가 설정 된 걸 볼 수 있다.

 

 

 

여기를 살펴보니까 서브페이지인 /login에는 세션이 잘 설정되어 있었다.

 

그러니까 쿠키 생성이 안되는 게 아니라 메인 페이지에 쿠키가 전달되지 않았던 것이다.

 

그러면 어떻게 하면 쿠키를 전달할 수 있을까 하던 차에 경고 문구를 발견했다.

(저 위의 사진은 해결되고 난 후의 사진이라 경고문구가 없다)

 

 

'Secure' 속성은 있지만 보안 연결을 통해 수신되지 않았으므로 Set-Cookie 헤더를 통해 쿠키를 설정하려는 시도가 차단되었습니다.

 

이게 무슨 말인지 처음엔 몰랐다가 구글링해보고 알았다.

 

Secure 속성을 true로 해놨으면서 왜 HTTPS로 통신하지 않았지?

 

라는 뜻이다.

 

내가 산 도메인은 HTTP라서 안됐던 것이다...

(HTTPS로 변경하려면 SSL 인증서를 돈 주고 사서 설치해야 한다)

 

그래서 저 속성을 지웠더니 Secure 속성이 없는데 Samesite 속성이 지정되어있다 라는 경고로 바뀌었고 Samesite 속성도 지웠더니 쿠키가 잘 전달 된다.

 

 

 

 

결론

 

보안 때문에 설정했던 코드가 나를 12일 동안 삽질하게 만들었다.

 

그래도 헤더를 읽을 수 있게 됐다는 기쁨이 있다!

 

에러를 겪으면서 하나하나 알아가는 것 같아서 좋다.

 

 

배포 준비 첫 번째 글이다.

 

배포 전에 보안 처리를 해야하는 데 나는 어차피 GIT에 모든 코드가 올라가있기 때문에 보안 상 다 뚫려있지만 실제 상업용 사이트를 배포하는 것처럼 세팅을 해놓을 것이다.

 

그 동안은 회원가입/로그인 기능 테스트를 위해 패스워드를 암호화하지 않았지만, 이제 배포를 앞두고 보안 처리를 해야 한다.

 

먼저 패스워드를 암호화해서 저장하고 로그인 시 암호화 된 문자열을 비교해 회원 가입 여부를 판단할 수 있도록 구현할 것이다. 

 

 

참고한 사이트

 

여기는 암호화의 종류와 개념을 이해하기 쉽게 설명되어 있다.

 

(NodeJS) crypto 모듈을 사용한 암호화

안녕하세요. 이번 시간에는 crypto 모듈을 사용해서 비밀번호를 암호화하는 방법에 대해 알아보겠습니다. 예전 패스포트 강좌에서는 패스포트 기능 설명에 중점을 두었기 때문에 비밀번호는 그

www.zerocho.com

 

여기는 실제로 코드에 구현할 때 참고하기 좋다.

 

Node 내장 암호화 모듈 Crypto

Node.js를 설치하게 되면 내장 모듈중 crypto라는 모듈이 있습니다. 이 모듈을 사용하여 암호화 하는 방법...

blog.naver.com

 

두 사이트 모두에서 코드를 참고했다.

 

 

 

 

패스워드 암호화해서 DB에 저장하기

 

사실 암호화하는 코드는 참고한 사이트에서 보면 생각보다 짧아서 어렵지 않구나 하고 생각했는데 salt를 어떻게 저장해야 하는 가에서 약간 헤맸다.

 

구글링을 해보면 salt는 돌릴 때마다 값이 달라지니 로그인 기능을 구현하려면(동일 값 여부 확인) 하려면 암호화 할 당시의 salt 값이 필요하고(사용자마다 고유한 salt 값 가짐) 그러려면 어딘가에는 저장해야 한다고 나와 있었다.

 

근데 저장해야 한다면 DB가 제일 먼저 생각났는데 DB에 암호화된 패스워드랑 salt 값을 같이 저장하면 암호화한 의미가 없어지지 않나? 라는 생각이 들어서 망설여졌다.

 

게다가 DB에 저장하는 경우도 찾아봤는데 그냥 암호화 개념 설명을 편하게 하기 위해 DB에 저장하는 듯한 느낌이어서 DB에 저장해도 되나 라는 생각이 들었다.

 

그런데 다시 생각해보면 패스워드를 암호화하는 이유는 DB가 해킹 당했을 때 패스워드를 알아보지 못하게 하기 위함이고 그러면 DB에 암호화된 패스워드와 salt 값을 같이 넣어도 어떤 방식으로 암호화했는 지는 DB가 아니라 코드에 나와있기 때문에 같이 저장해도 되는 거 아닐까 라는 생각이 들었다.

 

그래서 나는 일단 DB에 salt 값을 같이 저장했다.

 

var password = request.body.password;
      crypto.randomBytes(64, (err, buf) => {
        const salt = buf.toString('base64')
        crypto.pbkdf2(password, salt, 107529, 64, 'sha512', (err, key) => {
          User.create ({  //DB 생성
            id:request.body.id,
            password:(key.toString('base64')),
            salt : salt,
            name:request.body.name,
            phone:request.body.phone,
          }); 
        });
      });

 

 

 

암호화한 패스워드 값 비교하기(로그인)

 

사용자가 입력한 id 값으로 DB에서 사용자를 찾고 DB에 같이 저장된 salt 값을 가져와서 동일한 방식으로 암호화해서 패스워드 동일 유무를 확인하는 방식으로 코드를 작성했다.

 

  User.findOne({id : req.body.id},(err, users) => {  // id를 먼저 체크해서
    if(!users || err){ //id가 DB에 없으면(즉, id가 틀렸거나 회원가입이 안되어 있거나) 
      res.redirect('/login');
    }
    else {
    var cpassword = req.body.password;  // 사용자가 입력한 패스워드를 가져와서
    crypto.randomBytes(64, (err, buf) => {
      crypto.pbkdf2(cpassword, users.salt, 107529, 64, 'sha512', (err, key) => {
        cpassword = key.toString('base64')  // DB에 저장했던 동일한 방식으로 암호화 
        
        if (users.password == cpassword) {  // 저장된 패스워드와 암호화된 입력받은 패스워드 비교
          res.cookie('session', users.name, {
          maxAge : 6000000 // 쿠키 1시간 유지
        });
      res.redirect('/');
        }
      });
    });
  }
  })

 

 

 

저장된 DB

 

 

굳이 따지자면 Node.js 카테고리는 아니지만 올랑상점 패치니까 여기에 작성한다.

 

원래는 바로 로그인, 로그아웃, 회원가입 전부 뜨게 해 놓은 상태라 로그인 안 한 상태이면 로그아웃이 뜨지 않도록 하고 싶었다.

 

해결하는 데에는 4시간 정도 걸린 것 같은데 무슨 기능을 써야 할 지 헤매느라 오래 걸렸지만 구현하는데는 오래 걸리지 않았다.

 

 

 

참고한 곳

 

Computed 속성과 Watch | Vue.js

Computed 속성과 Watch Computed 속성 템플릿 내에 표현식을 넣으면 편리하지만, 간단한 연산을 위한 부분입니다. 템플릿 안에서 너무 많은 연산을 하면 코드가 비대해지고 유지보수가 어렵습니다. 중

v3.ko.vuejs.org

 

 

 

쿠키와 document.cookie

 

ko.javascript.info

 

 

 


 

 

 

Vue에서 쿠키 가져오기

 

나는 Nodejs에서 쿠키를 생성하는데 구글링해보면 Vue에서 쿠키를 생성해서 사용하는 글이 대부분이라 여기서 조금 헤맸지만 javascript에서 쿠키 사용하는 글을 보니 document.cookie 라는 명령어를 사용했고 다행히 잘 동작했다.

 

 return document.cookie

 

예를 들면 저런 식으로.

 

 

 

 

로그인 후 목록 갱신하기

 

내 코드는 아래와 같다. (이 기능 구현에 필요한 코드만 있음. 전체 코드 X)

 

frontend/components/Header.vue

<!-- component 용. component는 재사용을 위해 사용됨 -->

<template>
  <div class="head" v-if="cookie">  <!--cookie가 true일 때 즉, 쿠키가 있다면 보여줄 목록-->
    <a v-bind:href="main.url">{{main.message}}</a>
    <h3>{{username.message}} 님!</h3>
    <router-link to="/logout">LOG OUT</router-link>
  </div>
  <div class="head" v-else>  <!--쿠키가 없다면 보여줄 목록-->
    <a v-bind:href="main.url">{{main.message}}</a>
    <router-link to="/login"> {{ login.message }} </router-link>
    <router-link to="/signup"> {{ signup.message }} </router-link>
  </div>
</template>

<script>
export default {
  name: "Header",
  data () {
    return {
      username : {  /* 쿠키에 저장된 값 디코딩해서 가져오기  */
        message :  decodeURIComponent(document.cookie),
      },
  
     computed : {  /* 여기서 쿠키를 반환하면, v-if에서 쿠키 존재하면 true로 인식  */
        cookie(){
          return document.cookie
        }
      },
};


</script>

 

 

제일 먼저 쿠키가 있을 때는 이 목록, 없을 때는 else 블록에 있는 목록을 보여주기 위해서 v-if를 써야한다.

 

v-if는 true/false로 판별해서 해당하는 값을 보여주는데 나처럼 값(쿠키)의 유무를 이용해서 띄울 수 있다.

 

여기서 제일 중요한 게 computed 다.

 

참고한 사이트에 들어가서보면 설명이 잘 되어 있는데, 내가 막혔던 부분이 로그인 후 없던 쿠키가 생겼을 때 갱신이 되면서 회원명, 로그아웃 버튼이 보이도록하는 부분이었는데 이 명령어가 해결해줬다.

 

원래는 쿠키가 생겨도 목록이 갱신되지 않아서 막혀있던 차에 저 명령어를 사용하니 로그인 후에 잘 갱신된다.

 

주의할 점 : 반드시 computed는 return {}밖에 써줘야 한다. 이거 때문에 1시간이나 붙잡고 있었다.

 

 

 return 부분에 디코딩이 있는 이유는 내 코드는 쿠키에 사용자명을 저장하고 그 사용자 명을 가져와서 000이라는 사람이 로그인 되었다고 보여주고 싶었는데 디코딩을 안하면 쿠키값을 그대로 가져오기 때문에 숫자%알파벳으로 보인다.

 

그리고 사실 지금 저 코드 상태로는 session=000 님! 이라고 뜨기 때문에 String.slice()를 써서 session= 부분을 잘라야하는데 일단 잠시 미뤘다. (백엔드에 할 게 많아서)

 

 

로그인 전

 

로그인 후

 

 

토익하느라 포트폴리오 마무리를 잠시 미루다가 다시 실행시켜보니 redirect 실행이 안되면서 에러가 발생했다.

 

게다가 에러 내용이 낯선 에러가 아니라 봤었던 에러였는데 이 에러가 왜 발생했지? 라는 생각이 들었고 결론은, 해결하긴 했지만 원래 잘 동작하던 코드를 고치는 계기가 됐다.

 

고치고보니 사실 원래 이렇게 작성했어야 했던 건가? 라는 생각이 들었다.

 

반복문이 없어져서 코드가 더 빠르고 효율적으로 된건가, 잘 고친 것 같기도 하다.

 

 


 

 

에러 내용

 

[ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

 

이게 무슨 에러냐면 한 페이지에 두 가지 요청을 보낼 때 발생하는 에러로, 나는 이미 alter 구현할 때 많이 만났던 에러다.

 

로그인을 하려는데 로그인이 안되면서 에러가 발생하고 서버가 멈추는 상황이 발생했고 처음에 나는 저번에 alter 구현하려고 설정하면서 수정된 코드 때문인가 싶어서 그 쪽을 계속 살펴봤으나 아닌 것 같았다.

 

redierct 문제인가 싶어서(콘솔에 저 에러 밑에 express 에러가 떴었기 때문) redirect만 테스트를 해봤는데 redierct는 잘 됐고 따라서 express 문제는 아니었다.

 

이 때 순간적으로, alter 구현하면서 ajax를 사용하게됐고 그 때문인지는 몰라도 이 이후부터 버튼을 한번만 눌렀음에도 두 번 요청이 되는 상태를 발견했던 기억이 떠올랐다.

 

요청이 두 번 되면 같은 페이지에 두 가지 요청을 보내게 되니까 이런 에러가 뜰 수 있겠다 싶었고 프론트엔드에서 ajax를 원래 쓰던 axios로 바꿨지만 소용이 없었다.

 

 

 


 

 

해결

 

다시 백엔드 코드로 돌아와서 로그인 코드를 보고 있는데 문득 반복문 코드가 눈에 밟혔다.

 

로그인 할 때 find 안에서 forEach가 돌아가면서 일치하는 id, password를 찾는 구조인데, 이 코드를 보고 있자니 find만 써도 되지 않나? 라는 생각이 들었고 반복문을 없앴더니 해결됐다.

 

 결론적으로 반복문을 쓰면서 redirect를 여러 번 요청하게 되는 것 때문에 저 에러가 떴던 것 같은데 원래는 잘 돌아가는 코드였는데 왜 갑자기 에러가 났는 지는 모르겠지만 반복문을 덜어냈으니 코드가 더 깔끔해진 것 같아서 괜찮은 것 같다.

 

 

원래는 패치 내용을 따로 포스팅 할 계획이 없었으나 해결이 안되다보니 기록이라도 남기려 작성하게 됐다.

 

그 김에 앞으로 패치 관련 포스팅은 이 카테고리에서 하도록 하겠다.

 

 


 

 

내가 올랑상점을 구현하면서 가장 오래 붙잡고 있었고, 제일 골치거리였던 게 alert라서 패치할 때도 가장 먼저 선택했건만, 역시나 해결하지 못했다.

 

앞으로도 많은 에러와 기능 구현을 맞닥뜨리게 될텐데 끝까지 해결해보기 위해 부끄럽지만 이렇게 기록을 남기기로 한다.

 

 


 

 

 

해결했다고 착각했던 이유

 

이유가 참 부끄럽지만, 테스트를 잘못했기 때문이었다.

 

ajax로 코드를 작성해서 로그인할 때 일부러 틀린 아이디/비번을 입력했고 alert가 잘 나왔다.

 

그 당시 components 코드

 methods : {
        login() {
                    jQuery.ajax({
                        url: "http://localhost:3000/login",
                        method: "POST",
                        headers: { "Content-Type": "application/json" },
                        data: ({
                            id : this.id,
                            password : this.password }),
                        error : function () {
                            alert("이미 존재하는 아이디 입니다.");
                        } 
                    });
                }

 

그래서 아. 되는구나! 했으나 다음날 쿠키 관련 기능 때문에 로그인을 해야해서 제대로 된 아이디랑 비번을 입력했더니 그 때도 alert 창이 뜨는 것이었다.

 

alert 창이 뜨고 로그인이 되긴하나 제대로 입력했을 때는 alert 창이 뜨면 안되니까, 몇 시간 동안 구글링해서 문제를 알아냈다.

 

알고보니 엄청 흔한 ajax 데이터타입 문제였다.

 

그러니까 주고받은 데이터타입이 달라서 무조건 에러가 났던 것이다.

 

그래서 아래와 같이 코드를 고치니까 에러가 없어졌다.

 

 methods : {
        login() {
                    jQuery.ajax({
                        url: "http://localhost:3000/login",
                        method: "POST",
                        headers: { "Content-Type": "application/json" },
                        data: JSON.stringify({
                            id : this.id,
                            password : this.password }),
                        error : function () {
                            alert("이미 존재하는 아이디 입니다.");
                        } 
                    });
                }

 

근데 이제 문제는 잘못된 아이디/비번을 입력해도 성공처리 된다는 것이다.

 

 

 

강제 에러 발생시키기

 

그래서 나는 강제로 에러를 발생시켜 alert 창을 띄우려 했다.

 

1. res.status(400) 코드를 작성해봐도 아예 먹히지 않았다.

 

2. res.send로 변수를 전달해 받은 변수가 특정 값이면 alert를 띄워라 도 안됐다. 아니 그전에 send를 쓰면 무조건 서버가 다운되어버린다. render도 마찬가지.

 

3. throw new Error() 는 그냥 쓰면 서버가 다운되어버리고 try~catch로 감싸면 catch때문에 에러가 처리됐다고 보는 지 에러 로그만 출력할 뿐 뷰로 전달되진 않는듯했다.

 

4. next() 솔직히 이게 제일 가능성 있다고 생각했다. 에러가 발생하면 처리를 다음 함수에게 맡기는 방식인데, 에러는 나는데(alert 창도 뜸) 다음 함수로 안 넘어가서 실패했다. 왜 안 넘어가지는 지는 지금까지도 모르겠다. 다음 함수를 로그인 페이지 띄우는 함수로 해놨고 alert 창도 뜨고 로그인 페이지도 띄우면 괜찮을 거라고 생각했는데, 흠.

 

5. ajax가 아닌 axios로 다시 시도해보기 도 해봤으나 여전히 axios는 then이든 error에서 값이 받아지지 않았다.

 

 

+물론 success 상태에서 값을 받아 alert를 띄우는 것도 해봤는데 안됐다.

 

 

결론

 

삽질 기록은 alert만 두 번째다. 

 

첫 번째 삽질 기록과 두 번째 삽질기록에서 겹치는 건 거의 없어서 여러 시도를 해봤다는 거에 일단 의의를 두기로 했다...

 

언젠가 해결하는 그 날이 온다면 더욱 좋겠지만.