app.py

from flask import Flask, render_template, jsonify, request, session, redirect, url_for

app = Flask(__name__)

from pymongo import MongoClient

client = MongoClient('mongodb://3.34.44.93', 27017, username="sparta", password="woowa")
db = client.dbsparta_plus_week4

# JWT 토큰을 만들 때 필요한 비밀문자열입니다. 아무거나 입력해도 괜찮습니다.
# 이 문자열은 서버만 알고있기 때문에, 내 서버에서만 토큰을 인코딩(=만들기)/디코딩(=풀기) 할 수 있습니다.
SECRET_KEY = 'SPARTA'

# JWT 패키지를 사용합니다. (설치해야할 패키지 이름: PyJWT ver.1.7.1)
import jwt

# 토큰에 만료시간을 줘야하기 때문에, datetime 모듈도 사용합니다.
import datetime

# 회원가입 시엔, 비밀번호를 암호화하여 DB에 저장해두는 게 좋습니다.
# 그렇지 않으면, 개발자(=나)가 회원들의 비밀번호를 볼 수 있으니까요.^^;
import hashlib

#################################
##  HTML을 주는 부분             ##
#################################
@app.route('/')
def home():
    token_receive = request.cookies.get('mytoken')

    try:
        payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
        user_info = db.user.find_one({"id": payload['id']})
        return render_template('index.html', nickname=user_info["nick"])
    except jwt.ExpiredSignatureError:
        return redirect(url_for("login", msg="로그인 시간이 만료되었습니다."))
    except jwt.exceptions.DecodeError:
        return redirect(url_for("login", msg="로그인 정보가 존재하지 않습니다."))

@app.route('/login')
def login():
    msg = request.args.get("msg")
    return render_template('login.html', msg=msg)

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

#################################
##  로그인을 위한 API            ##
#################################

# [회원가입 API]
# id, pw, nickname을 받아서, mongoDB에 저장합니다.
# 저장하기 전에, pw를 sha256 방법(=단방향 암호화. 풀어볼 수 없음)으로 암호화해서 저장합니다.
@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']

    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'})

# [로그인 API]
# id, pw를 받아서 맞춰보고, 토큰을 만들어 발급합니다.
@app.route('/api/login', methods=['POST'])
def api_login():
    id_receive = request.form['id_give']
    pw_receive = request.form['pw_give']

    # 회원가입 때와 같은 방법으로 pw를 암호화합니다.
    pw_hash = hashlib.sha256(pw_receive.encode('utf-8')).hexdigest()

    # id, 암호화된pw을 가지고 해당 유저를 찾습니다.
    result = db.user.find_one({'id': id_receive, 'pw': pw_hash})

    # 찾으면 JWT 토큰을 만들어 발급합니다.
    if result is not None:
        # JWT 토큰에는, payload와 시크릿키가 필요합니다.
        # 시크릿키가 있어야 토큰을 디코딩(=풀기) 해서 payload 값을 볼 수 있습니다.
        # 아래에선 id와 exp를 담았습니다. 즉, JWT 토큰을 풀면 유저ID 값을 알 수 있습니다.
        # exp에는 만료시간을 넣어줍니다. 만료시간이 지나면, 시크릿키로 토큰을 풀 때 만료되었다고 에러가 납니다.
        payload = {
            'id': id_receive,
            'exp': datetime.datetime.utcnow() + datetime.timedelta(seconds=5)
        }  # id 값, 로그인이 얼만큼 유지될지. 다른 사람이 로그인 값을 훔칠 수 있기 때문에 payload 도 암호화

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

        # token을 줍니다.
        return jsonify({'result': 'success', 'token': token})
    # 찾지 못하면
    else:
        return jsonify({'result': 'fail', 'msg': '아이디/비밀번호가 일치하지 않습니다.'})

# [유저 정보 확인 API]
# 로그인된 유저만 call 할 수 있는 API입니다.
# 유효한 토큰을 줘야 올바른 결과를 얻어갈 수 있습니다.
# (그렇지 않으면 남의 장바구니라든가, 정보를 누구나 볼 수 있겠죠?)
@app.route('/api/nick', methods=['GET'])
def api_valid():
    token_receive = request.cookies.get('mytoken')

    # try / catch 문?
    # try 아래를 실행했다가, 에러가 있으면 except 구분으로 가란 얘기입니다.

    try:
        # token을 시크릿키로 디코딩합니다.
        # 보실 수 있도록 payload를 print 해두었습니다. 우리가 로그인 시 넣은 그 payload와 같은 것이 나옵니다.
        payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
        print(payload)

        # payload 안에 id가 들어있습니다. 이 id로 유저정보를 찾습니다.
        # 여기에선 그 예로 닉네임을 보내주겠습니다.
        userinfo = db.user.find_one({'id': payload['id']}, {'_id': 0})
        return jsonify({'result': 'success', 'nickname': userinfo['nick']})
    except jwt.ExpiredSignatureError:
        # 위를 실행했는데 만료시간이 지났으면 에러가 납니다.
        return jsonify({'result': 'fail', 'msg': '로그인 시간이 만료되었습니다.'})
    except jwt.exceptions.DecodeError:
        return jsonify({'result': 'fail', 'msg': '로그인 정보가 존재하지 않습니다.'})

if __name__ == '__main__':
    app.run('0.0.0.0', port=5000, debug=True)

index.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/[email protected]/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('로그아웃!')
        window.location.href='/login'
      }

    </script>

  </head>
  <body>
    <p>
      <h1 class="title">로그인하고 5초 동안만 볼 수 있는 페이지입니다.</h1>
      <h1 class="subtitle">계속 새로고침 해보세요</h1>
    </p>
    <h5 class="subtitle">나의 닉네임은: {{nickname}}</h5>
    <button class="button is-danger" onclick="logout()">로그아웃하기</button>
  </body>
</html>

login.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/[email protected]/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>
            {% if msg %}
                alert("{{ msg }}")
            {% endif %}
            // ['쿠키'라는 개념에 대해 알아봅시다]
            // 로그인을 구현하면, 반드시 쿠키라는 개념을 사용합니다.
            // 페이지에 관계없이 브라우저에 임시로 저장되는 정보입니다. 키:밸류 형태(딕셔너리 형태)로 저장됩니다.
            // 쿠키가 있기 때문에, 한번 로그인하면 네이버에서 다시 로그인할 필요가 없는 것입니다.
            // 브라우저를 닫으면 자동 삭제되게 하거나, 일정 시간이 지나면 삭제되게 할 수 있습니다.
            function login() {
                $.ajax({
                    type: "POST",
                    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('로그인 완료!')
                            window.location.href = '/'
                        } else {
                            // 로그인이 안되면 에러메시지를 띄웁니다.
                            alert(response['msg'])
                        }
                    }
                })
            }

        </script>

    </head>
    <body>
        <div class="section has-text-centered">
            <h1 class="title">로그인 페이지</h1>
            <div class="container" style="width:60%">
                <div class="field is-horizontal">
                    <div class="field-label is-normal">
                        <label class="label" for="userid">ID</label>
                    </div>
                    <div class="field-body">
                        <div class="field">
                            <div class="control">
                                <input type="text" class="input" id="userid" aria-describedby="emailHelp"
                                       placeholder="My ID">
                            </div>
                        </div>
                    </div>
                </div>
                <div class="field is-horizontal">
                    <div class="field-label is-normal">
                        <label class="label" for="userpw">PW</label>
                    </div>
                    <div class="field-body">
                        <div class="field">
                            <div class="control">
                                <input type="password" class="input" id="userpw" placeholder="My Password">
                            </div>
                        </div>
                    </div>
                </div>
                <button class="button is-primary" onclick="login()">로그인</button>
            </div>
        </div>
    </body>
</html>

register.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/[email protected]/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>
        // 간단한 회원가입 함수입니다.
        // 아이디, 비밀번호, 닉네임을 받아 DB에 저장합니다.
        function register() {
            $.ajax({
                type: "POST",
                url: "/api/register",
                data: {
                    id_give: $('#userid').val(),
                    pw_give: $('#userpw').val(),
                    nickname_give: $('#usernick').val()
                },
                success: function (response) {
                    if (response['result'] == 'success') {
                        alert('회원가입이 완료되었습니다.')
                        window.location.href = '/login'
                    } else {
                        alert(response['msg'])
                    }
                }
            })
        }

    </script>
</head>
<body>
<div class="section has-text-centered">
    <h1 class="title">회원가입 페이지</h1>
    <div class="container" style="width:60%">
        <div class="field is-horizontal">
            <div class="field-label is-normal">
                <label class="label" for="userid">ID</label>
            </div>
            <div class="field-body">
                <div class="field">
                    <div class="control">
                        <input type="text" class="input" id="userid" aria-describedby="emailHelp"
                               placeholder="My ID">
                    </div>
                </div>
            </div>
        </div>
        <div class="field is-horizontal">
            <div class="field-label is-normal">
                <label class="label" for="userpw">PW</label>
            </div>
            <div class="field-body">
                <div class="field">
                    <div class="control">
                        <input type="password" class="input" id="userpw" placeholder="My Password">
                    </div>
                </div>
            </div>
        </div>
        <div class="field is-horizontal">
            <div class="field-label is-normal">
                <label class="label" for="usernick">NICKNAME</label>
            </div>
            <div class="field-body">
                <div class="field">
                    <div class="control">
                        <input type="text" class="input" id="usernick" placeholder="My Nickname">
                    </div>
                </div>
            </div>
        </div>
        <button class="button is-primary" onclick="register()">회원가입</button>
    </div>
</div>
</body>
</html>