• 카테고리

    질문 & 답변
  • 세부 분야

    풀스택

  • 해결 여부

    해결됨

(회원가입 비밀번호 일치하지 않음 및 flask_wtf.csrf 오류) 보안강화 - 로그인 정보 암호화 하고 CSRF Protect 구현하기 강의중 오류..

22.04.22 23:19 작성 조회수 539

2

남박사님 안녕하세요
 
이번 보안강화 - 로그인정보 암호화 강좌 진행중에.. 오류를 해결하지 못해서 질문 드립니다.
 
우선 내용은 이렇습니다.
 
1. werkzeug.security 설정진행중 패스워드 보안작업중에 "pass": hash_password(pass1) 로 변경후에 회원가입을 하면
비밀번호가 일치하지 않습니다라고 계속 나오더라구요.. 몽고db에서 내용지워도 마찬가지 입니다. 그전에는 정상적으로 로그인이 되었습니다.
 
2. flask_wtf.csrf import CSRFProtect 를 적용하는데 pip install flask-wtf를 설치해도 오류가 나오더라구요..
 
 
 
문제 사진
 
1번
 
 
 
 
2번
 
 
 
3번 
 
 
 
 
 
4번
 
 
 
 
 
5번
 
 
 
 
 
 
 
코드
 
 
- board.py-
 
from main import *
from flask import Blueprint
 
bluerprint = Blueprint("member", __name__, url_prefix="/member")
 
# @csrf.exempt
@bluerprint.route("/join", methods=["GET", "POST"])
def member_join():
    if request.method == "POST":
        name = request.form.get("name", type=str)
        email = request.form.get("email", type=str)
        pass1 = request.form.get("pass", type=str)
        pass2 = request.form.get("pass2", type=str)
 
        if name == "" or email == "" or pass1 == "" or pass2 == "":
            flash("입력되지 않은 값이 있습니다.")
            return render_template("join.html", title="회원가입")
        if pass1 != pass2:
            flash("비밀번호가 일치하지 않습니다.")
            return render_template("join.html", title="회원가입")
 
        members = mongo.db.members
        cnt = members.count_documents({"email": email})
 
        if cnt > 0:
            flash("중복된 이메일 주소입니다.")
            return render_template("join.html", title="회원가입")
 
        current_utc_time = round(datetime.utcnow().timestamp() * 1000)
 
        post = {
            "name": name,
            "email": email,
            "pass": hash_password(pass1),
            "joindate": current_utc_time,
            "logintime": "",
            "logincount": 0,
        }
 
        members.insert_one(post)
 
        return redirect(url_for("member.member_login"))
    else:
        return render_template("join.html", title="회원가입")
 
 
@bluerprint.route("/login", methods=["GET", "POST"])
def member_login():
 
    if request.method == "POST":
        email = request.form.get("email")
        password = request.form.get("pass")
        next_url = request.form.get("next_url")
        members = mongo.db.members
        data = members.find_one({"email": email})
 
        if data is None:
            flash("회원 정보가 없습니다.")
            return redirect(url_for("member.member_login"))
        else:
            if check_password(data.get("pass"), password):
                session["email"] = email
                session["name"] = data.get("name")
                session["id"] = str(data.get("_id"))
                session.permanent = True
                if next_url is not None:
                    return redirect(next_url)
                else:
                    return redirect(url_for("board.lists"))
            else:
                flash("비밀번호가 일치하지 않습니다.")
                return redirect(url_for("member.member_login"))
        return ""
    else:
 
        next_url = request.args.get("next_url", type=str)
        if next_url is not None:
            return render_template("login.html", next_url=next_url, title="회원로그인")
        else:
            return render_template("login.html", title="회원로그인")
 
@bluerprint.route("/logout")
def member_logout():
    try:
        del session["name"]
        del session["id"]
        del session["email"]
    except:
        pass
    return redirect(url_for('member.member_login'))
 
----------------------------------------------------------------------------------------------------
 
- common.py  -
 
 
from functools import wraps
from main import session, redirect, request, url_for, ALLOWED_EXTENSIONS
from string import digits, ascii_uppercase, ascii_lowercase
import random
import re
import os
from werkzeug.security import generate_password_hash, check_password_hash
 
 
def hash_password(password):
    return generate_password_hash(password)
 
 
def check_password(hashed_password, user_password):
    return check_password_hash(hashed_password, user_password)
 
 
def check_filename(filname):
    reg = re.compile("[^A-Za-z0-9_.가-힝-]")
    for s in os.path.sep, os.path.altsep:
        if s:
            filname = filname.replace(s, ' ')
            filname = str(reg.sub('', '_'.join(filname.split()))).strip("._")
    return filname
 
 
def allowed_file(filename):
    return "." in filename and filename.rsplit(".", 1)[1] in ALLOWED_EXTENSIONS
 
 
def rand_generator(length=8):
    char = ascii_lowercase + ascii_uppercase + digits
    return "".join(random.sample(char, length))
 
 
def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if session.get("id") is None or session.get("id") == "":
            return redirect(url_for("member.member_login", next_url=request.url))
        return f(*args, **kwargs)
    return decorated_function
 
 
----------------------------------------------------------------------------------------------------
 
 
- __init__.py -
 
 
from flask import Flask
from flask import request
from flask import render_template
from flask_pymongo import PyMongo
from bson.objectid import ObjectId
from datetime import datetime, timedelta
from flask import abort
from flask import redirect
from flask import url_for
from flask import flash
from flask import session
# from flask_wtf.csrf import CSRFProtect
import math
import time
import os
 
app = Flask(__name__)
app.config["MONGO_URI"] = "mongodb://localhost:27017/myweb"
app.config["SECRET_KEY"] = "abcd"
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(minutes=30)
mongo = PyMongo(app)
 
BOARD_IMAGE_PATH = "C:\\python\\images"
BOARD_ATTACH_FILE_PATH = "C:\\python\\uploads"
 
ALLOWED_EXTENSIONS =set(["txt", "pdf", "png", "jpg", "jpeg", "gif"])
 
app.config["BOARD_IMAGE_PATH"] = BOARD_IMAGE_PATH
app.config["BOARD_ATTACH_FILE_PATH"] = BOARD_ATTACH_FILE_PATH
app.config["MAX-CONTENT_LENGTH"] = 15 * 1024 * 1024
 
if not os.path.exists(app.config["BOARD_IMAGE_PATH"]):
    os.mkdir(app.config["BOARD_IMAGE_PATH"])
 
if not os.path.exists(app.config["BOARD_ATTACH_FILE_PATH"]):
    os.mkdir(app.config["BOARD_ATTACH_FILE_PATH"])
 
from .common import login_required, allowed_file, rand_generator, check_filename, hash_password, check_password
from .filter import format_datetime
from . import board
from . import member
 
app.register_blueprint(board.bluerprint)
app.register_blueprint(member.bluerprint)
 
 
----------------------------------------------------------------------------------------------------
 
 
-member.py-
 
 
from main import *
from flask import Blueprint
 
bluerprint = Blueprint("member", __name__, url_prefix="/member")
 
# @csrf.exempt
@bluerprint.route("/join", methods=["GET", "POST"])
def member_join():
    if request.method == "POST":
        name = request.form.get("name", type=str)
        email = request.form.get("email", type=str)
        pass1 = request.form.get("pass", type=str)
        pass2 = request.form.get("pass2", type=str)
 
        if name == "" or email == "" or pass1 == "" or pass2 == "":
            flash("입력되지 않은 값이 있습니다.")
            return render_template("join.html", title="회원가입")
        if pass1 != pass2:
            flash("비밀번호가 일치하지 않습니다.")
            return render_template("join.html", title="회원가입")
 
        members = mongo.db.members
        cnt = members.count_documents({"email": email})
 
        if cnt > 0:
            flash("중복된 이메일 주소입니다.")
            return render_template("join.html", title="회원가입")
 
        current_utc_time = round(datetime.utcnow().timestamp() * 1000)
 
        post = {
            "name": name,
            "email": email,
            "pass": hash_password(pass1),
            "joindate": current_utc_time,
            "logintime": "",
            "logincount": 0,
        }
 
        members.insert_one(post)
 
        return redirect(url_for("member.member_login"))
    else:
        return render_template("join.html", title="회원가입")
 
 
@bluerprint.route("/login", methods=["GET", "POST"])
def member_login():
 
    if request.method == "POST":
        email = request.form.get("email")
        password = request.form.get("pass")
        next_url = request.form.get("next_url")
        members = mongo.db.members
        data = members.find_one({"email": email})
 
        if data is None:
            flash("회원 정보가 없습니다.")
            return redirect(url_for("member.member_login"))
        else:
            if check_password(data.get("pass"), password):
                session["email"] = email
                session["name"] = data.get("name")
                session["id"] = str(data.get("_id"))
                session.permanent = True
                if next_url is not None:
                    return redirect(next_url)
                else:
                    return redirect(url_for("board.lists"))
            else:
                flash("비밀번호가 일치하지 않습니다.")
                return redirect(url_for("member.member_login"))
        return ""
    else:
 
        next_url = request.args.get("next_url", type=str)
        if next_url is not None:
            return render_template("login.html", next_url=next_url, title="회원로그인")
        else:
            return render_template("login.html", title="회원로그인")
 
@bluerprint.route("/logout")
def member_logout():
    try:
        del session["name"]
        del session["id"]
        del session["email"]
    except:
        pass
    return redirect(url_for('member.member_login'))
 
 
----------------------------------------------------------------------------------------------------
 
 
- write.html -
 
 
{% extends "main.html" %}
 
{% block contents %}
 
<script>
$(document).ready(function() {
$("#summernote").summernote({
height: 300,
minHeight: null,
maxHeight: null,
lang: "ko-KR",
popover: {
image:[],
link:[],
air:[]
},
callbacks: {
onImageUpload: function(image) {
for(var i = 0; i < image.length; i++) {
uploadImage(image[i]);
}
}
}
});
});
 
function uploadImage(image) {
var data = new FormData();
data.append("image", image);
var csrf_token = "{{csrf_token()}}";
 
$.ajaxSetup({
beforeSend: function(x, s) {
if(!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(s.type)) {
x.setRequestHeader("X-CSRFToken", csrf_token)
}
}
});
 
$.ajax({
            url: "{{url_for('board.upload_image')}}",
            cache: false,
            contentType: false,
            processData: false, 
            data: data,
            type:"post",
            success: function(url) {
                var image = $("<img>").attr("src",url).css('max-width', "900px");
                $("#summernote").summernote("insertNode",image[0]);
            },
            error: function(data) {
                console.log(data);
                alert(data);
            }
        });
    }
</script>
 
<script>
function checkForm() {
if ($.trim($("#title").val()) == "") {
alert("제목을 입력하세요");
$("#title").focus();
return false;
}
if ($.trim($("#summernote").val()) == "") {
alert("내용을 입력하세요");
$("#summernote").focus();
return false;
}
}
</script>
 
<div style="padding: 50px 50px 50px 50px;">
<form name="form" method="post" action="{{url_for('board.board_write')}}" onsubmit="return checkForm()"enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
<div class="form-group">
<label for="name">작성자</label>
<input class="form-control" id="name" type="text" name="name" value="{{session['name']}}" readonly>
</div>
<div class="form-group">
<label for="title">제목</label>
<input class="form-control" type="text" name="title" id="title" placeholder="제목을 입력하세요"/>
</div>
<div class="form-group">
<label for="contents">내용</label>
<textarea rows="8" class="form-control" name="contents" id="summernote" placeholder="내용을 입력하세요"></textarea>
</div>
<div class="custom-file">
<input class="custom-file-input" id="customFile" type="file" name="attachfile">
<label class="custom-file-label" for="customFile">파일선택</label>
</div>
 
<div class="text-center"><input class="btn btn-primary" type="submit" value="작성하기"></div>
</form>
</div>
 
{% endblock %}
 
 
 
----------------------------------------------------------------------------------------------------
 
 
- edit.html -
 
 
 
 
{% extends "main.html" %}
 
{% block contents %}
 
<script>
$(document).ready(function() {
$("#summernote").summernote({
height: 300,
minHeight: null,
maxHeight: null,
lang: "ko-KR",
popover: {
image:[],
link:[],
air:[]
},
callbacks: {
onImageUpload: function(image) {
for(var i = 0; i < image.length; i++) {
uploadImage(image[i]);
}
}
}
});
});
 
function uploadImage(image) {
var data = new FormData();
data.append("image", image);
        var csrf_token = "{{csrf_token()}}";
 
        $.ajaxSetup({
beforeSend: function(x, s) {
if(!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(s.type)) {
x.setRequestHeader("X-CSRFToken", csrf_token)
}
}
});
 
$.ajax({
            url: "{{url_for('board.upload_image')}}",
            cache: false,
            contentType: false,
            processData: false, 
            data: data,
            type:"post",
            success: function(url) {
                var image = $("<img>").attr("src",url).css('max-width', "900px");
                $("#summernote").summernote("insertNode",image[0]);
            },
            error: function(data) {
                console.log(data);
                alert(data);
            }
        });
    }
</script>
 
<div style="padding: 50px 50px 50px 50px;">
    <form name="form" method="post" action="{{url_for('board.board_edit', idx=data._id)}}" enctype="multipart/form-data">
        <input type="hidden" name="csrf_token" value="{{csrf_token()}}">
        <div class="form-group">
            <label for="name">작성자</label>
            <input class="form-control" type="text" name="name" value="{{session['name']}}" readonly />
        </div>
        <div class="form-group">
            <label for="title">제목</label>
            <input class="form-control" type="text" name="title" value={{data.title}}>
        </div>
        {% if data.attachfile %}
        <div class="form-check text-right">
            <input type="checkbox" class="form-check-input" id="deleteoldfile" name="deleteoldfile">
            <label class="form-check-label" for="deleteoldfile">첨부파일 삭제 ({{data.attachfile}})</label>
        </div>
        {% endif %}
        <div class="form-group">
            <label for="contents">내용</label>
            <textarea rows="8" class="form-control" name="contents" id="summernote">{{data.contents}}</textarea>
        </div>
        <div class="custom-file">
<input class="custom-file-input" id="customFile" type="file" name="attachfile">
<label class="custom-file-label" for="customFile">파일선택</label>
</div>
        <div class="text-center"><input class="btn btn-primary" type="submit" value="수정하기"/></div>
    </form>
</div>
 
{% endblock %}
 
 
----------------------------------------------------------------------------------------------------
 
 
- join.html - 
 
 
 
{% extends "main.html" %}
 
{% block contents %}
 
<script>
function checkForm() {
if ($.trim($("#name").val()) == "") {
alert("이름을 입력하세요");
$("#name").focus();
return false;
}
if ($.trim($("#email").val()) == "") {
alert("이메일을 입력하세요");
$("#email").focus();
return false;
}
if (!validateEmail($.trim($("#email").val()))) {
alert("이메일 유효성이 올바르지 않습니다.");
$("#email").focus();
return false;
}
if ($.trim($("#pass1").val()) == "") {
alert("비밀번호를 입력하세요");
$("#pass1").focus();
return false;
}
if ($.trim($("#pass2").val()) == "") {
alert("비밀번호를 입력하세요");
$("#pass2").focus();
return false;
}
if($.trim($("#pass1").val()) != $.trim($("#pass2").val())) {
alert("비밀번호가 일치하지 않습니다.");
$("#pass2").select().focus();
return false;
}
}
</script>
 
<div style="padding: 50px 50px 50px 50px;">
<form name="form" action="{{url_for('member.member_join')}}" method="POST" onsubmit="return checkForm()">
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
<div class="form-group">
<label for="name">이름</label>
<input class="form-control" type="text" name="name" id="name">
</div>
<div class="form-group">
<label for="name">이메일</label>
<input class="form-control" type="text" name="email" id="email">
</div>
<div class="form-group">
<label for="name">비밀번호</label>
<input class="form-control" type="password" name="pass1" id="pass1">
</div>
<div class="form-group">
<label for="name">비밀번호 확인</label>
<input class="form-control" type="password" name="pass2" id="pass2">
</div>
<div class="text-center"><input class="btn btn-primary" type="submit" value="가입하기" /></div>
</form>
</div>
 
{% endblock %}
 
 
----------------------------------------------------------------------------------------------------
 
- login.html -
 
 
 
{% extends "main.html" %}
 
{% block contents %}
 
<script>
 
function checkForm() {
if ($.trim($("#email").val()) == "") {
alert("이메일을 입력하세요");
$("#email").focus();
return false;
}
 
if (!validateEmail($.trim($("#email").val()))) {
alert("이메일 유효성이 올바르지 않습니다.");
$("#email").focus();
return false;
}
 
if ($.trim($("#pass").val()) == "") {
alert("비밀번호를 입력하세요");
$("#pass").focus();
return false;
}
return true;
}
</script>
 
<div style="padding: 50px 50px 50px 50px;">
<form name="form" action="{{url_for('member.member_login')}}" method="POST" onsubmit="return checkForm()">
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
{% if next_url %}
<input type="hidden" name="next_url" value="{{next_url}}" />
{% endif %}
<div class="form-group">
<label for="email">이메일</label>
<input class="form-control" type="text" name="email" id="email">
</div>
<div class="form-group">
<label for="pass">비밀번호</label>
<input class="form-control" type="password" name="pass" id="pass">
</div>
<div class="text-center"><input type="submit" class="btn btn-primary" value="로그인"></div>
</form>
</div>
 
{% endblock %}
 
 
----------------------------------------------------------------------------------------------------
 
- view.html -
 
 
{% extends "main.html" %}
 
{% block contents %}
 
<div style="padding: 50px 50px 50px 50px;">
<table class="table table-bordered">
<tbody>
<tr>
<td colspan="2">{{result.title}}</td>
</tr>
<tr>
<td>{{result.name}}</td>
<td class="text-right">{{result.pubdate|formatdatetime}}</td>
</tr>
{% if result.attachfile %}
<tr>
<td>첨부파일</td>
<td><a href="{{url_for('board.board_files', filename=result.attachfile)}}">{{result.attachfile}}</a></td>
</tr>
{% endif %}
<tr>
<td colspan="2"><div style="min-height: 200px;"></div>{% autoescape false %}{{result.contents}}{% endautoescape %}</td>
</tr>
</tbody>
</table>
 
<a class="btn btn-primary" href="{{url_for('board.lists', page=page, search=search, keyword=keyword)}}">리스트</a>
 
{% if session["id"] == result.writer_id %}
<a class="btn btn-danger float-right ml-2" href="{{url_for('board.board_delete', idx=result.id)}}">글삭제</a>
<a class="btn btn-warning float-right" href="{{url_for('board.board_edit', idx=result.id)}}">글수정</a>
{% endif %}
</div>
 
{% endblock %}
 
 
----------------------------------------------------------------------------------------------------

 

답변 1

답변을 작성해보세요.

0

1.werkzeug.security 설정진행중 패스워드 보안작업중에 "pass": hash_password(pass1) 로 변경후에 회원가입을 하면 비밀번호가 일치하지 않습니다라고 계속 나오더라구요.. 몽고db에서 내용지워도 마찬가지 입니다. 그전에는 정상적으로 로그인이 되었습니다.

일단 이런 오류는 항상 로직의 흐름을 상상해보시면서 왜 오류가 발생하고 있는지를 역추적 해보셔야 합니다. 웹프로그래밍은 코드의 양이 점점 많아지면 어느 부분에서 오류 발생하는지를 찾기 힘들어지기 때문에 항상 이런 습관을 들이셔야 하고 웹프로그래밍에서 가장 중요한 노하우가 생기게 됩니다.

일단 올려주신 코드는 내용이 많아 제가 일일히 모두 확인은 할 수 없으나 전체적으로 봤을때 board.py 와 memeber.py 의 내용이 왜 같은지 모르겠습니다만 단순히 잘못 올려주신거라 생각하고 먼저 스샷을 봤을때 DB에 데이터 자체가 없다는 얘기는 DB까지 전달도 안되었단 얘기가 되니까 그 전에 문제가 생긴것일 테고 화면에 비밀번호가 일치하지 않는다는 팝업을 띄우는 구간은 자바스크립트에 의해 팝업되거나 아니면 서버의 flash() 에 의해 팝업되는 2가지 경우를 볼 수 있으니 그쪽 부분을 확인해봐야 합니다.

그래서 그 부분에 대해 확인을 해보니 HTML 파일에서는 비밀번호 input 의 name 이 pass1, pass2 로 선언을 하셨으면서 파이썬 코드에서는

pass1 = request.form.get("pass", type=str)
pass2 = request.form.get("pass2", type=str)

위의 코드처럼 pass 와 pass2 로 받고 있고 이 값을 비교하니 당연히 일치할수가 없는 문제 입니다.

 

2. flask_wtf.csrf import CSRFProtect 를 적용하는데 pip install flask-wtf를 설치해도 오류가 나오더라구요.. 

위 문제는 flask_wtf.csrf import CSRFProtect  을 import 만 하고 프로젝트에 적용하는 내용이 없습니다. 강좌의 내용을 놓치신것 같습니다.

app = Flask(__name__)
csrf = CSRFProtect(app)

위와 같이 app 에 적용되어야 합니다. 이 내용은 강좌 10:07초쯤 나오는 내용입니다.

문신호님의 프로필

문신호

질문자

2022.04.23

남박사님 감사합니다! 문제 해결했습니다.

패스워드의 경우 pass1 = request.form.get("pass", type=str) => pass1 = request.form.get("pass1", type=str) 수정하니까 정상적으로 동작하더라구요!

그리고 오류나는 부분은 

 flask_wtf.csrf import CSRFProtect 말씀하신대로

 

 

app = Flask(__name__)
csrf = CSRFProtect(app)

 

내용을 추가하니까 오류가 나지 않습니다!

flask_wtf.csrf import CSRFProtect 아래 빨간줄 그어지는거는

구글링 해서 pip install --upgrade flask-wtf 로 업그레이드 하니까 해결됬습니다!

 

답변 주셔서 감사합니다~ 화이팅 하겠습니다!

 

넵 화이팅 입니다~