0. 프로젝트 셋팅
- 보안, 로그인, 암호화 기능의 필요성 : 주소만 알면 누구나 들어와 내용을 추가/수정/삭제가 가능합니다. 이를 방지하기위해 비밀번호를 입력해야만 내용을 볼 수 있는 식으로 보안을 강화할 수 있습니다. 여러 사람들이 한 서비스를 이용해야 하며 공개된 내용이 달라져야 한다면 로그인 기능이 필요합니다.
- 패키지 설치 : flask, pymongo, pyJWT
1. Bulma 로 웹사이트를 꾸미기
Bulma는 미리 정해진 클래스를 가져다 쓰는 무료 CSS 프레임 워크입니다. Bulma는 Bootstrap과 달리 순수 CSS 프레임워크이기 때문에 기능을 직접 구현해야 하지만 더 자유롭게 커스터마이징이 됩니다. 또한 커서, 테마, 플러그인 등 많은 부분이 개발되어 있고 질문과 답, 예시를 찾기 쉽습니다. 게다가 Bulma는 문법이 직관적이고 Flexbox 등의 최신 기술을 많이 사용합니다.
Bulma 공식문서에는 각 컴포넌트의 묘사와 예시가 잘 정리되어 있습니다.
https://bulma.io/documentation/
Bulma: Free, open source, and modern CSS framework based on Flexbox
Bulma is a free, open source CSS framework based on Flexbox and built with Sass. It's 100% responsive, fully modular, and available for free.
bulma.io
Bulma의 모든 CSS는 헤드 태그 안에 아래 내용을 포함시키면 됩니다.
<!-- Bulma CSS 링크 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
Bulma는 태그명과 같은 클래스 명을 사용해 직관적입니다. 각 기능은 모듈화 해 적용하고 싶은 CSS를 얹어 사용하는 방식을 추구합니다.
Hero 배너 : 레이아웃에서 화면 전체 너비를 채우는 배너 클래스입니다. hero-body 안에 내용을 넣을 수 있습니다.
Section : 구역을 나누어줄 때 사용합니다.
Box, Media : box는 테두리와 그림자 등을 이용해 만든 카드이며 media는 스위터에서 많이 쓰이는 이미지와 글의 조합입니다.
2. 회원가입 기능 구현
해시함수란? : 해시함수는 알고리즘의 한 종류로서 임의의 데이터를 입력 받아 항상 고정된 길이의 임의의 값으로 변환해주는 함수를 말합니다. SHA256은 어떤 길이의 입력값을 넣어도 항상 256바이트의 결과값이 나옵니다. 추가적으로 동일한 특징 : 입력값은 항상 같은 결과값이 나오고 입력값이 달라지면 완전히 다른 값이 나옵니다. 그리고 결과값을 통해 입력값을 알아내는 것이 불가능 하다는 특징을 가지고 있습니다.
Flask에서 회원가입, 로그인기능을 구현해 보도록 하겠습니다. 회원가입 API는 유저가 입력한 아이디와 패스워드를 서버에 전달받아 db에 저장해야 합니다. 입력을 받는 과정에서 패스워드를 해시함수를 통해 암호화를 하는 과정을 거칩니다. 물론 아이디와 패스워드가 이미 기존 디비에 존재하는 경우 회원가입 기능을 반려해야 하지만 차후에 해당 기능을 추가하도록 하겠습니다.
# [회원가입 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'})
로그인 기능을 구현해보도록 하겠습니다. 여기에는 JWT를 활용해 로그인 기능을 구현합니다. JWT란 JSON Web Token 의 줄임말로서, JSON 객체를 사용해 정보를 안정성 있게 전달하는 웹 표준입니다. 로그인 기능에서는 사용자가 로그인하면 서버에서 회원임을 인증하는 토큰을 넘겨줌으로써 이후 회원만 접근할 수 있는 서비스 영역에서 신분을 확인하는데 ㅆ일 수 있습니다. (https://tansfil.tistory.com/58?category=255594)
# [로그인 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)
}
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
# token을 줍니다.
return jsonify({'result': 'success', 'token': token})
# 찾지 못하면
else:
return jsonify({'result': 'fail', 'msg': '아이디/비밀번호가 일치하지 않습니다.'})
위의 코드는 로그인 기능을 플라스크 서버에서 구현을 했습니다. 사용자로부터 아이디와 패스워드 값을 서버로 전달 받습니다. 이 때 패스워드는 디비에 암호화된 값으로 저장되어 있기 때문에 해당 해쉬 함수로 암호화된 값을 바탕으로 유저를 찾습니다. 만약 해당 유저를 찾았다면 JWT 토큰을 클라이언트에 반환합니다. JWT 토큰에는 아이디와 만료 시간을 담고 있습니다. Payload에 유저의 아이디와 만료 시간을 담은 후 인코딩을 해 유저에 전달됩니다. 만약 payload가 암호화 되지 않은 채로 클라이언트에 전달이 된다면 로그인 토큰이 남용될 수 있습니다. 따라서 서버만 가지고 있는 SECRET_KEY를 바탕으로 암호화를 진행합니다.
로그인한 이후 반환하는 index.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="로그인 정보가 존재하지 않습니다."))
홈 화면을 보여주는 코드에서는 로그인 토큰을 확인합니다. request.cookies.get('mytoken')은 이전에 서버에서 발급해준 토큰을 가져옵니다. 토큰을 가져온 후 jwt.decode() 함수를 통화 복호화를 진행합니다. payload를 꺼내게 되면 이전에 토큰을 발급했을 때 넣어 주었던 아이디와 만료시간 정보를 얻을 수 있습니다. 만약 해당 아이디가 디비에 존재한다면 닉네임과 함께 렌더링을 진행합니다.
3. 쿠키란?
쿠키는 브라우저에 저장된 정보입니다. 브라우저는 딕셔너리 형태로 특정한 정보를 저장합니다. 페이지에 관계없이 브라우저는 계속 저장하고 있어 서버와 통신할 때 항상 쿠키값을 함께 전달합니다. 서버는 해당 쿠키 값을 확인하고 토큰을 확인해 특정 동작을 실행시킵니다.
쿠키는 브라우저에 저장된 정보이기 때문에 크롬에서 로그인 활동을 하더라도 IE를 쓰거나 모바일로 접속하는 경우 재로그인해야 합니다.
jquery를 활용해 쿠키 값을 저장할 수 있습니다.
$.cookie('mytoken', response['token']);
로그 아웃 기능은 간단하게 쿠키를 삭제함으로서 구현할 수 있습니다.
4. 기능 명세 :
로그인, 회원가입 페이지 :
기본 화면을 로그인 화면으로 보이기
회원가입하기 버튼을 클릭 시 외원가입 화면으로 바뀌기
취소 버튼을 클릭하면 로그인 화면으로 돌아오기
회원가입 :
아이디 비밀번호 형식 확인
아이디 중복확인
DB에 아이디, 비밀번호 저장 하여 회원가입/로그인 화면으로 전환
로그인 :
아이디 비밀번호 입력 확인
서버로 POST 요청을 보내 가입 정보 확인
회원인 경우 토큰 부여
메인 페이지 :
모든 사람의 포스트를 시간 역순으로 보여주기
각 포스트에 좋아요, 좋아요 취소 기능 -> 좋아요한 포스트는 찬 하트로 보여주기
포스팅 칸에 내 프로필 사진 보여주기 -> 프로필 사진을 누르면 프로필 페이지로 이동
포스팅 칸을 클릭하면 포스팅 모달 띄우기 -> 포스팅하기 버튼을 클릭하면 DB 저장, 새로고침해 다시 띄우기
프로필 페이지 :
사용자의 포스트를 시간 역순으로 보여주기
내 프로필인 경우 프로필 수정, 로그아웃 버튼 보이기
내 프로필인 경우 포스팅 칸 보이기
프로필 수정 버튼을 클릭 시 프로필 수정 모달 보이기 -> 기존 값 보이기, 수정 시 db업데이트
로그아웃 버튼 클릭 시 토큰 삭제, 로그인 페이지로 이동
5. 회원가입 기능 구현하기
회원 가입 기능은 다음 세가지를 구현해야 합니다.
아이디, 비밀번호 형식 확인
아이디, 비밀번호 중복확인
문제가 없는 경우 DB에 저장하기
형식 확인은 정규식을 통해 필터링합니다.
function is_nickname(asValue) {
var regExp = /^(?=.*[a-zA-Z])[-a-zA-Z0-9_.]{2,10}$/;
return regExp.test(asValue);
}
function is_password(asValue) {
var regExp = /^(?=.*\d)(?=.*[a-zA-Z])[0-9a-zA-Z!@#$%^&*]{8,20}$/;
return regExp.test(asValue);
}
아이디 중복 여부를 확인하기위해 서버에 중복여부확인 API 요청을 합니다.
function check_dup() {
let username = $("#input-username").val()
console.log(username)
if (username == "") {
$("#help-id").text("아이디를 입력해주세요.").removeClass("is-safe").addClass("is-danger")
$("#input-username").focus()
return;
}
if (!is_nickname(username)) {
$("#help-id").text("아이디의 형식을 확인해주세요. 영문과 숫자, 일부 특수문자(._-) 사용 가능. 2-10자 길이").removeClass("is-safe").addClass("is-danger")
$("#input-username").focus()
return;
}
$("#help-id").addClass("is-loading")
$.ajax({
type: "POST",
url: "/sign_up/check_dup",
data: {
username_give: username
},
success: function (response) {
if (response["exists"]) {
$("#help-id").text("이미 존재하는 아이디입니다.").removeClass("is-safe").addClass("is-danger")
$("#input-username").focus()
} else {
$("#help-id").text("사용할 수 있는 아이디입니다.").removeClass("is-danger").addClass("is-success")
}
$("#help-id").removeClass("is-loading")
}
});
}
이 때, 아이디의 중복 검사 여부를 확인하기위해 .removeClass("is-danger")와 .addClass("is-safe")를 활용하여 조건의 통과 여부를 가상으로 적용합니다. 해당 중복검사 여부는 .hasClass("is-safe")를 통해 확인이 가능합니다.
마지막으로 회원가입 요청 API를 구현합니다.
function sign_up() {
let username = $("#input-username").val()
let password = $("#input-password").val()
let password2 = $("#input-password2").val()
console.log(username, password, password2)
if ($("#help-id").hasClass("is-danger")) {
alert("아이디를 다시 확인해주세요.")
return;
} else if (!$("#help-id").hasClass("is-success")) {
alert("아이디 중복확인을 해주세요.")
return;
}
if (password == "") {
$("#help-password").text("비밀번호를 입력해주세요.").removeClass("is-safe").addClass("is-danger")
$("#input-password").focus()
return;
} else if (!is_password(password)) {
$("#help-password").text("비밀번호의 형식을 확인해주세요. 영문과 숫자 필수 포함, 특수문자(!@#$%^&*) 사용가능 8-20자").removeClass("is-safe").addClass("is-danger")
$("#input-password").focus()
return
} else {
$("#help-password").text("사용할 수 있는 비밀번호입니다.").removeClass("is-danger").addClass("is-success")
}
if (password2 == "") {
$("#help-password2").text("비밀번호를 입력해주세요.").removeClass("is-safe").addClass("is-danger")
$("#input-password2").focus()
return;
} else if (password2 != password) {
$("#help-password2").text("비밀번호가 일치하지 않습니다.").removeClass("is-safe").addClass("is-danger")
$("#input-password2").focus()
return;
} else {
$("#help-password2").text("비밀번호가 일치합니다.").removeClass("is-danger").addClass("is-success")
}
$.ajax({
type: "POST",
url: "/sign_up/save",
data: {
username_give: username,
password_give: password
},
success: function (response) {
alert("회원가입을 축하드립니다!")
window.location.replace("/login")
}
});
}
서버에서는 아이디의 중복 여부 확인과 회원가입을 구현합니다.
@app.route('/sign_up/save', methods=['POST'])
def sign_up():
username_receive = request.form['username_give']
password_receive = request.form['password_give']
password_hash = hashlib.sha256(password_receive.encode('utf-8')).hexdigest()
doc = {
"username": username_receive, # 아이디
"password": password_hash, # 비밀번호
"profile_name": username_receive, # 프로필 이름 기본값은 아이디
"profile_pic": "", # 프로필 사진 파일 이름
"profile_pic_real": "profile_pics/profile_placeholder.png", # 프로필 사진 기본 이미지
"profile_info": "" # 프로필 한 마디
}
db.users.insert_one(doc)
return jsonify({'result': 'success'})
@app.route('/sign_up/check_dup', methods=['POST'])
def check_dup():
username_receive = request.form['username_give']
exists = bool(db.users.find_one({"username": username_receive}))
return jsonify({'result': 'success', 'exists': exists})
6. 로그인 기능 구현하기
회원가입기능과 달리 로그인 기능은 매우 간단하게 구현이 됩니다. 사용자가 입력한 아이디와 패스워드를 전달받아 서버에서 매칭되는 유저를 확인하고 유저가 존재하는 경우 로그인 토큰을 발행합니다. 클라이언트에서는 해당 토큰을 쿠키에 저장하면 됩니다.
function sign_in() {
let username = $("#input-username").val()
let password = $("#input-password").val()
if (username == "") {
$("#help-id-login").text("아이디를 입력해주세요.")
$("#input-username").focus()
return;
} else {
$("#help-id-login").text("")
}
if (password == "") {
$("#help-password-login").text("비밀번호를 입력해주세요.")
$("#input-password").focus()
return;
} else {
$("#help-password-login").text("")
}
$.ajax({
type: "POST",
url: "/sign_in",
data: {
username_give: username,
password_give: password
},
success: function (response) {
if (response['result'] == 'success') {
$.cookie('mytoken', response['token'], {path: '/'});
window.location.replace("/")
} else {
alert(response['msg'])
}
}
});
}
import datetime
import hashlib
import jwt
@app.route('/sign_in', methods=['POST'])
def sign_in():
# 로그인
username_receive = request.form['username_give']
password_receive = request.form['password_give']
pw_hash = hashlib.sha256(password_receive.encode('utf-8')).hexdigest()
result = db.users.find_one({'username': username_receive, 'password': pw_hash})
if result is not None:
payload = {
'id': username_receive,
'exp': datetime.datetime.utcnow() + datetime.timedelta(seconds=60 * 60 * 24) # 로그인 24시간 유지
}
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
return jsonify({'result': 'success', 'token': token})
# 찾지 못하면
else:
return jsonify({'result': 'fail', 'msg': '아이디/비밀번호가 일치하지 않습니다.'})
7. 좋아요 기능 구현하기
서버가 어떤 글이 좋아요 되었는지를 기록하려면 네 가지 정보가 필요합니다. 어떤 사람이 어떤 글을 좋아요한 건지와 하트를 누른건지 좋아요를 취소한건지 알아야 합니다. DB에는 누가 어떤 포스트에 어떤 반응인지를 넣으면 좋습니다.
누가 좋아요를 했는가? -> 쿠키에 저장된 토큰값을 가져옵니다.
어떤 게시글인가? -> post id를 넘겨줍니다.
어떤 종류의 좋아요인가? -> 별, 하트, 좋아요 중 어떤 유형인지를 넘겨줍니다.
어떤 유형의 실행인가? -> 취소인가 좋아요인가를 넘겨줍니다.
이 세가지 정보를 가져와서 실제로 좋아요를 가지고 있다면 디비에 저장을 하고 만약 취소를 하고 있다면 디비에서 삭제를 합니다.
위 동작이 정상적으로 완료된다면 좋아요의 갯수를 세어 준 뒤 클라이언트로 응답하면 됩니다.
클라이언트 코드에서는 서버코드로부터 사용자 정보, 포스트 아이디, 좋아요/좋아요 취소, 아이콘 종류를 응답받습니다.
하트를 누른 사람은 쿠키에 저장된 토큰에 있으므로 나머지 3개의 데이터만 보내주면 됩니다. 좋아요와 좋아요 취소는 아이콘 클래스 fa-heart와 fa-heart-o인지를 살펴보면 됩니다.
'웹 개발 > 웹개발플러스' 카테고리의 다른 글
웹 개발 플러스 3 - 맛집지도 만들기 (0) | 2023.03.17 |
---|---|
웹 개발 플러스 2 - 나만의 단어장 (0) | 2023.03.13 |
웹 개발 플러스 1 - 나만의 일기장 만들기 (1) | 2023.03.10 |