Programming/Project Log

[가계부 만들기] Backend - 회원 관리 기능 #4

minarae7 2023. 4. 7. 13:41
728x90
반응형

이제 사용자가 전달한 값을 통해서 실제로 Database에 연동하는 코드를 넣을 것이다.

서비스 디렉토리 생성

먼저 비즈니스 로직을 담당하는 파일들을 모아둘 디렉토리를 생성하고 __init.py__ 파일을 추가해둔다.

그리고 회원 관련 로직을 작성할 members_service.py 파일을 생성한다.

이제 구조를 만들었으니 이제 내용을 작성해보자.

외부 파일 참조 및 변수 선언

from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.sql import func
from datetime import timedelta
from ..database import models, schemas
from ..libraries import auth

ACCESS_TOKEN_EXPIRE_MINUTES = 60
REFRESH_TOKEN_EXPIRE_HOURS = 24

 members_service.py 파일에서 참조하는 라이브러리 및 외부 함수를 먼저 불러온다.

그리고 JWT의 유효시간을 선언해둔다. access_token은 한 시간, refresh_token은 하루로 유효시간을 설정했다.

Backend와 Frontend를 분리해서 개발하면 서버에서 session을 사용하기가 어렵기 때문에 JWT를 사용한다.

로그인할 때 내부적으로는 access_token과 refresh_token을 생성해서 전달하고 Frontend에서 데이터를 요청할 때는 항상 access_token을 헤더에 붙여서 보내도록 한다.

이 때 인증 만료 시간인 한 시간이 지나버리면 refresh_token을 통해서 새로운 access_token을 자동으로 받아오고 새로 발급받은 access_token을 이용해서 통신하게 된다. 여기에 사용하는 시간을 변수로 미리 설정해둔다.

728x90

회원가입

async def create_member(db: Session, member: schemas.MemberCreate):
    # 아이디가 중복되는 계정이 있는지 확인
    stmt = select(models.Members.member_id).filter(models.Members.member_id == member.member_id)
    result = db.execute(stmt)

    list = result.fetchall()
    if len(list) > 0:
        raise Exception("아이디가 이미 사용 중입니다")

    # 패스워드 해싱 처리
    password_hash = auth.get_password_hash(member.member_pw)

    # DB 저장
    db_member = models.Members(**member.dict(exclude={"member_pw"}), member_pw=password_hash)
    db.add(db_member)
    db.commit()
    db.refresh(db_member)

    return db_member

회원 가입을 할 때는 먼저 입력한 아이디가 다른 사용자가 사용중인지 먼저 확인하여야 한다. 만약 다른 사용자가 해당 아이디를 이미 점령해서 사용 중이라면 Exception을 발생시키고 더 이상 진행하지 않는다.

아이디가 사용할 수 있다면 이제 패스워드를 해시처리해서 디비에서 패스워드를 확인할 수 없도록 한다.

마지막으로 해당 정보를 테이블에 저장하고 저장된 정보를 반환하도록 한다.

db_member = models.Members(**member.dict(exclude={"member_pw"}), member_pw=password_hash)

이 코드에서는 member를 models.Members에 연결하는데 exclude를 넣어서 암호화되지 않은 패스워드는 제외하고 member_pw=password_hash에서 패스워드는 해쉬처리된 코드로 변경하도록 한다.

코드를 작성했으니 테스트를 해볼 수 있다. http://localhost:8000/docs 페이지로 이동해서 /members/create 라우팅을 다음과 같이 테스트해볼 수 있다.

회원 가입 라우팅 테스트

이렇게 테스트를 진행하면 다음과 같은 결과를 얻을 수 있다.

호출 결과

정상적으로 처리 되었고 디비를 보면 실제로 값이 잘 들어갔고 패스워드도 암호화된 상태로 들어간 것을 확인할 수 있다.

테이블 내용

반응형

로그인

회원 가입이 되었으니 이후의 내용들은 다 로그인한 상태에서 진행된다. 그래서 먼저 로그인을 먼저 구현하도록 한다.

async def login_proc(db: Session, member_id: str, member_pw: str):
    # 해당 아이디가 있는지 찾는다/
    stmt = select(models.Members).filter(models.Members.member_id == member_id, models.Members.is_deleted == 'F')
    result = db.execute(stmt)

    db_member = result.fetchone()
    if db_member is None:
        raise Exception("해당하는 아이디를 찾을 수 없습니다")

    if auth.verify_password(member_pw, db_member.Members.member_pw) == False:
        raise Exception("패스워드가 일치하지 않습니다.")

    data = {
        "member_no": db_member.Members.member_no,
        "member_id": db_member.Members.member_id,
        "member_name": db_member.Members.member_name,
        "member_email": db_member.Members.member_email
    }
    data["access_token"] = auth.create_access_token(data, timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
    data["refresh_token"] = auth.create_access_token({
        "member_no": data["member_no"],
        "member_id": data["member_id"]
    }, timedelta(hours=REFRESH_TOKEN_EXPIRE_HOURS))

    return data

아이디와 패스워드를 받아서 회원 정보 테이블에서 조회한다. 아이디가 없으면 Exception 처리한다.

그리고 입력한 패스워드와 테이블의 패스워드를 비교해서 일치하지 않으면 Exception 처리한다.

모두 정상적으로 처리 되었다면 access_token과 refresh_token을 생성하여 반환한다.

마찬가지로 docs 페이지에서 테스트를 해보면 아래와 같다.

로그인 테스트

json 형태로 아이디와 패스워드를 전송한다.

로그인데 대한 결과

로그인에 성공하면 위의 이미지와 같이 access_token과 refresh_token 값이 로그인 정보에 포함되어서 전송된다.

여기서 결과로 온 access_token과 refresh_token은 frontend에서 저장하여서 사용하면 된다. 기본적인 통신은 모두 access_token을 통해서 사용하면 된다.

회원 정보 수정

이제 회원 정보를 수정하는 코드를 작성할 것이다.

JWT 안에 기본적인 회원 정보를 저장하고 있기 때문에 기존의 회원 정보를 다시 보낼 필요없고 jwt 안에서 꺼내쓸 수 있다.

async def member_modify(db: Session, member: schemas.MemberModify, payload: schemas.JWTPayload):
    # 회원이 존재하는 아이디인지 확인
    db_member = db.query(models.Members).filter_by(member_no = payload['member_no'], is_deleted = 'F').first()

    if db_member is None:
        raise Exception("해당하는 회원 정보를 찾을 수 없습니다.")

    member_info =  member.dict()
    member = {k: v for k, v in member_info.items()}
    for key, value in member.items():
        if value is None:
            continue

        if key == 'member_pw':
            setattr(db_member, key, auth.get_password_hash(value))
        else:
            setattr(db_member, key, value)

    db.commit()
    return db_member

코드는 매우 간단하다. 우선 jwt에 포함된 회원 정보에서 회원 번호를 꺼내는데 이게 실제로 존재하는지 먼저 확인한다.

확인되었다면 수정을 원하는 컬럼을 하나씩 변경하도록 한다. 이 때 패스워드를 변경하는 경우에는 해당 값은 hash 처리해서 저장하도록 한다.

변경이 끝났다면 변경된 내용을 저장하기 위해서 commit을 호출하고 변경된 회원 정보를 반환하도록 한다.

만약에 변경을 원하지 않는 컬럼은 보내지 않으면 된다.

회원 정보를 변경할 때는 request 헤더에 로그인할 때 발급받은 access_token을 포함하여야 한다. 이 때 값의 형태는 "Bearer ...." 이와 같이 앞에 Bearer를 붙여 주어야 한다.

docs에서 테스트한 페이지를 보면 다음과 같다.

회원 수정 테스트

이미지를 보면 다른 이미지와 달리 parameters 항목이 추가된 것을 볼 수 있다. token 값에 JWT 값을 추가하면 된다.

실행 결과는 다음 이미지와 같다.

회원 정보 수정 결과

마찬가지로 request를 보낼 때 curl에서 헤더에 token이 추가된 것을 확인할 수 있다.

회원 탈퇴

마지막으로 회원 탈퇴를 구현한다.

async def member_delete(db: Session, payload: schemas.JWTPayload):
    # 회원이 존재하는 아이디인지 확인
    db_member = db.query(models.Members).filter_by(member_no = payload['member_no'], is_deleted = 'F').first()

    if db_member is None:
        raise Exception("해당하는 회원 정보를 찾을 수 없습니다.")

    setattr(db_member, 'is_deleted', 'T')
    setattr(db_member, 'del_dt', func.now())

    db.commit()
    return db_member

회원 탈퇴는 매우 간단하다. 회원 번호가 넘어오면 해당하는 회원 정보를 찾아서 삭제 플래그를 변경해주고 삭제 시간을 저장한다.

삭제할 때 실제로 테이블에서 delete해서 처리하는 것이 올바른 방향이나 실제로 일을 하다가보면 그렇게 바로 삭제하는 경우는 많지 않다.

변경된 히스토리를 찾아야하는 경우도 있고 드물고 잘못 삭제해서 다시 복원해야 하는 경우도 있기 때문에 최초에 탈퇴시에는 일단 플래그만 변경하도록 한다.

회원 탈퇴도 정보 수정과 마찬가지로 jwt 값을 헤더에 추가해서 보내며 어차피 회원 정보는 jwt에 포함되기 때문에 따로 전송하지 않는다.

회원 탈퇴 테스트
호출 결과
테이블 결과

호출이 이루어지고 나면 테이블에서 is_deleted 값이 T로 변경된다. 이렇게 되면 해당 회원은 탈퇴된 것으로 간주된다.


이제 간단한 회원 관련 기능에 대한 기능 구현이 끝났다. 아직 구현하지 않은 내용은 refresh_token을 통해서 access_token을 갱신하는 코드인데 해당 코드는 다음 포스팅에서 다루도록 하겠다.

refresh_token을 통한 인증까지 마무리되면 가계부에 대한 기능 구현을 시작할 수 있을 수 있을듯 하다.

지금까지 구현한 내용 및 앞으로 구현할 내용은 개인 github에 지속적으로 업데이트할 예정이니 아래 주소에서 확인이 가능하다.

https://github.com/minarae/accountbook_backend

 

GitHub - minarae/accountbook_backend

Contribute to minarae/accountbook_backend development by creating an account on GitHub.

github.com

 

728x90
반응형