티스토리 뷰

지금 개발 중인 프로젝트에서는 웹 서버와 API 서버가 분리되어 있다. 웹 서버에서는 페이지마다 필요한 데이터들을 API 서버에 요청을 보내 받아와 사용자가 접근하고자 하는 뷰를 렌더링한다. 이에 따라 API 서버에서는 OAuth 기능이 필연적으로 필요하다. 웹 서버와 API 서버 둘 다 flask로 구현되어 있기 때문에 API 서버에서는 flask와 연동이 잘 되는 OAuth 2.0을 지원하는 라이브러리를 찾았고, Flask-OAuthlib를 사용하고 있다. Flask-OAuthlib를 이용하여 OAuth 2.0을 지원하도록 하는 작업은 다음 링크를 참고하자. 차후 가능하면 추가로 블로깅을 할 생각은 있으나 그게 언제가 될지는....


웹 서버와 API 서버의 연동을 설명하면 다음과 같다.


가장 우선 로그인이 필요하다.

1. 유저가 로그인을 시도하면 API 서버의 로그인 페이지로 이동한다.

2. API 서버의 로그인 페이지에서 로그인을 성공하면 API 서버가 response로 웹 서버의 callback url로 redirect 시키는 302 response를 준다. 이 때 임시로 발급되는 code 값을 획득한다.

3. 브라우저에서는 302 response에 의해 웹 서버의 callback url로 이동한다.

4. 웹 서버의 해당 url에서는 이전에 획득한 code 값을 이용하여 oauthlib의 token_handler에 요청을 보내 access token과 refresh token을 획득한다. 획득한 토큰들은 쿠키로 저장하던가 아니면 세션에 저장하여 앞으로 요청시 계속 사용한다. (access token과 refresh token 등을 만들어주는 것은 create_token_response 메소드를 참고. 이 글에서는 자세히 다루지 않는다.)


API 서버의 로그인 페이지로 이동할 때는 조금 복합한 URL 생성이 필요하다. 



params를 보면 여러가지 추가적인 파라미터를 명시해 놓은 것을 알 수 있다. 이는 OAuth 2.0 스펙에 필요한 추가적인 내용들이다. 예를 들면 `client_id`를 통해 어떤 애플리케이션에서 토큰 요청을 한 것인지 판별하고, `redirect_url`을 통해 OAuth 2.0 서버의 authorize 작업 후 callback할 url을 명시하여준다. 나 같은 경우 어떤 링크에 접근하고자 하는데, 로그인이 되지 않은 상태였을 경우, 로그인 후 원래 이동하려던 링크로 이동이 될 수 있도록 하기 위해 `next` 필드를 추가해줬었다. `response_type`이 code인 것을 보아 위에서 설명한 4번의 code 값을 이렇게 얻는다는 것을 알 수 있을 것이다.


다음은 callback url에서의 처리에 대한 코드이다. form 데이터로 어떤 것을 어떤 필드명으로 넣어줘야 하는지에 대한 설명이 잘 되어 있는 곳이 별로 없다는게 황당했다. flask-oauthlib 라이브러리의 예제에서도 자신의 라이브러리에 있는 클라이언트 클래스를 사용하는 것으로 되어 있었으니... 나중에 토큰 갱신과 비교하였을 때 봐두면 좋은 필드는 `grant_type`이다.



OAuth 2.0 과정 중 Bearer 토큰을 생성하기 전에 임시로 Grant 토큰이라는 것을 생성한다. 이 Grant 토큰에 지정한 expire time에 따라 Bearer 토큰의 expire time이 결정된다. Grant 토큰의 주요 역할은 `scope` 등에 대한 것도 있겠지만, 역시 `code` 값을 갖고 있는 것이 가장 주요 역할이다. 이렇게 만들어진 `code` 값을 정상적으로 획득한 애플리케이션이 다시 토큰을 얻기 위한 API 호출을 했을 때 정상적인 Bearer 토큰을 얻게 된다. 


`RequestOauth.get_token` 함수에서는 requests 라이브러리를 이용하여 POST 요청을 보낸다. 이 때 위에 써둔 form 데이터를 함께 보내야 한다. 이 때 Grant 토큰의 code 값을 사용하는 것을 확인할 수 있다. 요청 결과로는 access token과 refresh token을 획득하게 된다. 얻은 토큰은 어딘가에 잘 저장해두자. 위 코드에서는 저장 방법을 숨기기 위해 따로 적어두진 않았다. 세션이나 쿠키 등에 저장해둘 수 있을 것이다. 토큰을 획득한 뒤부터는 HTTP 헤더로 Authorization 헤더를 추가하고 그 값으로 `'Bearer {}'.format(access_token)`의 형태로 문자열을 지정하여 API 서버에 요청을 보낸다. API 서버에서는 `tokengetter`를 통해 적절한 토큰인지 확인하고, `usergetter`를 이용하여 유저 모델을 로드하여 사용한다. 


Bearer 토큰의 expire time이 지나면 해당 Bearer 토큰의 access token은 더 이상 사용할 수 없고, 다시 로그인을 하여 새로운 Bearer 토큰을 만들거나 refresh token을 이용하여 access token을 갱신해야 한다. 아무래도 사용자가 좀 더 편하게 애플리케이션을 사용하기 위해선 아이디와 비밀번호를 다시 입력하게 하는 것보다 refresh token을 이용하여 토큰 갱신을 하는 것이 훨씬 나은 방법이다. Flask-OAuthlib의 OAuth 2.0 Client를 사용하면 이것을 자동으로 해주는지 모르겠으나, 나 같은 경우 웹 서버에서는 모든 API 요청을 requests 라이브러리를 활용한 클래스로 만들어 이용하고 있었다. 따라서 토큰 갱신 작업은 직접 해줘야 했다.


Flask-OAuthlib의 서버에 `invalid_response` 데코레이터를 이용하여 인증 과정 중 어떤 문제가 있는 경우 따로 처리를 해줄 수 있다. 아래 코드를 보면 토큰이 만료되었을 때, 접근 권한이 부족할 때, 그 외 상황 3가지로 나눠 예외를 띄워주도록 한 것을 볼 수 있다. 예외를 이용하여 HTTP 에러를 반환하는 방법에 대해서는 다음 글을 참고. 모두 다 401 에러이지만, 클라이언트에서 상황에 따라 다른 처리를 해주기 위해서는 추가 메세지를 써주는 것이 필요하기 때문에 이렇게 하였다. 직접 비교되는 문자열은 flask-oauthlib에 의해 정해진 상황별 에러 메세지이다. 이를 이용하여 각각의 다른 예외를 생성한 것이다.



현재 우리 API 서버에서는 모든 사용자 정의 예외에 `reason` 필드를 추가한 json을 response로 전달하도록 되어 있다. 따라서, 클라이언트에서 요청을 보낸 결과 HTTP 에러가 발생한 경우 얻어온 response에서 `reason` 값을 확인하여 다른 처리를 할 수 있다는 의미이다. 웹 서버에서 API 요청을 보낼 때는 try-catch문을 사용하도록 되어 있고, requests 라이브러리를 이용하기 때문에 다음과 같은 정도의 코드가 될 것이다.



flask에서는 abort를 이용하여 에러를 띄울 경우 내부적으로 `werkzeug.exceptions`에 정의된 `HTTPException` 예외를 상속받은 여러가지 에러 코드별 예외 클래스의 인스턴스를 만들어 사용한다. 그리고, 사용자 정의 에러 핸들러를 등록해놓을 경우 해당 예외 객체를 받을 수 있다. 예를 들면 `abort(401)`를 호출하면 `Unauthorized` 예외가 만들어져 사용되는 것이다. abort에 추가적인 인자로 문자열을 넘겨주면 해당 `HTTPException` 객체에 description을 추가할 수 있다. 위 소스에서는 abort 함수에 인자로 에러 코드 뿐만 아니라 reason을 함께 넣어주는 것을 알 수 있다. 이렇게 넣어준 description을 사용하는 예는 다음과 같다.



앞서 예시로 든 401 에러를 대응하기 위한 에러 핸들러이다. 함께 전달된 예외의 description을 확인하여 각각 다른 response를 만들도록 한다. 일단 에러 핸들러의 주요 역할은 적절한 response를 만들어 돌려주는 것이다. 에러 페이지를 띄워야 할 때는 템플릿을 로드하여 response로 만들어 돌려주고, 로그인이 필요한 경우에는 로그인 페이지로 이동하게 하기 위한 URL을 만들어주는 `get_oauth_req_url` 함수를 호출하여 얻은 URL을 redirect location으로 사용한다.


`refreshing_oauth_token`는 아직 설명되지 않은 함수이다. 앞서 언급한 바와 같이 나는 우리 웹 애플리케이션에서 사용자의 access token이 만료된 경우 refresh token을 이용하여 토큰을 갱신하고자 했다. 이 때도 이전의 로그인 때와 크게 다르지 않다. 



위에서 로그인 callback 함수에서의 form 데이터와 다른 점은 `grant_type`이다. 이전에는 authorization_code이었던 것이 `refresh_token`으로 바뀌었다. 이 내용이 은근히 찾아도 잘 안 나온다는게 황당했다. 하여튼 마찬가지로 위 form 데이터를 이용하여 API 요청을 보내면 refresh token을 이용한 새로운 Bearer 토큰 생성이 완료된다.


OAuth 2.0 스펙에 맞는 서버 개발을 직접 하는 것이 불가능한 일은 아니지만, 이미 구현된 라이브러리가 있기 때문에 갖다 쓰는게 더 나을 것이다. 생각보다 복잡한 구석이 있어서 직접 개발하려면 시간이 꽤 걸릴 수 있다. 서버 구현 측은 flask-oauthlib의 문서를 보면 되긴 했다. 다만 flask-oauthlib에서는 내부 실행이 상당히 캡슐화 되어 있어서 나 같이 애플리케이션에서 flask-oauthlib 라이브러리의 클라이언트 클래스를 사용하지 않으려는 개발자에게 난감함을 줬다. 게다가 꾸준히 언급하는 것이지만 검색을 해도 잘 안 나온다.... 스택오버플로우 글에도 vote 수가 높지 않아서 이게 맞나 의심하면서 시도해보는 안습한 상황도 있었다. 코딩한 코드 자체는 얼마 안 되지만 진입 장벽이 좀 있겠다 싶은데다가 한글로 된 가이드 문서는 거의 없다시피 해서 나중에 글을 써봐야겠다고 생각했었는데, 거의 몇 달을 미루다가 이제서야 쓰는 것 같다. 많은 이들에게 도움이 됬으면 좋겠다!

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday