인증(Authentication), 인가(Authorization)

강민성·2021년 12월 20일
1

인증(Authentication)

Authentication은 유저의 identification을 확인하는 절차(유저의 아이디와 비번을 확인하는 절차)
우리 서비스를 누가 어떻게 사용하는지 추적하기 위해 필요
인증을 하기 위해선 먼저 유저의 아이디와 비번을 생성할 수 있는 기능이 필요
인증에 필요한 것들 중 가장 중요한 것은 비밀번호
민감한 개인 정보(ex. 비밀번호)들은 개발자들조차도 알 수 없어야 하기 때문에, 회원가입시 받은 비밀번호를 백엔드가 암호화해야 함
DB에 유저 비밀번호를 그대로 저장하면 안되는 이유: DB가 해킹당하면 유저의 비밀번호가 그대로 유출되며, 내부 인력이 회원의 비밀번호를 열람할 수도 있기 때문(개인정보보호법에 따라 의무적으로 암호화해야 함)

로그인 절차

회원가입

유저 아이디와 비밀번호 생성

유저 비밀번호를 암호화하여 DB에 저장

로그인

유저 아이디와 비밀번호 입력

유저가 입력한 비밀번호를 암호화

암호화된 입력 비밀번호를 DB에 저장된 유저 비밀번호와 비교

--> 일치하면

로그인 성공. access token을 클라이언트에게 전송

로그인 성공 이후:

로그인 상태가 유지되게 하기 위해, request에 access token을 첨부해서 서버로 전송

비밀번호 암호화 방법

프론트로부터 개인정보 받기 --> http통신이 가로채지면 정보가 유출될 수 있기 때문에, http통신 단계에서 먼저 암호화 --> 백엔드가, 개인정보를 DB에 저장할 때 개인 정보를 해싱하여 복원할 수 없게 함

단방향 해쉬 함수(one-way hash function)

원본 메시지를 암호화된 메시지(다이제스트)로 변환하는 함수
단방향성(one-way): 원본 메시지 --> 암호화된 메시지로 암호화하는 것은 쉽지만, 암호화된 메시지 --> 원본 메시지로 해독할 수는 없음
avalance: 암호화 전 값이 비슷하더라도 함수를 거친 해쉬 함수 값은 전혀 다르게 나옴
MD5(보안취약), SHA-1(보안취약), SHA-256 등이 있음
예시)

"test password" 를 hash256이라는 해쉬 함수로 암호화한 값:
0b47c69b1033498d5f33f5f7d97bb6a3126134751629f4d0185c115db44c094e

"test password2" 를 hash256이라는 해쉬 함수로 암호화한 값:
d34b32af5c7bc7f54153e2fdddf251550e7011e846b465e64207e8ccda4c1aeb

취약점

  • 같은 함수로 같은 값을 해싱하면 늘 같은 값이 나옴
  • Rainbow table attack: Rainbow table(미리 해쉬값들을 계산해 놓은 테이블)을 이용하여 해시값을 유추 가능
  • 해시 함수는 원래 패스워드를 저장하기 위해서 설계된 것이 아니라 짧은 시간에 데이터를 검색하기 위해 설계된 것(Remember Set?). 따라서 해시 함수는 본래 처리 속도가 최대한 빠르도록 설계됨. 이러한 속성 때문에 공격자는 매우 빠른 속도로 임의의 문자열의 다이제스트와 해킹할 대상의 다이제스트를 비교할 수 있음(MD5를 사용한 경우 일반적인 장비를 이용하여 1초당 56억 개의 다이제스트를 대입할 수 있다). 이런 방식으로 패스워드를 추측하면 패스워드가 충분히 길거나 복잡하지 않은 경우에는 알아내는 데에 그리 긴 시간이 걸리지 않음(대부분 사용자의 패스워드는 길거나 복잡하지 않을 뿐 아니라, 동일한 패스워드를 사용하는 경우도 많다).

Salting

입력한 비밀번호와 랜덤생성한 문자열(salt)을 합쳐서 해싱하여 그 해시값을 저장하는 방법
비교를 위해 해시값과 소금값(salt)을 같이 저장해야 함

Key Stretching


salting 및 해싱을 여러번 반복해서 원본 값을 유추하기 어렵게 하는 것
단방향 해쉬값을 계산 한 후 그 해쉬값을 또 해쉬 하고, 또 이를 반복하는 것
해커가 패스워드 무작위 대입을 통해 해시값을 계산하는 데 필요한 시간을 대폭 늘리는 것이 목적
최근에는 일반적인 장비로 1초에 50억 개 이상의 다이제스트를 비교할 수 있지만, 키 스트레칭을 적용하여 동일한 장비에서 1초에 5번 정도만 비교할 수 있게 함. GPU(Graphics Processing Unit)를 사용하더라도 수백에서 수천 번 정도만 비교할 수 있음(50억 번과는 비교할 수도 없을 정도로 적은 횟수). 앞으로 컴퓨터 성능이 더 향상되면 몇 번의 반복을 추가하여 보완할 수 있을 것.

Bcrypt


Salting과 Key Stretching을 구현한 해쉬 함수중 가장 널리 사용되는 라이브러리
단방향 해쉬 함수의 취약점들을 보완하기 위해 2가지 방법 사용(Salting, Key Stretching)
처음부터 비밀번호를 단방향 암호화 하기 위해 만들어진 해쉬함수 라이브러리
Bcrypt 라이브러리를 통해 백엔드가 쉽게 암호화를 구현할 수 있음
해시값(해싱한 결과값)에 소금값과 해시값 및 반복횟수를 같이 보관하기 때문에 비밀번호 해싱을 적용할 때 DB설계를 복잡하게 할 필요가 없음

  • bcrypt를 통해 해싱된 결과값(Digest)의 구조

JWT(JSON Web Tokens)


access token을 생성하는 여러 방법 중 가장 널리 사용되는 기술 중 하나
유저 정보를 담음 JSON 데이터를 암호화 해서 클라이언트와 서버간에 주고 받는 것
로그인 시 JWT 토큰(access token, 암호화된 유저 정보) 발행
유저가 로그인에 성공한 후에는, header의 authorization에 로그인 시 발행한 JWT 토큰을 첨부해서 request를 보냄

jwt token(jwt로 암호화된 데이터)


header/payload/signature의 세 부분으로 이루어져 있음
데이터타입은 bytes

토큰의 타입과 해시알고리즘 정보가 BASE64 방식으로 인코딩되어 토큰의 가장 첫 부분에 기록됨

payload(내용)

claim payload에 담는 정보 하나하나를 지칭
아래의 세 가지 claim을 조합하여 작성한 뒤 BASE64 방식으로 인코딩하여 토큰의 두번째 부분에 위치

  • Registered Claim: 만료시간을 나타내는 exp와 같이 미리 정의된 집합
  • Public Claim: 공개용 정보 전달을 목적으로 하는 Claim
  • Private Claim: 클라이언트와 서버 간 협의 하에 사용하는 Claim

signature(서명)

JWT 토큰이 원본 그대로라는 것을 확인할 때 사용하는 부분
BASE64URL 방식으로 인코딩된 header와 payload 그리고 토큰 생성 시 입력한 secret_key를 header에 지정된 암호 알고리즘으로 암호화하여 전송(복호화 가능)
프론트가 토큰을 백엔드 API 서버로 전송하면 서버에서는 전송받은 토큰의 서명부분을 복호화하여 서버에서 생성한 토큰이 맞는지 확인
예시)

  • 유저 로그인
POST /auth HTTP/1.1
Host: localhost:5000
Content-Type: application/json

{
    "username": "joe",
    "password": "pass"
}
  • access token
HTTP/1.1 200 OK
Content-Type: application/json

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6MSwiaWF0IjoxNDQ0OTE3NjQwLCJuYmYiOjE0NDQ5MTc2NDAsImV4cCI6MTQ0NDkxNzk0MH0.KPmI6WSjRjlpzecPvs3q_T3cJQvAgJvaQAPtk1abC_E"
}

--> 서버에서는 받은 access token을 복호화해서 해당 유저 정보(ex. id)를 얻음
예시) eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6MSwiaWF0IjoxNDQ0OTE3NjQwLCJuYmYiOjE0NDQ5MTc2NDAsImV4cCI6MTQ0NDkxNzk0MH0.KPmI6WSjRjlpzecPvs3q_T3cJQvAgJvaQAPtk1abC_E이라는 access token을 복호화하여 다음과 같은 정보를 얻을 수 있음

{
    user_id = 1 
}

인가(Authorization)

유저가 요청하는 request를 실행할 수 있는 권한이 있는 유저인가를 확인하는 절차
http는 stateless하기 때문에(기억을 못함), 권한을 확인하면 header에 토큰을 붙여줘서 해당 유저가 권한이 있다는 것을 알게 함
JWT를 통해서 구현 가능

인가 절차

인증(Authentication)

인증 절차를 통해 access token(JWT token)을 생성. access token에는 유저 정보를 확인할 수 있는 정보(ex. id)가 들어가 있어야 함
유저가 request를 보낼때 header의 authorization에 access token을 첨부해서 보냄
백엔드는 토큰 정보를 해석하는 decorator를 만들어서 인가가 필요한 엔드포인트들을 사용할 수 있음

인가

서버에서는 유저가 보낸 access token을 복호화
복호화된 데이터를 통해 user id를 얻음
user id를 사용해서 database에서 해당 유저의 권한(permission)을 확인
--> 유저가 충분한 권한을 가지고 있으면: 해당 요청을 처리
--> 유저가 권한을 가지고 있지 않으면: Unauthorized Response(401) 혹은 다른 에러 코드를 보냄

인증, 인가 실습

bcrypt로 데이터 암호화

가상환경 실행 후 bcrypt 설치
pip install bcrypt
파이썬 shell 실행 후 bcrypt import
import bcrypt

hashpw 함수

변수명1 = '암호화할데이터' # 암호화할 데이터를 변수명이라는 변수에 할당

변수명2 = 변수명1.encode('utf-8') # 암호화할 데이터를 인코딩

솔트변수명 = bcrypt.gensalt(추가데이터) # salt값을 변수에 할당

변수명4 = bcrypt.hashpw(변수명2, 솔트변수명) # 해시함수로 암호화한 값을 변수명4에 할당

해시함수 사용 전에 데이터를 인코딩해야 함
인코딩을 하면 암호화할내용의 type이 bytes로 바뀜(바이트화됨)
추가데이터 부분을 비워두면 추가데이터가 디폴트값인 14로 적용됨
salt값을 변수로 할당하지 않으면 매번 값이 변함

DB에 데이터를 저장할 때는 먼저 암호화할내용을 해시함수를 통해 암호화하고(바이트가 됨), 암호화된 값을 decode하여(문자열이 됨) DB의 Table에 저장

변수명3 = 변수명2.decode('utf-8') # 인코딩된 변수를 다시 인코딩 전으로 변환

check 함수

bcrypt.checkpw('입력받은데이터값'.encode('utf-8'), 변수명4(암호화된값))

입력받은 데이터값이 암호화된 값과 같으면 True, 다르면 False 리턴

JWT로 데이터 암호화

가상환경 실행 후 pyJWT 설치
pip install pyjwt
파이썬 shell 실행 후 jwt import
import jwt

JSON web token 생성

변수명1 = jwt.encode({payload에들어갈내용}, '프로젝트mysettings.py의secret_key', algorithm='알고리즘내용')

jwt.decode(변수명1, '프로젝트mysettings.py의secret_key', algorithms=['알고리즘내용'])
--> {payload에들어갈내용} 복호화

payload에들어갈내용: DB의 유저정보(ex.{'user_id': 2}). 복호화(디코딩)하면 payload에들어갈내용을 알 수 있기 때문에 payload에는 절대 유저의 개인정보(유출되면 안되는 정보)를 넣으면 안 됨
알고리즘에들어갈내용: 공식 문서에서는 HS256 사용. 이 내용은 기밀사항이므로 노출되지 않고 환경변수로 처리해야 함

profile
Back-end Junior Developer

0개의 댓글