[0507] TIL 17일차

nikevapormax·2022년 5월 8일
0

TIL

목록 보기
16/116
post-custom-banner

😂 [팀플] 인스타그램 클론코딩

😭 로그인 API를 이해하기 위한 배경지식

- HTTP 특성

  • HTTP : 인터넷 상에서 데이터를 주고 받기 위한 서버/클라이언트 모델을 따르는 프로토콜
  • 클라이언트(나)가 서버에게 요청을 보내면 서버는 응답을 보내는 과정을 거쳐 데이터를 교환
    (내가 지금까지 공부했던 GET과 POST 방식을 생각하면 될 것 같다!)
  • HTTP는 비연결성무상태성이라는 특징에 의해 클라이언트의 요청에 응답하고 나서는 연결을 바로 끊어버리게 된다. 즉, 클라이언트에 대한 상태 정보 / 통신 상태에 대한 정보를 가지고 있지 않는 것이다. 이유는 즉슨 나와 같은 클라이언트가 한 두명이 아닐텐데 이걸 다 유지하고 있으면 엄청난 자원 낭비가 있기 때문에 아예 한 번 주고 다 끊어버려서 낭비를 막는 것이다.
  • 자원 낭비를 막을 수 있는 장점을 가지고 있지만, 여기서 클라이언트에게는 큰 단점이 될 수 있는 일이 벌어지게 된다. 서버가 클라이언트를 식별하지 못한다는 것이다. 해당 특성만 가지고 본다면 사용자는 페이지를 옮기게 되거나 새로고침을 누를 때마다 로그인을 시도해야 하는 것이다. 생각만해도 진짜 개짜증날거 같다.
  • 위의 단점을 보완하기 위해 만들어진 기술이 CookieSession이다.
  • 클라이언트가 어떤 웹사이트를 방문할 경우, 그 사이트가 사용하고 있는 서버를 통해 클라이언트의 브라우저에 설치되는 작은 기록 정보 파일
  • cookie는 key-value 형식의 문자열이다.
  • 작동 방식
    • 서버는 클라이언트가 로그인 요청을 보내 이에 대한 응답을 할 때 클라이언트 측에 저장하고 싶은 정보응답 헤더의 cookie에 담아 전달한다.
    • 이 다음부터는 클라이언트는 요청을 보낼 때마다 본인의 요청 헤더에 저장된 cookie를 담아 보내게 된다. 서버는 cookie에 담긴 정보를 바탕으로 클라이언트 식별이 가능하게 되는 것이다.
  • 단점
    • 보안에 굉장히 취약
      • 쿠키의 값을 그대로 클라이언트에게 보낸다.
      • 따라서 유출될 위험이 크고, 조작당할 수도 있다.
    • 용량 제한으로 인해 많은 정보를 담을 수 없다.
    • 웹 브라우저간의 cookie 지원 형태가 다르다.
    • 얼마 들어가지 않지만 이 cookie의 크기가 커지게 되면 network 부하가 심해진다.

- Cookie/Session

  • 위와 같이 cookie를 사용하면 로그인 상태를 유지할 수 있어 좋았지만, 개인 정보 유출의 위험이 컸다. 따라서 session은 개인정보가 HTTP를 통해 주고받아 지지 않도록 클라이언트의 인증 정보cookie가 아닌 서버 측에 저장하고 관리한다.
  • 작동 방식
    • 서버는 클라이언트의 로그인 요청에 대한 응답을 작성할 시, 인증정보는 서버에 저장하고 클라이언트의 식별자인 JSESSIONID를 쿠키에 담는다.
    • 응답을 받은 클라이언트가 추후에 요청을 보내게 되면 매 요청마다 JSESSIONID이 담긴 쿠키를 서버에 보내게 된다.
    • 해당 정보를 받은 서버는 JSESSIONID의 유효성을 판단해 클라이언트를 확인한다.
  • 장점
    • 사용자마다 고유한 세션 ID를 가지기 때문에, 요청이 들어올 때마다 회원정보를 확인할 필요가 없다.
    • 쿠키를 포함한 요청이 외부에 노출되더라도 세션 ID 자체는 유의미한 개인정보가 없어 cookie때보다는 안전하다.
    • 서버 쪽에서 세션 삭제가 가능하다.(특정 사용자의 접속 강제 만료 가능)
  • 단점
    • 요청이 하이재킹당해도 cookie처럼 개인정보가 노출될 위험은 줄었지만, 해커가 클라이언트인 척 위장할 수 있다는 한계가 존재한다.
    • 서버에 세션 저장소가 존재하기 때문에 요청이 많아지게 되면 서버의 부하가 심해진다.

- JWT(JSON Web Token)

  • 인증에 필요한 정보를 암호화시킨 토큰
  • 위의 Cookie/Session 방식과 유사하게 JWT를 HTTP 헤더에 실어 전달하며, 이를 바탕으로 서버가 클라이언트를 식별하게 된다.
  • JWT는 .을 구분자로 하여 세 가지의 문자열로 이루어지게 된다. (https://jwt.io/ 에서 구조 확인가능)
  • Header
    • 해당 부분에서는 alg와 typ을 정하게 된다.
      • alg : 암호화할 해싱 알고리즘
      • typ : 토큰의 타입
    • 단순히 인코딩된 값이라 제 3자에 의해 복호화 및 조작 가능
  • payload
    • 토큰에 담을 정보(주로 클라이언트이 고유 ID 값 및 유효 기간)를 가지고 있다.
    • Claim : key-value 형식으로 이루어진 한 쌍의 정보
    • 단순히 인코딩된 값이라 제 3자에 의해 복호화 및 조작 가능
  • signature
    • 인코딩된 Header와 Payload를 더하고 비밀키로 해싱해 생성됨
    • 서버에서 관리하는 비밀키가 유출되지 않는 이상 복호화 불가능 -> 위변조 여부 확인에 사용
  • 인증 과정
    • 클라이언트가 보낸 로그인 요청이 들어오면, 서버는 검증 후 클라이언트 고유 ID 등의 정보를 payload에 저장
    • 서버는 암호화할 비밀키를 사용해 JWT 발급
    • 클라이언트는 서버가 준 토큰을 저장하고, 서버에 요청을 보낼 때마다 요청 Header Authorization에 토큰을 포함시켜 서버에 전달
    • 서버는 클라이언트가 보낸 토큰의 Signature비밀키로 복호화한 후, 위변조 여부 및 유효 기간을 확인 (Signature의 역할!!)
    • 만약 유효한 토큰이라면 요청에 응하는 응답을 진행
  • 장점
    • 데이터 위변조 방지 가능
    • 인증 정보에 대한 별도의 저장소가 없음
    • 토큰에 대한 기본 정보, 전달 정보, 검증에 필요한 서명 등의 필요한 정보를 자체적으로 보유
    • 확장성 우수
    • 다른 로그인 시스템에 대한 접근 및 권한 공유 가능
    • OAuth의 경우, 소셜 계정을 사용해 다른 웹서비스 로그인 가능
    • 모바일 애플리케이션 환경에서도 작동 가능
  • 단점
    • 토큰의 길이가 길어 인증요청이 많아지게 되면 network 부하가 심해짐
    • payload 자체에는 유저의 중요 정보를 담을 수 없음(암호화하지 않기 때문)
    • 토큰이 해커에게 갈취당하면 큰일남(토큰은 유효기간이 끝나기 전까지는 계속 사용이 가능하기 때문)
    • 특정 사용자의 접속을 강제로 만료하기 어려움

- 추가 정보 및 참조 블로그

Tecoble님의 블로그


😭 로그인 API 코드 공부

  • 우리 팀 프로젝트에서 해당 부분의 구현은 경수님이 진행하였다. 작성한 코드를 보고 혼자 이해하려 했으나 쉽사리 이해가 가지 않아 캠프에서 제공해주었던 코드를 통해 공부를 진행하였다. (영상님과 슬기님과 같이 진행)

- 메인 페이지

  • html
    • 로그아웃 시 발급받은 토큰을 삭제해주고 해당 페이지에서 로그인 창으로 경로를 수정해주어 화면이 바뀌게 해주면 된다.
<!doctype html>
<html lang="en">
  <head>

    <!-- Webpage Title -->
    <title>Hello, world!</title>

    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bulma CSS 링크 -->
		<!-- 부트스트랩과 비슷한 건데, 예제라서 쓴 것 뿐입니다!!!-->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">

		<!-- JS -->
		<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.js"></script>


    <script>
        // 로그아웃 함수
      function logout(){
          // 쿠키를 삭제하기만 하면 됨
        $.removeCookie('mytoken');
        // alert 띄워줌
        alert('로그아웃!')
          // 다시 로그인 페이지로 보내기
        window.location.href='/login'
      }

    </script>

  </head>
  <body>
    <p>
      <h1 class="title">로그인하고 5초 동안만 볼 수 있는 페이지입니다.</h1>
      <h1 class="subtitle">계속 새로고침 해보세요</h1>
    </p>
    <!-- 닉네임 불러오기 API의 데이터를 보여주는 곳! -->
    <h5 class="subtitle">나의 닉네임은: {{nickname}}</h5>
    <!-- 로그아웃 함수 -->
    <button class="button is-danger" onclick="logout()">로그아웃하기</button>
  </body>
</html>
  • flask 부분
    • 메인 화면은 사용자가 로그인을 시도해 성공하게 되면 들어올 수 있는 화면이다.
    • 로그인을 시도하면서 사용자가 요청과 함께 보낸 토큰을 복호화한다.
    • 복호화해 나오게 된 아이디와 동일한 아이디가 있는지 db에서 검색한다.
    • 만약
      • 동일한 아이디가 있다면 : 사용자의 정보를 html에 세팅된데로 보여준다.
      • 토큰이 만료가 되었다면 : 로그인 시간이 만료되었다는 메세지와 함께 로그인 창으로 다시 돌려보낸다.
      • 토큰 복호화 에러가 있다면 : 로그인 정보가 없다는 메세지와 함께 로그인 창으로 다시 돌려보낸다.
    • 단순히 로그인이 되는지 되지 않는지만 생각하는 것이 아닌 다른 에러도 생각해야 된다는 것을 몸으로는 많이 겪어봐서 알았는데 머리에서는 로직을 짤때 생각조차 안했던 부분이다. 너무 익숙한 과정이라 그런 것 같다. 클론 코딩의 대상을 자주 관찰해야 할 것 같다.
@app.route('/')
def home():
    # 사용차 요청
    token_receive = request.cookies.get('token')
    # 트라이 구문
    try:
        # 토큰 복호화 - 페이로드 변수 선언
        payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
        # 페이로드 아이디로 데이터베이스 조회, 변수 선언
        user_info = db.user.find_one({"id": payload['id']})
        # 성공 : 메인화면을 보여주고 닉네임 변수를 jinja로 사용하기 위해서 선언
        return render_template('index.html', nickname=user_info["nick"])
    # 만료 에러 : 로그인 페이지로 이동, 메시지 리턴
    except jwt.ExpiredSignatureError:
        # 로그인페이지로 이동하고, 메시지를 jinja로 사용하기 위해 선언
        return redirect(url_for("login", msg="로그인 시간이 만료되었습니다."))
    # 복호화 에러 : 로그인 페이지로 이동, 메시지 리턴
    except jwt.exceptions.DecodeError:
        # 로그인페이지로 이동하고, 메시리를 jinja로 사용하기 위해 선언
        return redirect(url_for("login", msg="로그인 정보가 존재하지 않습니다."))

- signup 기능

  • js 부분
    • 사용자의 정보를 입력해 회원가입을 하는 기능이므로 HTTP method는 POST를 사용했다.
<!doctype html>
<html lang="en">
<head>
    <script>
        // 회원가입 AJAX call
        function register() {
            // 사용자 요청 값 변수 지정(아이디, 비밀번호, 닉네임)
            let id_give = $('#userid').val()
            let pw_give = $('#userpw').val()
            let nickname_give = $('#usernick').val()
            $.ajax({
                // http method 명시
                type: "POST",
                // app.route 주소
                url: "/api/register",
                // 사용자 요청 데이터 형식
                data: {'id_give': id_give, 'pw_give': pw_give, 'nickname_give': nickname_give},
                // 백엔드 데이터 리턴에 성공하면, 함수 실행(response인자)
                success: function (response) {
                    // console.log로 데이터 잘 나오는지 확인!
                    console.log(response)
                    // 결과 값이 성공이면,
                    if (response['result'] == 'success') {
                        // 받아온 메시지 값으로 alert 띄우기
                        alert(response['msg'])
                        // 화면의 위치를 로그인으로 이동
                        window.location.href = '/login'
                        // 그게 아니라면,
                    } else {
                        // 실패 메시지를 띄워주기
                        alert('회원가입에 실패하였습니다')
                    }
                }
            })
        }

    </script>
</head>
</html>
  • flask 부분
    • 로그인을 하게 될 때 대조를 할 수 있도록 이곳에서 패스워드를 먼저 암호화하여 db에 저장하게 된다.
@app.route('/api/register', methods=['POST'])
def api_register():
    # 사용자 요청
    # 변수 지정
    id_receive = request.form['id_give']
    pw_receive = request.form['pw_give']
    nickname_receive = request.form['nickname_give']
    # 암호화 메서드(import hashlib)
    pw_hash = hashlib.sha256(pw_receive.encode('utf-8')).hexdigest()
    # 데이터베이스에 아이디, 비밀번호, 닉네임 저장
    db.user.insert_one({'id': id_receive, 'pw': pw_hash, 'nick': nickname_receive})
    # 응답데이터 리턴
    return jsonify({'result': 'success', 'msg': '회원가입에 성공했습니다!'})

- login 기능

  • js 부분
    • 로그인을 하려면 나의 아이디와 패스워드를 입력해 서버에 보내 검증을 해야하기 때문에 HTTP method로 POST를 사용한다.
    • 내가 입력한 아이디와 패스워드를 서버에 넘겨주며, 아래의 과정에 따라 입력한 정보를 가진 사용자가 있는지 없는지에 대해 판단한 사용자가 있다면 jwt를 발급해준다.
<!doctype html>
<html lang="en">
    <head>
        <script>
            // def home()에서 리턴한 메시지가 있다면,
            {% if msg %}
                // 메시지를 alert로 띄워줌
                alert("{{ msg }}")
            {% endif %}


            // 사용자 요청으로 아이디, 비밀번호를 보내고 토큰을 받아오는 AJAX call
            function login() {
                $.ajax({
                    // 로그인 http method type
                    // 브라우저 노출되면 안되기 때문에 POST
                    type: "POST",
                    // app.route 주소
                    url: "/api/login",
                    // 사용자 요청 데이터 형식
                    data: {id_give: $('#userid').val(), pw_give: $('#userpw').val()},
                    // 응답에 성공하면,
                    success: function (response) {
                        // 결과 값이 성공이면,
                        if (response['result'] == 'success') {
                            // 로그인이 정상적으로 되면, 토큰을 받아옵니다.
                            // 이 토큰을 mytoken이라는 키 값으로 쿠키에 저장합니다.
                            $.cookie('mytoken', response['token']);
                            // alert로 로그인 완료 메시지 보내기
                            alert('로그인 완료!')
                            window.location.href = '/'
                        // 결과 값이 실패면,
                        } else {
                            // 로그인이 안되면 에러메시지를 띄웁니다.
                            alert(response['msg'])
                        }
                    }
                })
            }

        </script>
    </head>
</html>
  • flask 부분
    • 입력 받은 회원정보를 바탕으로 db에 일치하는 데이터가 있는지 먼저 확인한다.
    • 만약 result가 있다면, payload에 사용자의 아이디와 유효기간을 key-value 값의 형태로 입력한다. (주로 아래 코드에 있는 아이디와 유효기간을 저장하게 되며, 단순히 인코딩만 진행하기 때문에 사용자의 중요정보를 담을 수 없다는 단점이 존재)
    • jwt를 발급하기 때문에 payloadSECRET_KEY를 HS-256 알고리즘을 통해 암호화해 만든 signature를 token이라는 변수에 저장한다.
@app.route('/api/login', methods=['POST'])
def api_login():
    # 사용자 요청 값 변수 선언
    id_receive = request.form['id_give']
    pw_receive = request.form['pw_give']

    # 암호화 변수 담기
    pw_hash = hashlib.sha256(pw_receive.encode('utf-8')).hexdigest()

    # 유저 DB에서 아이디와 패스워드가 동시에 일치하는 데이터를 찾기
    # 찾는 값은 괄호 안에 하나로 묶여 있어야 함!
    result = db.user.find_one({'id': id_receive, 'pw': pw_hash})

    # 결과의 내용이 있다면,
    if result is not None:
        # jwt 페이로드 선언
        # 담고 싶은 내용을 담을 수 있음
        payload = {
            # 사용자 식별 정보
            'id': id_receive,
            # 토큰 유효 기간
            # 24시간 만료(timedelta second 활용)
            'exp': datetime.datetime.utcnow() + datetime.timedelta(seconds=60 * 60 * 24)
        }
        # 토큰발행
        # 시크릿키, HS256암호화 알고리즘을 사용해 암호화
        token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')

        # 성공 데이터 응답 : 성공 메시지와 토큰
        return jsonify({'result': 'success', 'token': token})
    # 실패 데이터 응답
    else:
        # 아이디 비밀번호가 일치하지 않습니다 메시지
        return jsonify({'result': 'fail', 'msg': '아이디/비밀번호가 일치하지 않습니다.'})

- API 확인

  • flask 부분
    • 사용자가 부여받은 토큰을 사용해 사용자의 nickname을 찾는 과정이다.
    • payload를 복호화 해 사용자의 아이디를 찾아내고, 해당 아이디를 바탕으로 사용자의 정보를 db에서 찾아낸다.
    • 만약
      • 동일한 아이디가 있다면 : 사용자의 nickname을 리턴해준다.
      • 토큰이 만료가 되었다면 : 로그인 시간이 만료되었다는 메세지를 보낸다.
      • 토큰 복호화 에러가 있다면 : 로그인 정보가 없다는 메세지를 보낸다.
@app.route('/api/nick', methods=['GET'])
def api_valid():
    # 사용자 요청
    token_receive = request.cookies.get('token')

    # try 구문
    # 에러와 예외처리에 용이
    # 에러가 발생할 것 같은 코드를 사용할 때, 에러를 정해두면 프로그램이 멈추지 않고 처리
    # 경우에 따라 if,else 사용할 수 있음
    try:
        # 페이로드 복호화
        payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
        print(payload)

        # 페이로드의 아이디에 관련된 데이터를 변수 선언
        userinfo = db.user.find_one({'id': payload['id']}, {'_id': False})

        # 성공 데이터 응답: 성공결과, 닉네임 정보
        return jsonify({'result': 'success', 'nickname': userinfo['nick']})

    # 에러 : 토큰 만료 - 실패결과, 메시지
    except jwt.ExpiredSignatureError:
        return jsonify({'result': 'fail', 'msg': '로그인 시간이 만료되었습니다.'})
    # 에러 : 토큰이 없는 경우 - 실패결과, 메시지 
    except jwt.exceptions.DecodeError:
        return jsonify({'result': 'fail', 'msg': '로그인 정보가 존재하지 않습니다.'})
profile
https://github.com/nikevapormax
post-custom-banner

0개의 댓글