본문 바로가기
Python/FLASK

인스타그램 클론코딩하기

by sophia02 2022. 11. 6.

개요

현재까지 구현한 인스타그램 API는 로그인 한 사용자라면 누구나 게시물을 작성할 수 있고, 삭제할 수도 있습니다.

A 유저가 B 유저의 게시물을 삭제한다면 문제가 생깁니다. 따라서 게시물을 작성한 본인만 삭제가 가능하게끔 구현하려고 합니다.

  1. get_jwt_identity() 로 요청을 보낸 사용자의 username을 받아옴
  2. 받아온 username을 가진 사용자의 id를 구함
  3. 삭제를 원하는 게시물의 유저 id를 구함
  4. 요청을 보낸 사용자 id와 게시물 작성자의 id를 비교함
  5. 같다면 삭제하고, 다르다면 삭제하지 못함

토큰

resource.user.py에 해당 내용을 import 해줍니다

그 후 클래스를 작성해줍니다.

또한 잘못된 비밀번호를 전달했을 경우에는 401 에러가 나게 코드를 적어주었습니다.

 

이때 클라이언트가 보낸 비밀번호 정보가 데이터베이스에 있는 유저 정보의 비밀번호와 일치한다면 리프레시 토큰과 액세스 토큰을 발급해 줍니다.

 

api/__init__.py에 맨 아랫줄을 적어줍니다


JWT 토큰 발급 후 jwt가 필요한지 아ㅕ주는 데코레이터를 적어줍니다 (jwt_required())

api/__init__.py에

  @jwt.expired_token_loader
    def expired_token_callback(jwt_header, jwt_payload):
        """
        토큰이 만료되었을 때의 에러 메시지를 지정합니다.
        """
        return (
            jsonify({"Error": "토큰이 만료되었습니다."}),
            401,
        )

    @jwt.invalid_token_loader
    def invalid_token_callback(error):
        """
        토큰이 잘못된 값일 때의 에러 메시지를 지정합니다.
        """
        return (
            jsonify({"Error": "잘못된 토큰입니다."}),
            401,
        )

    @jwt.unauthorized_loader
    def missing_token_callback(error):
        """ """
        return (
            jsonify(
                {
                    "Error": "토큰 정보가 필요합니다.",
                }
            ),
            401,
        )

해당 코드를 작성해줍니다.

 

create app()에도

 

에러 메시지를 한글로 보기 위해 이 코드를 작성해줍니다.

 

마지막으로 post와 put, delete에 @jwt_required를 통해 jwt 토큰이 필요하다고 알려줍니다!

postman에서 테스트를 해봅니다

babo를 회원가입 시킨 뒤

글을 작성하려 하면 토큰 정보가 필요하다는 에러 메시지를 받게 됩니다.

 

따라서 로그인을 해 줍시다!

토큰이 발급되었습니다!

이때 access token을 복사해줍니다.

그후, posts의 Authorization에 Bearer Token을 선택해준 뒤 Token에 복사한 값을 넣어줍니다

짠~~ 게시물이 만들어 졌습니다~!

겟 요청을 보내면 작성한 게시물이 보이게 됩니다!

 

이번에는 리프레시 토큰 발급을 구현해 보겠습니다!

방금 사용했던 access token에는 유효시간이 있습니다. 따라서 유효시간이 지난 토큰으로는 로그인이 불가능하고, 액세스 토큰을 다시 발급받기 위해 refresh token을 사용하게 됩니다. 그리고 데이터베이스에 그 값을 저장하는 과정까지 구현해 보고자 합니다!

 

간단한 과정은

1. 전송받은 액세스 토큰 만료 => 액세스 토큰이 만료되었다는 에러 메시지, 401에러 응답

2. 액세스 토큰 만료 응답을 받으면, 클라이언트는 서버에세 리프레시 토큰을 전송

3. 리프레시 토큰을 받은 서버는, 그것을 검증 및 액세스 토큰과 새로운 리프레시 토큰을 발급하며, 그것을 데이터베이스에 저장

4. 클라이언트는해당 액세스 토큰을 받아서 다시 요청

5. 서버는 로그인한 계정이 어떤 계정인지를 알게됨

 

의 흐름으로 진행됩니다.


리프레시 테이블을 저장하기 위해 models.user.py에 테이블을 하나 만들어 줍니다

class RefreshTokenModel(db.Model):
    __tablename__ = "RefreshToken"

    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey(
        "User.id", ondelete="CASCADE"), nullable=False)
    user = db.relationship("UserModel", backref="token")
    refresh_token_value = db.Column(
        db.String(512), nullable=False, unique=True)

    def save_to_db(self):
        """
        토큰을 데이터베이스에 저장
        """
        db.session.add(self)
        db.session.commit()

    def delete_from_db(self):
        """
        토큰을 데이터베이스에서 삭제
        """
        db.session.delete(self)
        db.session.commit()

이후 flask db migrate => flask db upgrade를 수행해줍니다.

 

새 테이블이 생겼다면 리프레시 토큰을 발급할 때 그것이 데이터베이스에 업데이트 되도록 하겠습니다

이제 새로운 액세스 토큰을 발급해 줍시다

models.user.py RefreshTokenModel 아래에 메서드를 작성해줍니다

데이터베이스에 리프레시 토큰이 존재한다면 그에 맞는 사용자를 가져오고, 그렇지 않다면 None을 리턴합니다.

class RefreshToken(MethodView):
    """
    Refresh Token 을 받아 검증하고,
    새로운 Refresh Token, Access token 을 발급합니다.
    Refresh Token 은 일회용이므로, 새로운 Refresh Token 이 발급되면
    데이터베이스에 그 값을 저장합니다.
    """

    @jwt_required(refresh=True)
    def post(self):
        """
        -> refresh token 은 이미 검증된 상태라고 가정 (틀린 토큰, 만료된 토큰 X)
        -> 해당 유저가 데이터베이스에서 가지고 있는 refresh token 과 요청으로 들어온 refresh token이 다르다면,
        -> access token 발급은 실패해야 함
        """
        identity = get_jwt_identity()
        token = dict(request.headers)["Authorization"][7:]
        user = RefreshTokenModel.get_user_by_token(token)
        if not user:
            return {"Unauthorized": "Refresh Token은 2회 이상 사용될 수 없습니다."}, 401
        # access token, refresh token 발급
        access_token = create_access_token(fresh=True, identity=identity)
        refresh_token = create_refresh_token(identity=user.username)
        if user:
            token = user.token[0]
            token.refresh_token_value = refresh_token
            token.save_to_db()
            return {"access_token": access_token, "refresh_token": refresh_token}, 200

Resources.User 아래에 위의 코드를 작성해줍니다. 새 리프레시 토큰과 액세스 토큰을 발급하고 리프레시 토큰을 업데이트 합니다.

 

만약 이 곳에 만료되거나 데이터베이스에 존재하지 않는 리프레시 토큰을 전송했다면, 서버는 다시 401응답을 보냅니다.

=> 클라이언트는 다시 비밀번호와 이베일을 입력함으로서 로그인을 진행


이제 api.__init__.py에 아래의 코드를 작성함으로서 액세스 토큰과 리프레시 토큰의 유효시간을 정해줍니다.

RefreshToken 리소스를 등록해줍니다.

/refresh/에 리프레시 토큰을 담아 전송하면, 새 리프레시 토큰과 액세스 토큰을 발급해줍니다.

 


 

로그인을 진행하고 두 종류의 토큰을 발급받은 뒤 리프레시 토큰을 복사해줍니다.

 

리프레시로 요청하게 되면, 새로운 액세스 토큰과 리프레시 토큰을 발급받게 됩니다.

그리고 이 리프레시 토큰을 복사해줍니다.

만약 한번 더 요청을 보낸다면

2회 연속 사용이 불가능하다는 오류 메시지를 보게 됩니다.

아까 복사해둔 리프레시 토큰을 담고 요청을 보내보면

다시 액세스 토큰과 리프레시 토큰을 발급받을 수 있게 됩니다.

 


게시물 작성 시 저자가 자동으로 추가되도록 구현하기

 

지금까지 로그인 처리를 구현했고, 이제는 게시물 추가 시 작성자가 자동으로 추가되도록 구현해 봅시다.

우리가 만든 posts 는 우리가 저자의 id를 입력해 줘야 했지만, 이제는 로그인 한 사람의 저자 id를 자동으로 추가되도록 만들어 볼 겁니다

schemas.post.py 아래의 PostSchema 클래스의 메타 클래스를 위와 같이 수정해줍니다.

이제 서버는 제목과 내용만 받을 수 있게 되며, 게시물 저장에 대한 로직은 resources에서 진행되므로

resources.post.py를 수정해 줍니다.

이렇게 수정해 주었습니다!

클라이언트는 게시물을 작성할 때에 우리에게 JWT를 보내주게 되며, 우리는 이를 통해 유저 이름을 알아낼 수 있습니다.

 


게시물 수정 시 저자 본인만 게시물을 수정하고, author_id를 보내지 않아도 작동할 수 있도록 구현하기

models.post.py에서 게시물을 수정하기 위한 update_to_db() 메서드를 작성하겠습니다.

 data라는 인자를 입력받습니다. 그리고 그것의 자료형은 딕셔너리 일 것이라고 가정합니다.

이후, data.items() 로 딕셔너리의 키, 값을 가져올 수 있습니다. 이때 반복문이 도는 동안 setattr() 메서드를 사용해 해당 게시물의 ()라는 속성을 ()로 설정한다와 같은 로직을 사용합니다.

 

@classmethod
    @jwt_required()
    def put(cls, id):
        """
        게시물의 전체 내용을 받아서 게시물을 수정
        없는 리소스를 수정하려고 한다면 HTTP 404 상태 코드와 에러 메시지를,
        그렇지 않은 경우라면 수정을 진행
        """
        post_json = request.get_json()
        # first-fail 을 위한 입력 데이터 검증
        validate_result = post_schema.validate(post_json)
        if validate_result:
            return validate_result, 400
        username = get_jwt_identity()
        author_id = UserModel.find_by_username(username).id
        post = PostModel.find_by_id(id)
        # 게시물의 존재 여부를 먼저 체크한다.
        if not post:
            return {"Error": "게시물을 찾을 수 없습니다."}, 404

        # 게시물의 저자와, 요청을 보낸 사용자가 같다면 수정을 진행할 수 있다.
        if post.author_id == author_id:
            post.update_to_db(post_json)
        else:
            return {"Error": "게시물은 작성자만 수정할 수 있습니다."}, 403

        return post_schema.dump(post), 200

resource/post.py의 Post 클래스의 put 메서드를 위와 같이 수정해 줍니다

 

3번 게시물을 수정해주겠습니다

put을 사용해 수정해 주었습니다

만약 데이터 베이스에 없는 4번 게시물을 수정하겠다는 요청을 보내면

게시물을 찾을 수 없다는 에러 메시지를 띄워 줍니다.

만약 다른 유저의 토큰을 가지고 게시물을 수정하려고 한다면

위와 같은 에러를 보여줍니다.

 

이 글은 https://gdsanadev.com/15375 를 참고해 만들었습니다

'Python > FLASK' 카테고리의 다른 글

Flask - 인스타그램 클론코딩(댓글 수정, 삭제 구현)  (0) 2022.11.20
플라스크 - WSGI?  (0) 2022.11.13
오류 모음집  (0) 2022.10.09
직렬화/ 역직렬화 처리하기  (0) 2022.09.12
간단한 HTTP API 구축해보기  (0) 2022.08.24