기존에는 클라이언트의 세션스토리지에서 로그인 상태를 확인했었다. 백엔드 서버에서는 클라이언트의 세션스토리지에 접근할 수 없기 때문에 로그인 토큰은 발행 해 줄 수 있었지만 백엔드 쪽에서 로그아웃은 구현할 수 없었다.
redis를 활용해서 백엔드 서버에서 로그아웃을 구현해보자
[1] engine = get_db_connection()
[2] redis_connection = get_redis_connection()
[3] random_key = RandomKey()
try:
Session = sessionmaker(bind=engine)
session = Session()
[4] user_info_db = session.query(User.password, User.id).filter(User.email == user_info['email']).all()
if len(user_info_db) == 0:
return jsonify({'message': 'USER_NOT_EXISTS'}), 400
[5] elif bcrypt.checkpw(user_info['password'].encode('utf-8'), user_info_db[0][0].encode('utf-8')):
[6] token = jwt.encode({'id': user_info_db[0][1],
'exp': datetime.utcnow() + timedelta(days=6)},
SECRET['secret_key'], algorithm=SECRET['algorithm'])
# store redis key in db
[7] random_name = str(uuid.uuid4())
[8] random_key.key = random_name
[9] session.add(random_key)
# store key and token in redis
[10] redis_connection.set(random_name, token)
# commit session after all tasks done
[11] session.commit()
[1] 데이터베이스 엔진을 열어준다
[2] redis 연결을 열어준다
[3] user_dao.py에 있는 RandomKey 클래스의 인스턴스를 만들어 값을 넣을 준비를 한다
[4] 로그인을 하기위해 서버로 들어온 유저정보를 통해서 유저 id와 password를 가져온다
[5] 데이터베이스에서 가져온 해시상태의 패스워드와 로그인을 위해 서버로 들어온 패스워드를 bcrypt checkpw를 통해서 확인한다
[6] 두 패스워드가 일치한다면 유저정보를 가지고 토큰을 만든다. 토큰의 유효기간은 6일로 지정한다. 현재시각(datetime.utcnow())에다가 timedelta를 더해서 유효기간을 정한다
[7] redis 저장소 안에서의 key를 생성한다
[8] 생성한 redis용 key를 데이터베이스에 저장한다. 데이터베이스에서는 redis용 key만을 가지고있는다
[9] RandomKey 클래스의 인스턴스를 데이터베이스에 넣는다
[10] redis에 [7]에서 생성한 key에 [6]에서 생성한 토큰을 value로 해서 저장시킨다
[11] redis에 저장하는 과정이 성공적으로 끝나면 session을 commit 해서 데이터베이스에 [9]에서의 결과를 반영한다. session commit을 제일 마지막에 해줌으로 인해서 redis에 저장하려는 데이터가 저장되지 않은 상태에서 redis key가 데이터베이스에 저장되는 것을 막아준다.
데이터베이스에 redis 접근 key를 저장하는 이유는 로그아웃 기능 때문이다. 로그아웃 기능을 살펴보자.
engine = get_db_connection()
redis_connection = get_redis_connection()
try:
Session = sessionmaker(bind=engine)
session = Session()
[1] if not session.query(exists().where(RandomKey.key == token_info['key'])).one()[0]:
return jsonify({'message': 'INVALID_KEY'}), 400
except Exception as e:
print(e)
return jsonify({'message': e}), 500
finally:
try:
session.close()
except Exception as e:
print(e)
return jsonify({'message': 'SESSION_CLOSE_ERROR'}), 500
key = token_info.get('key', None)
access_token = token_info.get('token', None)
if (not key) or (not access_token):
return jsonify({'message': 'INAVLID_REQUEST'}), 400
# generate expired token
[2] payload = jwt.decode(access_token, SECRET['secret_key'], algorithm=SECRET['algorithm'])
[3] payload['exp'] = datetime.utcnow()
[4] logged_out_token = jwt.encode(payload, SECRET['secret_key'], algorithm=SECRET['algorithm'])
# replacing value on allocated key to expired token
[5] redis_connection.set(key, logged_out_token)
return jsonify({'message': 'SUCCESS'}), 200
[1] 데이터베이스에 로그인할 때 넣은 redis key가 없으면 애러를 리턴한다. 여기서 걸리지지 않으면 redis에 기존에 있던 key에 새로운 value를 치환하는 것이 아니라 새로운 key-value가 생성된다
[2] 유저에게 받은 로그인 토큰을 decode한다
[3] 유저정보에 있는 expiration date를 현시각으로 치환한다
[4] 기간이 만료된 토큰을 만든다
[5] redis에 기간 만료 토큰을 기존에 있던 key-value에 치환해준다
로그아웃 기능을 알기 위해서는 토큰의 역할을 분명히 알아야한다.
- 로그인 시 서버는 클라이언트에게 토큰을 발행 한다
- 클라이언트는 그 토큰을 세션스토리지에 저장한다.
- 토큰이 필요한 api를 호출할 때 마다 세션스토리지에서 토큰을 가져와 request headers에 실어보낸다
- 로그아웃 시 세션스토리지에서 토큰을 삭제한다
세션스토리지를 활용하면 프론트엔드 서버에서 로그아웃을 세션스토리지에서 토큰 삭제를 통해서 구현한다.
백앤드서버에서 로그아웃을 구현하려면 어떻게 해야할까?
정답은 세션스토리지를 백앤드 서버가 접근할 수 있는 곳으로 옮기는 것이고 대표적인 key-value nosql 데이터베이스인 redis를 통해서 구현가능하다.
redis는 value에 해당하는 key를 할당해서 key를 통해서 value에 접근할 수 있다. 이제 프론트엔드는 세션스토리지 대신 자바스크립트 객체를 redis에 저장한다. redis를 통한 로그인 로그아웃 과정을 알아보자
- 로그인에 필요한 데이터를 백앤드 서버로 보낸다
- 백엔드 서버로 들어온 로그인정보를 확인해서 데이터베이스 정보와 일치하면 토큰을 생성한다
- redis에 랜덤한 key를 만들고, 그 key에 토큰을 붙혀서 저장시킨다
- 로그인에 성공해서 토큰을 발급할 때 직접 클라이언트에게 토큰을 주지않고 세션스토리지 대용으로 사용하는 redis에 랜덤이름의 key(uuid) + 토큰을 넣고, uuid와 토큰을 리턴한다. 클라이언트는 받은 토큰을 세션스토리지에 저장하지 않고 redis에서 꺼내쓴다.
- 클라이언트는 권한이 필요한 api를 요청할 때는 redis에 로그인 할 때 받은 key를 통해서 토큰을 가져와서 사용한다
- 그리고 로그아웃 api를 호출할 때 request body로 로그인 시 받은 key(uuid)와 토큰을 보낸다
- 백앤드 서버에선느 토큰을 decode해서 expiration date를 현시각으로 바꿔서 만료토큰을 만든다
- 클라이언트에게서 받은 key로 redis에 있는 로그인 토큰을 만료토큰으로 치환한다
이제 클라이언트가 로그아웃을 한 상태에서 redis에서 토큰을 가져와서 권한이 필요한 api를 호출하면 만료토큰을 사용할 것이고, 백앤드서버에서 만료토큰은 유효성검사에서 걸러지게 된다.