항해99 WIL(Weekly I Learned) 1주차 <미니프로젝트>편

김효진·2022년 1월 16일
0
post-thumbnail

처음으로 팀원을 만나다. |>o<|

2020/01/10 우리는 항해99를 시작해 처음으로 팀배정을 받게 되었다.
나는 떨리는 마음으로 배정표를 봤고 그 때, 내가 팀장이 되었다는 사실이 너무 놀랐고 부담스러운 마음이 몰려왔다. 내가 잘할 수 있을까라는 마음이 든 순간, 호흡을 가다듬고 다시 정신을 차렸다. "그래! 그 까짓거 해보자!"

팀원들과 프로젝트의 대해 논의하다.(컨텐츠, 역할 분담)

우선, 우리는 프로젝트의 컨셉과 역할을 정해야 했다. 논의한 결과, 우리는 MBTI유형의 사람들이 자신이 원하는 사람들과 소통할 수 있는 웹서비스를 만들기로 정했다. 그리고 만들 기능들과 역할을 담은 S.A를 만들었다.

MBTI 프로젝트 S.A 링크

웹페이지의 <로그인 페이지> 담당을 맡다.

로그인 페이지를 만드는 데는 4가지의 기능이 필요했다. 로그인 페이지, 아이디 찾기, 비밀번호 찾기, 로그아웃, (회원가입은 팀원분 한분이 맡기로 했기 때문에 생략하기로 하자.) 이렇게 4가지가 필요했는데 일단 로그인 기능이 필요한 로그인 페이지를 만들기 시작했다.

로그인 기능에 필요한 JWT 인증방법과 세션 인증방법

나는 로그인 기능에 대해 찾아본 결과 JWT 인증방법과 세션 인증방법을 찾아냈다.

  • <JWT 인증 방법> : 고객이 로그인 요청을 하면 클라이언트는 고객의 요청과 쿠키를 서버에게 보낸다. 그럼 서버는 고객의 정보가 있을 시, 요청한 데이터와 token을 클라이언트에게 보내준다. 그리고 클라이언트는 쿠키(저장소)에 데이터를 저장한다. token은 유저가 웹을 이용하면서 요청할때의 하나의 <인증 키> 역할을 하게 된다.

※ token 이란

→ Header 와 Payload 그리고 Signature로 이루어진 데이터다.

º Header: 3가지 요소를 암호화할 알고리즘 등과 같은 옵션이 들어간다.

º Payload: 유저의 고유 ID 등 인증에 필요한 정보가 들어간다.

º Verify Signature: Header, Payload와 Secret Key가 더해져 암호화된다.

★ <JWT 인증 방법의 장점과 단점>

▶ 장점

º 서버는 유저의 세션을 유지할 필요가 없다. 왜냐하면 유저가 보낸 토큰만 확인하면 되기 때문이다. 따라서 서버의 자원을 아낄 수 있는 큰 장점이 있다.

º JWT는 두 개체사이에서 안정성있게 정보를 교환할 수 있다. 그 이유는, 정보가 sign이 되어있기 때문에 정보를 보낸이가 바뀌진 않았는지 또는 정보가 도중에 조작되지는 않았는지 검증할 수 있다.(token에는 서버에만 존재하는 Secret key가 있기 때문이다.)

º JWT는 발급한 후 검증만하면 되기 때문에 추가 저장소가 필요가 없다. 그래서 서버를 확장하거나 유지, 보수하는데 유리하다.

º 토큰 기반으로 하는 다른 인증 시스템을 이용할 수 있다.(소셜 로그인 등)

▶ 단점 & 주의해야 할 점

º Self-contained : 토큰 자체에 정보를 담고 있으므로 양날의 검이 될 수 있다.

º Payload 인코딩 : 페이로드(Payload) 자체는 암호화 된 것이 아니라, BASE64로 인코딩 된 것이다. 따라서 중간에 탈취하여 디코딩을 하면 데이터를 볼 수 있으므로, JWE로 암호화하거나 Payload에 중요한 데이터를 넣지 않는 것이 핵심이다.

º Stateless: JWT는 상태를 저장하지 않기 때문에 한번 만들어지면 제어가 불가능하다. 즉 토큰(token)을 임의로 삭제하는 것이 불가능하기 때문에 서버는 안전성을 고려해 꼭 토큰의 만료 시간을 지정해 주어야만 한다.

  • <세션 인증방법>: 서버가 로그인 요청을 클라이언트에게 받아 확인하여 클라이언트에게 고유한 세션ID를 발급한다. 클라이언트는 서버에서 발급받은 세션ID를 쿠키(저장소)에 저장하게 되고 요청을 보낼 때 마다 쿠키를 보내게 된다. 서버는 쿠키에 담겨있는 세션ID와 세션 저장소에 있는 정보와 대조한 후 데이터를 클라이언트에게 준다.

★ <세션 인증 방법의 장점과 단점>

▶ 장점

º JWT인증방식과 다르게 데이터를 서버에 있는 세션저장소에 보관하기 때문에 쿠키보다는 보안이 좋다.

º

▶ 단점 & 주의사항

º 쿠키가 노출되어도 큰 문제가 없고, 요청을 주는 사용자마다 고유 세션ID가 있어 일일이 회원정보를 확인할 필요가 없다. 하지만 쿠키가 아닌 클라이언트 요청 자체를 탈취하면(하이재킹) 해커의 요청을 사용자로 인식하게 된다. (이를 방지하기 위해 세션의 유효시간을 짧게 하고, HTTPS를 사용해 보안성을 높인다).

º 세션ID를 모든 서버에서 이용할 수 있어야함으로, 중앙 세션 저장소가 없으면 시스템 확장이 어렵다. 또한 중앙 세션 저장소에 장애가 발생하면 인증 전체에 문제가 생길 수 있다.

º 세션인증은 JWT인증과 다르게 세션ID를 서버의 세션저장소에서 보관하는데 요청을 받을 때마다 내용을 저장소에 보관하기 때문에 이용자가 많아질수록 서버 메모리를 많이 차지한다.(자원 낭비가 심하다)

이것들은 내가 구글링을 하면서 찾아봤던 정보들이다 하지만 이러한 정보가 확실한지는 내가 경험이 더 쌓여봐야 알겠지만 이러한 내용을 반박하는 내용도 있다.
JWT와 세션 그리고 쿠키..

이러한 정보로 봤을 때 나는 JWT인증방식이 더 현대식이고 자주 쓰일 것 같아서 써보기로 했다.

JWT인증방식으로 로그인 기능 구현

< login.HTML 자바스크립트 구현>
서버와 클라이언트의 연결방식이 중요하므로 스크립트부분만 가져왔다.

<script>
        function enterkey() {
            if (window.event.keyCode == 13) {
                login();
            }
        }

        function login() {
            let id = $('#id').val();
            let password = $('#password').val();
            $.ajax({
                type: "POST",
                url: "/api/login",
                data: {id_give: id, pw_give: password},
                success: function (response) {
                    // 유저정보가 일치 시 토큰을 쿠키로 저장한다.
                    if (response['result'] == 'success') {
                        $.cookie("mytoken", response['token'])
                        alert('로그인 완료!');
                        window.location.href = '/';
                        // 아이디창이 공백일 때 알러트!
                    } else if (!id) {
                        alert('아이디를 입력해주세요');
                        window.location.reload();
                        // 아이디창이 이메일 형식이 아닐 때 알러트!
                    } else if (!(id.includes('@'))) {
                        alert('이메일형식으로 입력해주세요');
                        window.location.reload();
                        // 비밀번호창이 공백일 때 알러트!
                    } else if (!password) {
                        alert('비밀번호를 입력해주세요');
                        window.location.reload();
                    } else {
                        alert(response['msg']);
                        window.location.reload();
                    }
                }
            })
        }
    </script>

<app.py 로그인 서버연결 부분>

@app.route('/login')
def login():
    return render_template('login.html')


@app.route('/api/login', methods=['POST'])
def api_login():
    id_receive = request.form['id_give']
    pw_receive = request.form['pw_give']

    # hash 기능으로 pw를 암호화한다.
    pw_hash = hashlib.sha256(pw_receive.encode('utf-8')).hexdigest()

    # id, 암호화된 pw 가지고 있는 유저 찾기.
    result = db.users.find_one({'id': id_receive, 'password': pw_hash})
    # 찾으면 JWT 토큰 발급.
    if result is not None:
        payload = {
            'id': id_receive,
            'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=100)
        }

        token = jwt.encode(payload, SECRET_KEY, algorithm='HS256').decode('utf-8')

        # 만든 토큰을 준다.
        return jsonify({'result': 'success', 'token': token})
    else:
        return jsonify({'msg': '아이디 / 비밀번호가 일치하지 않습니다.'})

※ 위에서 JWT에 대해 설명한 바와 같이 유저의 정보를 입력받고 hash기능을 통해 암호화된 정보를 찾아서 정보가 유효하면 인코딩한 토큰을 클라이언트에게 준다.

로그인 구현을 끝으로 필요한 API <아이디 찾기 기능>

  • 로그인 구현이 끝났으니 이젠 로그인 기능과 연결해줄 <아이디 찾기 기능>을 만들었다.

(html 자바스크립트 부분)

<script>
        // 엔터 키를 누를 시 동작!
        function enterkey() {
            if (window.event.keyCode == 13) {
                id_check();
            }
        }

        function id_check() {
            $.ajax({
                type: "POST",
                url: "/information_check",
                data: {
                    name_give: $('#name').val(),
                    regisNum_give: $('#regisNum').val()
                },
                success: function (response) {
                    // 유저정보가 있을 시 아이디(이메일) 정보를 알러트 해준다!
                    if (response['result'] == 'success') {
                        let emails = response['email']

                        for (let i = 0; i < emails.length; i++) {
                            let name = emails[i]['name']
                            let id_email = emails[i]['id']
                            // alert(name + '님의 아이디는 ' + id_email + ' 입니다.')
                            Swal.fire({
                                icon: 'success',
                                title: '아이디찾기 완료',
                                text: name + '님의 아이디는 ' + id_email + ' 입니다.',
                                confirmButtonText: "예"
                            }).then((result) => {
                                if (result.isConfirmed)
                                    window.location.href = '/login'
                            });
                        }

                    } else {
                        alert(response['msg'])
                        window.location.href = '/login'
                    }
                }
            })
        }
    </script>

<app.py 서버 부분>

@app.route('/information_check')
def information_check():
    return render_template('information_check.html')


@app.route('/information_check', methods=['POST'])
def id_check():
    name_receive = request.form['name_give']
    regisNum_receive = request.form['regisNum_give']

    # mongodb에서 유저정보를 찾는다.
    result = db.users.find_one({'name': name_receive, 'regisNum': regisNum_receive})
    if result is not None:
        # 유저정보가 있을 시 (이름과 주민등록번호와 아이디(이메일))을 찾아서 보내준다.
        email = list(db.users.find({'name': name_receive, 'regisNum': regisNum_receive},
                                   {'password': False, '_id': False, 'MBTI': False}))
        return jsonify({'result': 'success', 'email': email})
    else:
        return jsonify({'result': 'fail', 'msg': '입력하신 정보의 아이디가 존재하지 않습니다.'})

아이디 찾기 기능은 유저의 개인정보(이름과 주민등록번호)를 받아 mongodb에 데이터가 있는지 확인하고 있을시, 유저의 아이디를 뽑아서 제공하는 기능이다.

  • 아쉬운 점 : 주민등록번호를 인증하는 시스템을 만들지 못해서 여기서 유저에게 입력받는 주민등록번호는 유저가 임의로 만든 데이터일 수 있다.

로그인과 연결해주는 API <비밀번호 찾기 기능>

(html 자바스크립트 부분)

<script>
        function enterkey() {
            if (window.event.keyCode == 13) {
                pw_check();
            }
        }

        function pw_check() {
            let name = $('#name').val()
            let regisNum = $('#regisNum').val()
            let id = $('#ID').val()
            let pw = $('#password').val()
            $.ajax({
                type: "POST",
                url: "/password_find",
                data: {
                    name_give: name,
                    regisNum_give: regisNum,
                    id_give: id,
                    pw_give: pw
                },
                success: function (response) {
                    if (response['result'] == 'success') {
                        // alert()
                        Swal.fire({
                            icon: 'success',
                            title: '변경완료',
                            text: response['msg'],
                            confirmButtonText: "예"
                        }).then((result) => {
                            if (result.isConfirmed)
                                window.location.href = '/login'
                        });

                    } else {
                        Swal.fire({
                            icon: 'success',
                            title: '변경완료',
                            text: response['msg'],
                            confirmButtonText: "예"
                        }).then((result) => {
                            if (result.isConfirmed)
                                window.location.reload()
                        });

                    }
                }
            })
        }

<app.py 서버 부분>

@app.route('/password_find', methods=['POST'])
def password_find_change():
    name_receive = request.form['name_give']
    regisNum_receive = request.form['regisNum_give']
    id_receive = request.form['id_give']
    pw_receive = request.form['pw_give']
    pw_ck_receive = request.form['pw_check_give']
    # 비밀번호와 비밀번호 확인이 같지 않을 시 에러메세지를 띄운다.
    if (pw_receive != pw_ck_receive):
        return jsonify({'result': 'fault', 'msg': '비밀번호가 같지 않습니다.'})
    result = list(db.users.find({'name': name_receive, 'regisNum': regisNum_receive, 'id': id_receive}, {'_id': False}))
    if result is not None:
        print(result)
        pw_hash = hashlib.sha256(pw_receive.encode('utf-8')).hexdigest()
        print(pw_hash)
        db.users.update_one({'name': name_receive, 'regisNum': regisNum_receive, 'id': id_receive},
                            {'$set': {'password': pw_hash}})
        return jsonify({'result': 'success', 'msg': '회원정보가 확인되어 비밀번호가 변경되었습니다.'})
    else:
        return jsonify({'result': 'fail', 'msg': '입력정보가 일치하지 않습니다.'})

유저에게 정보(이름, 주민등록번호, 아이디, 변경할 비밀번호)를 받아서 회원정보가 존재할 시 유저가 변경하고 싶은 비밀번호로 업데이트를 해주는 기능을 만들었다.

  • 아쉬웠던 점 : 사실은 비밀번호 변경 페이지를 따로 만드려고 했지만 변경 페이지에서 유저의 정보를 받아야하는데 비밀번호 찾기 페이지에서 정보를 받는 방법을 찾질 못해서 비밀번호 찾기 페이지에서 모든 것을 해결할 수 밖에 없었다.

마지막으로, 당연히 로그인이 있으면 존재해야할 <로그아웃 기능>

JWT 인증방법에서 로그아웃 기능을 만드는 것은 정말 간단하다. 유저에게 제공한 토큰(token)을 제거해주면 된다.

function switchLogout() {
    $.removeCookie('mytoken')
    alert('로그아웃!')
    window.location.href = '/login'
}

쿠키에 담겨있는 서버가 만들어준 토큰 삭제!

★ 이렇게 한주가 다 지나고 우리는 프로젝트를 완성 시켰다. 하지만 아쉬운건 항상 남길 마련이다. 우리는 주민등록인증 기능부분을 만드는데 실패를 했지만 제일 아쉬웠던 것은 채팅 기능을 실패했다는 것이 너무나도 아쉬웠다. 왜냐하면 그것이 제일 핵심이였던 기능이였기 때문이다. 하지만 아쉬움은 뒤로 한 채로 나아갈 수 밖에 없다.

<나는 꼭 발전하고 배워나아가면서 반드시 구현못한 기능을 꼭 구현할 생각이다.>

~ 다음이야기 ~
: 2 - 6주차까지는 알고리즘 주차이기 때문에 알고리즘으로 가보자!!!!

profile
어제보단 하나라도 나은 오늘이 되자!!💪

0개의 댓글