본문 바로가기
Tech Development/Python Backend (Flask API)

Python Backend - Study Notes 9

by JK from Korea 2022. 10. 2.

날짜: 2022.06.09

 

[인증, Authentication]

인증은 사용자의 신원을 확인하는 절차이다. 웹사이트에서 사용자가 로그인을 해서 아이디와 비번을 확인하는 절차가 대표적이다. 로그인과 인증 기능을 구현해주는 엔드포인트로 다음과 같이 처리된다.

 

  1. 사용자가 아이디와 비밀번호를 생성한다. (sign_up 엔드포인트)
  2. 아이디와 비밀번호를 데이터베이스에 저장한다. 보안을 위해 비밀번호는 암호화되어 저장된다.
  3. 사용자가 로그인 절차에서 아이디와 비밀번호를 입력한다. 비밀번호는 위와 동일하게 암호화된다.
  4. 데이터베이스에 저장되어 있는 비밀번호와 비교한다.
  5. 일치하면 로그인 절차가 통과된다.
  6. API 서버가 사용자한테 access token을 전송한다.
  7. 앞으로 서버의 기능 사용할 때 access token으로 사용자 인증을 받는다.

 

위 절차에서의 “암호화”와 “access token”을 자세히 다루고 구현할 것이다.

 

[비밀번호 암호화]

비밀번호를 암호화해서 데이터베이스에 저장하는 이유는 보안 때문이다. 사용자의 비밀번호를 암호화할 때는 단방향 해시 함수 (one-way hash function)를 사용한다. 단방향 해시 함수는 본래의 비밀번호 값을 사용자 이외에는 알지 못하도록 방지하는 기능을 해준다. 예를 들어 비밀번호가 ‘password1’ 과 ‘password2’ 가 있다고 가정하자. “hash256” 이라는 해시 함수를 사용해서 앞에 두개의 비밀번호를 암호화하면 완전히 다른 값을 출력한다. 패턴이 없다. 파이썬에서는 해시 함수를 구현하는 모듈인 hashlib를 사용해서 암호화를 했다. (실행시 터미널에서 직접 실행해야된다. 파일로 만들어서 실행하면 안된다.)

 

 

①  : haslib는 one-way hash function 을 가지고 있는 모듈이다.

② : sha256은 암호화 알고리즘이다.

③ : b”” 을 함으로써 string 형태에서 byte 형태로 data type이 바뀐다.

④ : 암호화된 값을 hex (16진수) 값으로 읽어들인다.

 

One-way hash function 은 생각보다 취약하다. 해킹이 가능하며 미리 만들어놓은 데이터 테이블을 통해서 기존 암호를 추론하는 것이 가능하다. 취약점을 보완하기 위해 우리는 salting과 key stretching 이라는 2가지 보완 방법을 사용할 것이다.

 

  1. Salting : 랜덤 값을 비밀번호 데이터에 더함으로서 해킹을 해도 어느 부분이 랜덤인지 아닌지 모른다.
  2. Key Stretching : 단방향 해시 함수를 여러번 돌리는 것이다. 해시를 반복해서 사용할수록 컴퓨터가 기존 데이터를 찾는데 더 오랜 시간이 걸린다.

 

[Salting & Key Stretching]

 

위의 salting과 key stretching 기능을 모두 구현한 함수가 bcrypt 알고리즘이고 이를 외부 라이브러리에서 불러와야 한다.

 

[bcrypt 설치]

 

이제 실제로 사용해보자.

 

[bcrypt를 통한 암호화]

 

[Access Token]

sign-up 절차를 설명하면서 access token을 언급했었다. 사용자가 sign-up을 하고 로그인 절차에서 비밀번호가 일치하면 서버는 access token이라는 것을 클라이언트한테 넘겨준다. Access Token은 “인증된 자”의 상징이다. 그래서 앞으로 사용자가 서버의 기능을 사용할 때 access token만 넘겨주면 된다.

 

Access Token은 서버 쪽에서 만들어줘야 된다. JWT (JSON Web Token)라는 기술을 사용해서 만들거다. JWT는 JSON 데이터를 token으로 변환해준다.



[Access Token & Authorization]

 

Access Token은 서버에서 JWT를 사용해서 생성하기 때문에 다순한 JSON 데이터가 아니라 “인증” 기능을 탑재한 정보이다. 그래서 로그인 뒤, 아무나 사용자라고 인식되는 경우를 방지한다.

 

[JWT 구조]

JWT를 import 해서 사용할거지만, 기본 구조와 작동 원리는 알고가자. 먼저 JWT는 세 부분으로 구성되어 있다.

 

“xxxx.yyyy.zzzz → header.payload.signature”

 

① header : 토큰 타입 (=JWT)와 해시 알고리즘 (hashing algorithm)을 지정한다.

② payload : 실제로 서버 간에 전송하고자 하는 데이터 부분이다. header와 payload는 Base64 URL이라는 형식으로 코드화 (encoding) 된다. 데이터를 컴퓨터 언어로 바꾼다고 생각하면 된다.

③ signature : ①에서 포함하고 있는 해시 알고리즘과 연관되어 있다. signature는 서버에서 access token을 “인정” 해주는 의미에서 “사인” 해준다. 즉, 클라이언트가 access token을 서버에 보내면, 서버는 access token을 받아서 사인 (signature)를 대조해본다. 서버에서 기존에 보내준 사인과 일치하면 인증된 토큰이라는 의미이다.

 

간단하게 예시를 보자. 파이썬에서 JWT를 사용할 수 있도록 PyJWT 라이브러리를 사용할 것이다. 

 

[PyJWT Installation]

 

[jwt encode & decode]

 

[인증 엔드포인트 구현하기]

앞서 얘기했듯이 인증은 회원가입 절차와 로그인 절차에 쓰이고, 그 뒤로는 access token을 사용하기 때문에 별도의 복잡한 인증 절차를 통과할 필요가 없다. 우리는 회원가입 절차부터 살펴 볼 것이다. 이미 구현해놓은 sign-up 엔드포인트를 수정할 것이다.

 

[sign-up 엔드포인트]

 

168번 코드를 보면 앞서 보았던 bcrypt 사용법과 동일하게 구현되어 있다. 단, “.encode(‘UTF-8)”는 new_user[‘password’] 를 byte 언어로 변화해주는 역할을 한다. request.json에서 받아온 password는 string 타입이기 때문에 bcrypt에 적용하기 위해서는 byte로 인코딩해야된다.

 

이제 인증 엔드포인트를 만들것이다. 인증 엔드포인트의 주요 기능은 “로그인” 과정을 담당하는 것이고, 비밀번호에 이상이 없을시 access token을 넘겨준다.

 

[Creating Access Token]

 

[Access Token in Depth]

Access Token을 만든 우리의 목적은 token을 통해서 인증된 사용자만 서버를 사용 가능하도록 하는 것이다. 서버의 기능 (엔드포인트)을 사용할 때 인증절차가 공통으로 필요하다. 여러 개의 엔드포인트를 호출할 때 마다 사용되는 기능을 decorator 를 사용해서 만들 것이다.

 

[Python Decorator]

Decorator로 지정된 함수는 다른 함수가 실행되기 직전에 자동으로 먼저 실행된다. Decorator의 간단한 형식을 살펴보자.

 

 

① : “@ + 함수” 형식으로 decorator가 호출된다.

② : 이 함수를 실행하려고 하면 터미널은 자동으로 위에서부터 읽으면서 ①을 보고 run_this_first 라는 함수를 찾아서 실행한다. @app.route와 같은 개념이다. @app.route도 decorator 이다.

 

[인증 decorator 함수 구현하기]

앞서 만든 로그인 엔드포인트에서는 JWT access token을 클라이언트한테 보내준다. 그리고 클라이언트가 서버를 사용하기 위해 인증을 할 때 access token을 HTTP Request Header에 포함시켜서 보낸다. Header에서 “Authorization” 부분에 포함시켜서 보내기 때문에 우리의 역할은 HTTP 요청의 Authorization에서 값을 읽고 JWT access token을 복호화해서 사용자 로그인 여부를 결정짓는 시스템을 만드는 것이다.

 

 

*추가 설명이 필요한 코드만 골랐다.

 

⑦ : functools 라이브러리의 wraps decorator 함수를 적용해서 decorator 함수를 구현한다. decorator는 함수를 반환하는 함수이다. wraps는 decorator를 구현할 때 주의해야 하는 __name__, __module__ 등이 함수와 속성들을 자동으로 처리해주기 때문에 사용한다. (참고 : https://flask-docs-kr.readthedocs.io/ko/latest/patterns/viewdecorators.html)

 

⑧ : (*args, **kwargs) 문법은 풀어써보면 이해가 된다. args는 arguments를 줄인말로 string, int 형태의 데이터를 인자로 받는다는 것이다. kwargs는 keyword arguments의 줄인말이다. args와 다른 점은 kwargs의 형태이다. python dictionary와 같이 key-value 로 묶여있는 형태이다. (참고 : https://brunch.co.kr/@princox/180)

⑫ : payload는 앞서 구현한 로그인 엔드포인트를 참고하면 된다. 로그인 엔드포인트에서 토큰을 만들 때 우리는 “user_id”와 “exp” 정보를 담은 payload를 인코딩했다. 그래서 jwt.decode를 하면 user_id와 exp 정보가 복호화 된다. 복호화 되서 읽을 수 있는 정보 상태가 우리가 원하는 상태이다.

 

㉕ : 나중에 구현하게 될 API를 보면 flask에서 “g” 클래스를 import해야 된다. g는 전역 변수 (global variable)이다. g라는 객체를 사용하면 불특정 다수의 모든 사용자들을 제어할 수 있기 때문에 사용한다. (참고: https://jong-seok-ap.tistory.com/12)

 

㉖ : “get_user_info” 함수는 우리가 구현한 데이터베이스 접속 함수이다. 이제 access token을 통해서 사용자가 validation이 끝난 시점이기 때문에 데이터베이스에 저장되어 있는 정보를 g.user라는 전역 변수에 저장하는 것이다.

 

이제 login decorator 함수를 포함한 전체 API를 구현할거다. 잠깐 decorator에 대한 추가 설명을 붙이자면, 이미 우리가 사용하는 “@app.route”도 decorator의 한 종류이다. 그래서 “@login_required” decorator를 추가하면 2개의 decorator를 적용시키는 것이고, 이 경우에 순서가 중요하다. 먼저 오는 코드가 먼저 실행되기 때문이다. 우리는 route decorator를 먼저 적용해서 해당 함수가 엔드포인트로 인식될 수 있도록 할 것이다. 이제 전체 코드를 구현하고 HTTP 테스트를 해보자.

 

[인증되지 않은 사용자가 엔드포인트 접근할 경우]

 

위에서 실행한 tweet 엔드포인트는 tweet, follow, unfollow 엔드포인트 중 하나이다. 이 세가지 엔드포인트는 “login_required” decorator가 붙었기 때문에 로그인 없이 실행하려고 하면 401에러가 뜬다. 현재는 SQL 데이터베이스에 사용자가 없다. 앞서 포스트에서 데이터베이스 정보를 모두 지웠기 때문이다. 그렇기 때문에 회원가입하고 로그인을 하고 다시 tweet 엔드포인트를 실행시켜 보자.

 

[junho@gmail.com 으로 사용자를 만들었음을 확인할 수 있다.]

 

[데이터베이스를 조회해보면 지금까지 생성한 사용자들이 모두 조회 가능하다. 위에 만든 junho 사용자도 있음이 확인된다.]

 

[이메일과 비밀번호를 입력했을 때 로그인 기능이 정상적으로 실행되는 것을 볼 수 있다.]

 

[조금 지저분하지만.. 로그인 기능에서 받은 Authorization 키를 사용해서 자동으로 인증이 되는 것을 볼 수 있다. Tweet은 인증 기능이 필요한 엔드포인트로 Tweet이 된다는 것은 인증 엔드포인트에서 넘겨준 Authorization Process가 정상 작동한다는 의미이다.]

 

[성공적으로 tweets 데이터베이스에 저장되었다.]

 

[인증 과정 중 겪은 에러 그리고 해결 방안]

 

[암호화 과정의 Signature 부분을 직접 입력으로 코드 수정]

 

위의 인증 과정을 테스트하는 과정에서 ‘JWT_SECRET_KEY’ KeyError 가 지속적으로 떴다. app.config라는 것을 보면 config.py에서 가져오는 것인데 config.py 파일에는 지정된 키 값이 없기 때문에 KeyError를 반환한것이라고 추측한다. 그래서 암호화 과정에서 사용하는 signature를 굳이 config.py 에서 가져오는 것이 아닌, 코드에 직접 입력하는 방식으로 바꾸니까 해결됐다.

 

[리턴 과정에서 token이 string 타입이므로 그대로 반환해줌]

 

위에 KeyError를 고치니까 이번에는 decode error가 떴다. Encode와 decode는 byte와 string 간의 타입 변환을 위해 해주는 것이라고 알고 있다. 기존의 token.decode를 하려고 했을 때 이미 string 타입이라고 에러가 떳기 때문에 decode 과정을 거치지 않고 그대로 리턴할 수 있도록 코드를 수정했다.

 

728x90
반응형

댓글