ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • flask-jwt-extended를 활용한 로그인 구현
    Python 2021. 7. 6. 14:30

    그간 JWT(Json-Web-Token)을 쉽게 사용했었는데 막상 직접 구현하려 하니 막막한 것이 한둘이 아니었다. 따라서 결국엔 기존에 존재하는 라이브러리를 사용하기로 하며 그동안 잘못 알고있었던 많은 개념을 다시 잡고 실제 내가 활용할 서비스에 어떻게 적용 할지에 대한 고민을 했다.

    1. JWT란

    JWT는 JSON Web Token의 약자이며 두 개체에서 JSON 객체를 사용하여 정보를 안정성 있게 전달한다.
    또한, 다양한 프로그래밍 언어에서 지원 가능하다.
    주로 회원 인증, 정보 교류의 상황에서 JWT를 사용하게 되는데 이는 JWT는 서명을 기반으로 생성된다. 따라서 토큰에 대한 유효성을 서버 혹은 양측에서 검증할 수 있기 때문에 정보의 조작 혹은 송신자의 변경 여부를 알 수 있다.

    간단하게 예를 들어 설명한다.
    1. 특정 사용자가 로그인(회원 인증)을 위해 ID/PW를 전송하고 이를 수신한 서버는 PW가 Correct함을 확인한다.
    2. 서버는 인증을 확인했기에 확인했다는 증명서 즉 토큰(어떤 사람에 대한 인증인지 정보가 서명되어 담겨있고 토큰의 만료기간 역시 포함된다.)을 발급하고 사용자에게 토큰을 보내준다.
    3. 이후 사용자는 토큰을 가지고 회원 만이 사용 가능한 특정 기능을 활용하려 한다. 이 때 사용자는 자신이 발급받은 토큰을 “쓱”하고 들이 밀며 본인이 인증받은 사용자임을 토큰을 통해 대신 설명할 수 있다.
    4. 서버는 토큰을 받기 전에는 요청한 사용자가 누구인지 인가받은 사용자인지 알 수 없으나 사용자가 들이민 토큰을 확인하면 사용자가 누구인지 또, 인가 받은 대상인지, 토큰의 만료 시간이 지났는지 여부를 알 수 있다.

    2. refresh token이 필요한 이유

    refresh token을 설명하기 앞서 access token을 먼저 설명해야 할 것 같다. access 토큰이란 사용자가 본인이 인가 받았음을 증명할 때 사용하는 토큰이다 즉 앞서 예를 들어가며 설명한 그 토큰은 access 토큰이다.

    만약 access 토큰의 만료 기간이 2주라고 하자, access 토큰의 경우 로그가 그대로 노출되는 경우가 많다. 따라서 특정한 경로로 access 토큰이 탈취 하기 쉽다는 것을 의미하기도 한다. 따라서 만약 오늘 토큰을 발급받고 탈취 하면 2주란 기간동안 해커는 별다른 노력없이 본인이 탈취한 토큰의 주인인 사용자인 척 행동할 수 있다.
    그러나 access 토큰을 탈취하기 쉬운 만큼이나 refresh 토큰도 탈취하기 쉽지 않을까 라는 의문이 들 것이고 필자 역시 그렇다. 그러나 “access 토큰 보다는 비교적 사용이 적고 인증용도가 아닌 재발급 용도로만 사용 하도록 분리되어 있다면 토큰 탈취의 위협에서 ‘비교적’ 안전할 수 도 있겠다.” 라는 생각도 할 수 있다.

    또한, 어떤 사람이 stackoverflow에 질문을 올려둔 것을 정리한 한국인의 블로그를 확인해봤다.
    access 토큰 사용자가 아닌 발급자의 입장에서 생각하게 되면, 토큰의 유효기간이 아주 긴 경우 시스템에 문제가 생겨 모든 세션을 만료시켜야 하는 경우가 발생했다. 이 때 서버는 오직 토큰만 활용하여 세션을 유지하고 있는 서버이다. 이런 상황에 토큰의 만료기간이 길기 때문에 상당한 시간동안 사용자들은 서버의 API를 사용할 수 있을 것이므로 문제가 될 수 있다고 한다.
    그러나 이는 어차피 JWT은 서명 방식으로 인증을 진행하는데 서명을 할 때 기저가 되는 Key를 변경하면 쉽게 해결 될 것 같다는 생각이 들었지만, 정확한건 없이 모두 나의 예측일 뿐이다.

    3. 해보자 구현!

    pip install flask-jwt-extended

    먼저 라이브러리 부터 install 해준다.

    from flask import Flask, request, jsonify
    from flask_jwt_extended import (
        JWTManager, jwt_required, create_access_token, get_jwt_identity, unset_jwt_cookies, create_refresh_token, jwt_refresh_token_required,
    )
    import Config
    
    app = Flask(__name__)

    Config 파일에 키, 토큰의 유효시간에 대한 정보를 따로 정의해뒀다.

    # Setup the Flask-JWT-Extended extension
    app.config['JWT_SECRET_KEY'] = Config.key
    app.config['JWT_ACCESS_TOKEN_EXPIRES'] = Config.access
    app.config['JWT_REFRESH_TOKEN_EXPIRES'] = Config.refresh
    
    jwt = JWTManager(app)

    JWT_SECRET_KEY는 서명에 사용되는 Key 이고 직접 String으로 정의해주면 된다.
    JWT_ACCESS_TOKEN_EXPIRES는 Access 토큰의 만료 시간
    JWT_REFRESH_TOKEN_EXPIRES는 Refresh 토큰의 만료 시간이다.
    이는 Access 토큰은 15분이 기본이고, Refresh 토큰은 30일이 기본 설정이다
    설정해줄 때 Access, Refresh 토큰 모두 초단위로 정수로 기입하면 된다.
    해당 내용 Documents

    @app.route('/auth', methods=['POST'])
    def login():
        if not request.is_json:
            return jsonify({"msg": "Missing JSON in request"}), 400
    
        username = request.json.get('username')
        password = request.json.get('password')
        if not username:
            return jsonify({"msg": "Missing username parameter"}), 400
        if not password:
            return jsonify({"msg": "Missing password parameter"}), 400
    
        if username != 'test' or password != 'test':
            return jsonify({"msg": "Bad username or password"}), 401
    
        # Identity can be any data that is json serializable
        access_token = create_access_token(identity=username)
        refresh_token = create_refresh_token(identity=username)
    
        return jsonify(access_token=access_token, refresh_token=refresh_token), 200

    토큰을 발급하는 부분이다. 입력받은 username과 password가 test, test 인 경우 토큰을 발급할 것이다.
    create_access_token(identity=username) – 사용자의 username을 서명하여 Access 토큰을 발급하는 메서드.
    create _refresh_token(identity=username) – 사용자의 username을 서명하여 Refresh 토큰을 발급하는 메서드.

    @app.route('/protected', methods=['GET'])
    @jwt_required
    def protected():
        # Access the identity of the current user with get_jwt_identity
        current_user = get_jwt_identity()
        return jsonify(logged_in_as=current_user), 200

    @jwt_required는 헤더로 수신한 Access 토큰의 유효성을 검증하는 데코레이터이다. 만약 만료 되었거나 유효하지 않은 토큰이라면 인가받지 못했다는 리턴을 확인할 수 있을 것이다.
    get_jwt_identity() 메서드는 현재 유효한 토큰임을 확인했기 때문에 서명된 사용자 이름을 찾을 수 있을 것이다. 그 사용자 이름 즉 식별자 identity를 반환하는 함수이다.

    @app.route('/refresh', methods=['GET'])
    @jwt_refresh_token_required
    def refresh():
        current_user = get_jwt_identity()
        access_token = create_access_token(identity=current_user)
        return jsonify(access_token=access_token, current_user=current_user)

    만약 refresh token을 활용해 토큰을 재발급 한다면 기존의 토큰의 유효시간이 남은 경우 이를 페기하기 위해서 Black-list에 별도로 보관하여 리스트에 있는 토큰으로 접속을 시도한다면 거부해야 한다. 하지만 토큰 시간이 이미 짧다면 굳이 만료시키지 않고 자동으로 만료가 될 것이기 때문에 기존 Access 토큰을 따로 관리하지 않고 완전히 새로운 Access 토큰을 발급하여 돌려준다.
    @jwt_refresh_token_required는 헤더로 수신한 Refresh 토큰의 유효성을 검증하는 데코레이터이다.

    @app.route('/refresh_long', methods=['GET'])
    @jwt_refresh_token_required
    def refreshLong():
        cur_user = get_jwt_identity()
        delta = datetime.timedelta(days=1)
        access_token = create_access_token(identity=cur_user, expires_delta=delta)
        return jsonify(access_token=access_token)

    해당 코드는 글 작성 이후에 새롭게 추가하였다.
    글쓰기 등 기존에 설정해둔 만료 시간보다 긴 만료 시간을 갖는 토큰을 새로 발급해줘야 할 때, 위와 같이 expires_delta 옵션을 통해 토큰 발급시에 만료 시간을 설정할 수 있다.
    또한, expires_delta는 datetime안에 있는 timedelta 메소드를 통해 만들어진 delta 타입의 변수를 매개변수로 갖는다.
    해당 코드는 하루 짜리 토큰을 발급해 준다.

    4. 코드 전문

    from flask import Flask, request, jsonify
    from flask_jwt_extended import (
        JWTManager, jwt_required, create_access_token, get_jwt_identity, unset_jwt_cookies, create_refresh_token, jwt_refresh_token_required,
    )
    import Config
    
    app = Flask(__name__)
    
    # Setup the Flask-JWT-Extended extension
    app.config['JWT_SECRET_KEY'] = Config.key
    app.config['JWT_ACCESS_TOKEN_EXPIRES'] = Config.access
    app.config['JWT_REFRESH_TOKEN_EXPIRES'] = Config.refresh
    
    jwt = JWTManager(app)
    
    
    # Provide a method to create access tokens. The create_access_token()
    # function is used to actually generate the token, and you can return
    # it to the caller however you choose.
    @app.route('/auth', methods=['POST'])
    def login():
        if not request.is_json:
            return jsonify({"msg": "Missing JSON in request"}), 400
    
        username = request.json.get('username')
        password = request.json.get('password')
        if not username:
            return jsonify({"msg": "Missing username parameter"}), 400
        if not password:
            return jsonify({"msg": "Missing password parameter"}), 400
    
        if username != 'test' or password != 'test':
            return jsonify({"msg": "Bad username or password"}), 401
    
        # Identity can be any data that is json serializable
        access_token = create_access_token(identity=username)
        refresh_token = create_refresh_token(identity=username)
    
        return jsonify(access_token=access_token, refresh_token=refresh_token), 200
    
    
    # Protect a view with jwt_required, which requires a valid access token
    # in the request to access.
    @app.route('/protected', methods=['GET'])
    @jwt_required
    def protected():
        # Access the identity of the current user with get_jwt_identity
        current_user = get_jwt_identity()
        return jsonify(logged_in_as=current_user), 200
    
    
    @app.route('/refresh', methods=['GET'])
    @jwt_refresh_token_required
    def refresh():
        current_user = get_jwt_identity()
        access_token = create_access_token(identity=current_user)
        return jsonify(access_token=access_token, current_user=current_user)
    
    
    if __name__ == '__main__':
        app.run()

    Api flask-jwt-extended – documentation

    5. 마치며

    사실 회원가입시에 비밀번호를 암호화 하여 저장하고 암호화된 비밀번호를 다시 찾아서 비교하며 동일한 경우에 토큰을 발급해주는 현재 구성하고 있는 로그인 과정의 모든 내용을 작성하고 싶었으나 시간이 부족하여 여기서 글을 마친다. 그래도 다음에 bcrypt 라이브러리를 활용한 비밀번호 암호화 및 인증 과정에 대한 글을 작성해야겠다.

    'Python' 카테고리의 다른 글

    Python(flask)으로 mongoDB 제어하기 – pymongo  (3) 2021.07.06
Designed by Tistory.