class UserLoginForm(FlaskForm):
username = StringField('사용자이름', validators=[DataRequired(), Length(min=3, max=25)])
password = PasswordField('비밀번호', validators=[DataRequired()])
@bp.route('/login/', methods=('GET', 'POST'))
def login():
form = UserLoginForm()
if request.method == 'POST' and form.validate_on_submit():
error = None
user = User.query.filter_by(username=form.username.data).first()
if not user:
error = "존재하지 않는 사용자입니다."
elif not check_password_hash(user.password, form.password.data):
error = "비밀번호가 올바르지 않습니다."
if error is None:
session.clear()
session['user_id'] = user.id
return redirect(url_for('main.index'))
flash(error)
return render_template('auth/login.html', form=form)
POST 방식 요청에는 로그인을 수행하고, GET 방식 요청에는 로그인 템플릿을 렌더링한다. 로그인 과정은 폼 입력으로 받은 username이 DB에 있는지 확인하고, 없다면 오류를 발생시킨다. 존재한다면 폼 입력으로 받은 password와 check_password_hash 함수를 사용해 DB에 있는 암호화된 비밀번호와 일치하는지 비교한다. username이 존재하고 password도 맞다면 플라스크 session에 키와 키값을 저장한다 (session['user_id'] = user.id).
세션
- 서버 측에서 관리, 쿠키 사용
- 클라이언트 구분을 위해 세션 ID를 부여하고 클라이언트 종료시까지 인증상태 유지
- 시간제한이 있어 일정 시간 미접속 시 자동으로 삭제
- 쿠키보다 보안이 좋지만 사용자가 많아질수록 서버 메모리 차지
클라이언트 서버 접속 시 세션 ID 발급, 클라이언트는 세션 ID를 쿠키를 사용해 저장. 클라이언트 서버 요청 시 세션 ID를 전달. 서버 세션 ID로 세션에 있는 클라이언트 정보 가져옴. 클라이언트 정보를 가지고 서버 요청을 처리하여 클라이언트에 응답.
ex) 로그인
쿠키
- 클라이언트 로컬에 저장되는 키와 값이 들어있는 데이터 파일
- 유효 시간을 명시할 수 있고, 브라우저가 종료되어도 인증 유지 가능
- 클라이언트 상태 정보를 로컬에 저장했다가 참조
클라이언트 요청시 서버는 쿠키를 생성해 전송, 클라이언트는 쿠키를 저장. 이후 서버에 요청할 때 쿠키를 같이 전송, 서버가 전송 받은 쿠키와 이전에 전송한 쿠키를 비교, 클라이언트를 구분.
ex) 로그인 페이지 아이디 비밀번호 저장, 팝업 오늘 더 이상 이 창을 보지 않음 체크
쿠키와 세션은 HTTP의 특징인 connectionless, stateless로 인한 클라이언트 구분 문제를 해결하기 위한 방법으로 비슷한 역할을 한다. 둘의 가장 큰 차이점은 정보 저장되는 위치이다.
{% extends "base.html" %}
{% block content %}
<div class="container my-3">
<form method="post" class="post-form">
{{ form.csrf_token }}
{% include "form_errors.html" %}
<div class="form-group">
<label for="username">사용자 이름</label>
<input type="text" class="form-control" name="username" id="username"
value="{{ form.username.data or '' }}">
</div>
<div class="form-group">
<label for="password">비밀번호</label>
<input type="password" class="form-control" name="password" id="password"
value="{{ form.password.data or '' }}">
</div>
<button type="submit" class="btn btn-primary">로그인</button>
</form>
</div>
{% endblock %}
로그인 버튼을 누르면 form 엘리먼트가 /auth/login/ URL을 통해 POST 방식으로 요청
@bp.before_app_request
def load_logged_in_user():
user_id = session.get('user_id')
if user_id is None:
g.user = None
else:
g.user = User.query.get(user_id)
@bp.before_app_request 애너테이션이 적용된 함수는 라우트 함수보다 먼저 실행된다. g는 플라스크가 제공하는 컨텍스트 변수로 request와 마찬가지로 요청->응답 과정에서 유효하다.
@bp.route('/logout/')
def logout():
session.clear()
return redirect(url_for('main.index'))
session.clear()는 세션의 모든 값을 삭제한다. 여기선 user_id가 세션에서 삭제되고, user_id를 읽을 수 없어 g.user도 None이 된다.
{% if g.user == question.user %}
<div class="my-3">
<a href="{{ url_for('question.modify', question_id=question.id) }}"
class="btn btn-sm btn-outline-secondary">수정</a>
</div>
{% endif %}
세션 유저와 글쓴이가 같은 경우에만 수정 버튼을 표시한다
@bp.route('/modify/<int:question_id>', methods=('GET', 'POST'))
@login_required
def modify(question_id):
question = Question.query.get_or_404(question_id)
if g.user != question.user:
flash('수정권한이 없습니다')
return redirect(url_for('question.detail', question_id=question_id))
if request.method == 'POST':
form = QuestionForm()
if form.validate_on_submit():
form.populate_obj(question)
question.modify_date = datetime.now() # 수정일시 저장
db.session.commit()
return redirect(url_for('question.detail', question_id=question_id))
else:
form = QuestionForm(obj=question)
return render_template('question/question_form.html', form=form)
GET 방식으로 요청시 QuestionForm(obj=question)으로 글 제목과 내용을 폼에 담아 렌더링한다. POST 방식으로 요청시 폼 검증 후 form.populate_obj(question)로 데이터를 question 객체에 적용하고 저장한다. db.session.commit()에서 session은 플라스크 세션이 아닌 SQLALCHEMY의 세션으로 트랜젝션 비슷한 것이다.
<a href="#" class="delete btn btn-sm btn-outline-secondary"
data-uri="{{ url_for('question.delete', question_id=question.id) }}">삭제</a>
<script type='text/javascript'>
$(document).ready(function(){
$(".delete").on('click', function() { // 클래스값이 delete인 엘리먼트가 눌리면?
if(confirm("정말로 삭제하시겠습니까?")) { // 확인 창이 열림
location.href = $(this).data('uri'); // data-uri 속성값으로 URL 호출
}
});
});
</script>
삭제 버튼 클릭 시 확인 창을 띄우기 위해 jQuery를 사용한다
{% block script %}
<script type='text/javascript'>
$(document).ready(function(){
$(".delete").on('click', function() { //클래스값이 delete인 엘리먼트가 눌리면
if(confirm("정말로 삭제하시겠습니까?")) { // 확인 창이 열림
location.href = $(this).data('uri'); //data-uri 속성값으로 URL 호출
}
});
});
</script>
{% endblock %}
$(document).ready 함수는 화면이 표시된 이후 자동으로 호출되는 jQuery 함수이다.
{% block content %}{% endblock %}으로 상속받은 템플릿이 블록을 구현한것 처럼,
{% block script %}{% endblock %}로 스크립트 블록을 구현할 수 있다.
@bp.route('/delete/<int:question_id>')
@login_required
def delete(question_id):
question = Question.query.get_or_404(question_id)
if g.user != question.user:
flash('삭제권한이 없습니다')
return redirect(url_for('question.detail', question_id=question_id))
db.session.delete(question)
db.session.commit()
return redirect(url_for('question._list'))
class Comment(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
user = db.relationship('User', backref=db.backref('comment_set'))
content = db.Column(db.Text(), nullable=False)
create_date = db.Column(db.DateTime(), nullable=False)
modify_date = db.Column(db.DateTime())
question_id = db.Column(db.Integer, db.ForeignKey('question.id', ondelete='CASCADE'), nullable=True)
question = db.relationship('Question', backref=db.backref('comment_set'))
answer_id = db.Column(db.Integer, db.ForeignKey('answer.id', ondelete='CASCADE'), nullable=True)
answer = db.relationship('Answer', backref=db.backref('comment_set'))
{% if question.comment_set|length > 0 %}
<div class="mt-3">
{% for comment in question.comment_set %}
<div class="comment py-2 text-muted">
<span style="white-space: pre-line;">{{ comment.content }}</span>
<span>
- {{ comment.user.username }}, {{ comment.create_date|datetime }}
{% if comment.modify_date %}
(수정:{{ comment.modify_date|datetime }})
{% endif %}
</span>
{% if g.user == comment.user %}
<a href="{{ url_for('comment.modify_question', comment_id=comment.id) }}" class="small">수정</a>,
<a href="#" class="small delete"
data-uri="{{ url_for('comment.delete_question', comment_id=comment.id) }}">삭제</a>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
<div>
<a href="{{ url_for('comment.create_question', question_id=question.id) }}"
class="small"><small>댓글 추가 ..</small></a>
</div>
div class="comment"는 별도로 구현해야할 css 클래스이다.
.comment {
border-top:dotted 1px #ddd;
font-size:0.7em;
}
class CommentForm(FlaskForm):
content = TextAreaField('내용', validators=[DataRequired()])
@bp.route('/create/question/<int:question_id>', methods=('GET', 'POST'))
@login_required
def create_question(question_id):
form = CommentForm()
question = Question.query.get_or_404(question_id)
if request.method == 'POST' and form.validate_on_submit():
comment = Comment(user=g.user, content=form.content.data, create_date=datetime.now(), question=question)
db.session.add(comment)
db.session.commit()
return redirect(url_for('question.detail', question_id=question_id))
return render_template('comment/comment_form.html', form=form)
question에 달린 댓글이기 때문에 Comment 모델 인스턴스를 생성할 때 question 필드에 값을 설정한다.
{% extends 'base.html' %}
{% block content %}
<div class="container my-3">
<h5 class="border-bottom pb-2">댓글등록하기</h5>
<form method="post" class="post-form my-3">
{{ form.csrf_token }}
{% include "form_errors.html" %}
<div class="form-group">
<label for="content">댓글내용</label>
<textarea class="form-control" name="content" id="content"
rows="3">{{ form.content.data or '' }}</textarea>
</div>
<button type="submit" class="btn btn-primary">저장하기</button>
</form>
</div>
{% endblock %}
@bp.route('/modify/question/<int:comment_id>', methods=('GET', 'POST'))
@login_required
def modify_question(comment_id):
comment = Comment.query.get_or_404(comment_id)
if g.user != comment.user:
flash('수정권한이 없습니다')
return redirect(url_for('question.detail', question_id=comment.question.id))
if request.method == 'POST':
form = CommentForm()
if form.validate_on_submit():
form.populate_obj(comment)
comment.modify_date = datetime.now() # 수정일시 저장
db.session.commit()
return redirect(url_for('question.detail', question_id=comment.question.id))
else:
form = CommentForm(obj=comment)
return render_template('comment/comment_form.html', form=form)
GET 방식으로 요청시 기존 댓글을 조회해 폼에 담아 반환하고 POST 방식으로 요청 시 폼에서 받은 내용으로 업데이트한다.
@bp.route('/delete/question/<int:comment_id>')
@login_required
def delete_question(comment_id):
comment = Comment.query.get_or_404(comment_id)
question_id = comment.question.id
if g.user != comment.user:
flash('삭제권한이 없습니다')
return redirect(url_for('question.detail', question_id=question_id))
db.session.delete(comment)
db.session.commit()
return redirect(url_for('question.detail', question_id=question_id))