😂 [팀플] 인스타그램 클론코딩
😭 로그인 API를 이해하기 위한 배경지식
- HTTP 특성
- HTTP : 인터넷 상에서 데이터를 주고 받기 위한
서버/클라이언트
모델을 따르는 프로토콜
- 클라이언트(나)가 서버에게 요청을 보내면 서버는 응답을 보내는 과정을 거쳐 데이터를 교환
(내가 지금까지 공부했던 GET과 POST 방식을 생각하면 될 것 같다!)
- HTTP는
비연결성
과 무상태성
이라는 특징에 의해 클라이언트의 요청에 응답하고 나서는 연결을 바로 끊어버리게 된다. 즉, 클라이언트에 대한 상태 정보 / 통신 상태
에 대한 정보를 가지고 있지 않는 것이다. 이유는 즉슨 나와 같은 클라이언트가 한 두명이 아닐텐데 이걸 다 유지하고 있으면 엄청난 자원 낭비
가 있기 때문에 아예 한 번 주고 다 끊어버려서 낭비를 막는 것이다.
- 자원 낭비를 막을 수 있는 장점을 가지고 있지만, 여기서 클라이언트에게는 큰 단점이 될 수 있는 일이 벌어지게 된다. 서버가 클라이언트를
식별하지 못한다
는 것이다. 해당 특성만 가지고 본다면 사용자는 페이지를 옮기게 되거나 새로고침을 누를 때마다 로그인을 시도해야 하는 것이다. 생각만해도 진짜 개짜증날거 같다.
- 위의 단점을 보완하기 위해 만들어진 기술이
Cookie
와 Session
이다.
- Cookie
- 클라이언트가
어떤 웹사이트를 방문
할 경우, 그 사이트가 사용하고 있는 서버
를 통해 클라이언트의 브라우저
에 설치되는 작은 기록 정보 파일
- 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>
<title>Hello, world!</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
<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>
- 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']})
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="로그인 정보가 존재하지 않습니다."))
- signup 기능
- js 부분
- 사용자의 정보를 입력해 회원가입을 하는 기능이므로 HTTP method는 POST를 사용했다.
<!doctype html>
<html lang="en">
<head>
<script>
function register() {
let id_give = $('#userid').val()
let pw_give = $('#userpw').val()
let nickname_give = $('#usernick').val()
$.ajax({
type: "POST",
url: "/api/register",
data: {'id_give': id_give, 'pw_give': pw_give, 'nickname_give': nickname_give},
success: function (response) {
console.log(response)
if (response['result'] == 'success') {
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']
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>
{% 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') {
$.cookie('mytoken', response['token']);
alert('로그인 완료!')
window.location.href = '/'
} else {
alert(response['msg'])
}
}
})
}
</script>
</head>
</html>
- flask 부분
- 입력 받은 회원정보를 바탕으로 db에 일치하는 데이터가 있는지 먼저 확인한다.
- 만약
result
가 있다면, payload
에 사용자의 아이디와 유효기간을 key-value 값의 형태로 입력한다. (주로 아래 코드에 있는 아이디와 유효기간을 저장하게 되며, 단순히 인코딩만 진행하기 때문에 사용자의 중요정보를 담을 수 없다는 단점이 존재)
- jwt를 발급하기 때문에
payload
와 SECRET_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()
result = db.user.find_one({'id': id_receive, 'pw': pw_hash})
if result is not None:
payload = {
'id': id_receive,
'exp': datetime.datetime.utcnow() + datetime.timedelta(seconds=60 * 60 * 24)
}
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:
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': '로그인 정보가 존재하지 않습니다.'})