도커 실습 10 - Redis을 활용한 이기종 서버간 세션 공유 > 클라우드네이티브

본문 바로가기

[실습] 도커 실습 10 - Redis을 활용한 이기종 서버간 세션 공유

필기자
2025-11-04 19:10 682 0

본문

목차

  • 최종 목표 시스템 구성도
  • PHP 서버간 세션 처리 FLOW
  • gctask.com php 쿠키 확인
  • 파이썬 커스텀 도커 이미지 생성
  • fastapi 프레임워크 로그인/로그아웃 app 개발
  • php 세션 공유 프로그램
  • task app 수정


최종 목표 시스템 구성도
20251112163907_c0ee6dfabfc9cddfc61c5f9ece40468e_n4td.png

PHP 서버간 세션 처리 FLOW

단계 동작 설명
브라우저에서 /user/login.php 요청 최초 로그인 시도
PHP가 session_start() 실행 새 세션 ID(abcd1234567890ef) 발급
PHP가 세션 데이터를 Redis에 저장 key=session:abcd1234
응답 시 쿠키 전송 Set-Cookie: PHPSESSID=abcd1234; path=/; domain=gctask.com
브라우저가 모든 요청에 쿠키 포함 /task, /user 모두 전송됨
Task 서버도 session_start() 실행 Redis에서 동일한 세션 복원
결과적으로 동일 로그인 상태 유지 컨테이너 간 완전한 세션 공유

20251104192531_c0ee6dfabfc9cddfc61c5f9ece40468e_hjto.png

20251104192551_c0ee6dfabfc9cddfc61c5f9ece40468e_5vuq.png

20251104192627_c0ee6dfabfc9cddfc61c5f9ece40468e_vsex.png

20251104192650_c0ee6dfabfc9cddfc61c5f9ece40468e_ymb1.png

20251104192711_c0ee6dfabfc9cddfc61c5f9ece40468e_0gph.png

20251104192741_c0ee6dfabfc9cddfc61c5f9ece40468e_2fkh.png

20251104192801_c0ee6dfabfc9cddfc61c5f9ece40468e_xsv5.png

gctask.com php 쿠키 확인 20251104193159_c0ee6dfabfc9cddfc61c5f9ece40468e_6hbq.png


파이썬 커스텀 도커 이미지 생성
  • mkdir -p ~/fastapi/user
  • cd ~/fastapi
  • requirements.txt(python 패키지) 설정
    • nano ~/fastapi/requirements.txt


# requirements.txt
# FastAPI 기반 웹 프레임워크
fastapi

# ASGI 서버 (FastAPI 실행용)
uvicorn

# Redis 클라이언트 (세션 저장소)
redis

# MariaDB/MySQL 연결용
pymysql

# HTML form 데이터 파싱용 (await request.form() 사용할 때 필수)
python-multipart

 

  • Dockerfile 파일 생성
    • nano ~/fastapi/Dockerfile


# --------------------------------------------------------
# Python 3.11 경량 이미지 기반 (FastAPI는 Python 3.11 이상 권장)
# --------------------------------------------------------
FROM python:3.11-slim

# --------------------------------------------------------
# 컨테이너 내 작업 디렉토리 설정
# 모든 명령이 /app 폴더 기준으로 실행됨
# --------------------------------------------------------
WORKDIR /app

# --------------------------------------------------------
# Python 패키지 의존성 파일 복사 및 설치
# (requirements.txt는 호스트에서 컨테이너로 복사됨)
# --------------------------------------------------------
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# --------------------------------------------------------
# 코드 파일은 COPY 하지 않음
# (차후 docker run 시 volume 바인딩 마운트로 연결할 예정)
# 예: -v ~/fastapi/user:/app
# --------------------------------------------------------
# COPY . .

# --------------------------------------------------------
# FastAPI 환경 변수 (선택)
# Flask와 달리 FastAPI는 ENV 설정이 필수는 아님
# --------------------------------------------------------
ENV FASTAPI_APP=main.py
ENV FASTAPI_HOST=0.0.0.0

# --------------------------------------------------------
# 컨테이너 외부로 80포트 노출
# uvicorn이 내부 80포트로 서비스하게 설정함
# --------------------------------------------------------
EXPOSE 80

# --------------------------------------------------------
# FastAPI 애플리케이션 실행
# Flask의 "flask run"과 달리 uvicorn을 직접 실행
# --------------------------------------------------------
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]

  • 커스텀 이미지 생성
  • docker build -t myfastapiimage .


fastapi 프레임워크 로그인/로그아웃 app 개발

  • nano ~/fastapi/user/main.py


from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse
import redis
import uuid
import pymysql
import re
app = FastAPI()
# -------------------------------
# Redis 연결 설정
# -------------------------------
r = redis.Redis(host="myredis", port=6379, decode_responses=True)
# -------------------------------
# DB 연결 함수 (PHP 환경과 동일 설정)
# -------------------------------
def get_db():
    return pymysql.connect(
        host="mysql",
        user="php-mysql",
        password="123456",
        database="php-mysql",
        cursorclass=pymysql.cursors.DictCursor
    )
# -------------------------------
# PHP 세션 직렬화 함수
# -------------------------------
def php_session_encode(session_dict: dict) -> str:
    """
    Python dict → PHP 세션 직렬화 문자열 변환
    예: {"useremail": "abc@gctask.com", "username": "홍길동"}
     → useremail|s:14:"abc@gctask.com";username|s:9:"홍길동";
    """
    session_str = ""
    for key, value in session_dict.items():  #인자 이름 일치시킴
        byte_len = len(value.encode("utf-8"))  # UTF-8 바이트 길이 계산
        session_str += f"{key}|s:{byte_len}:\"{value}\";"
    return session_str
# -------------------------------
# PHP 세션 역직렬화 함수
# -------------------------------
def php_session_decode(session_data: str) -> dict:
    """
    PHP 세션 문자열 → Python dict 변환
    예: 'useremail|s:14:"abc@gctask.com";username|s:9:"홍길동";'
     → {'useremail': 'abc@gctask.com', 'username': '홍길동'}
    """
    if not session_data:
        return {}
    return dict(re.findall(r'(\w+)\|s:\d+:"([^"]*)"', session_data))
# -------------------------------
# 세션 쿠키 이름
# -------------------------------
COOKIE_NAME = "GCTASKID"
# ================================================================
# 1. 로그인 API
# URL: http://gctask.com/user/login
# ================================================================
#@app.post("/login")
@app.get("/login")
async def login(request: Request, response: Response):
    """
    PHP 세션 구조(useremail|s:len:"...";username|s:len:"...";)에 맞춰 Redis에 저장
    PHP 서버(taskapi)와 완전 호환.
    """
    email = None
    password = None
    # 1. JSON 또는 Form 방식 구분 없이 데이터 읽기
    '''
    try:
        form = await request.json()
    except:
        form = await request.form()
    email = form.get("email")
    password = form.get("password")
    '''
    # (테스트용 GET 허용 — 개발 중에만 사용)
    email = email or request.query_params.get("email")
    password = password or request.query_params.get("password")
    # 2. 필수값 검증
    if not email or not password:
        return JSONResponse(status_code=400, content={"result": "no", "msg": "이메일 또는 비밀번호 누락"})
    # 3. DB 조회
    conn = get_db()
    with conn.cursor() as cur:
        cur.execute("SELECT * FROM user WHERE email=%s AND pass=%s", (email, password))
        user = cur.fetchone()
    conn.close()
    if not user:
        return JSONResponse(status_code=401, content={"result": "no", "msg": "로그인 정보가 틀립니다."})
    # 4. 세션 ID 생성
    session_id = str(uuid.uuid4())
    # 5. PHP 직렬화 포맷으로 세션 문자열 생성
    session_data = php_session_encode({
        "useremail": email,
        "username": user["name"]
    })
    # 6. Redis 저장 (TTL 1시간)
    r.setex(f"session:{session_id}", 3600, session_data)
    # 7. 쿠키 발급
    response.set_cookie(
        key=COOKIE_NAME,
        value=session_id,
        httponly=True,
        samesite="Lax"
    )
    # 8. 결과 반환
    return {
        "result": "ok",
        "msg": "정상 로그인이 되었습니다.",
        "email": email,
        "username": user["name"]
    }
# ================================================================
# 2. 인증 확인 API
# URL: http://gctask.com/user/check
# ================================================================
@app.get("/check")
async def check_session(request: Request):
    """
    Redis에 저장된 PHP 세션 문자열을 파싱하여
    PHP의 $_SESSION 구조로 복원
    """
    session_id = request.cookies.get(COOKIE_NAME)
    if not session_id:
        return {"result": "no", "msg": "세션 없음"}
    data = r.get(f"session:{session_id}")
    if not data:
        return {"result": "no", "msg": "세션 만료"}
    # PHP 세션 문자열 파싱
    session_vars = php_session_decode(data)
    if "useremail" not in session_vars or "username" not in session_vars:
        return {"result": "no", "msg": "세션 파싱 실패"}
    return {
        "result": "ok",
        "msg": "세션 유지 중",
        "email": session_vars["useremail"],
        "username": session_vars["username"]
    }
# ================================================================
# 3. 로그아웃 API
# URL: http://gctask.com/user/logout
# ================================================================
@app.post("/logout")
async def logout(request: Request, response: Response):
    """
    Redis 세션 제거 + 쿠키 삭제
    """
    session_id = request.cookies.get(COOKIE_NAME)
    if session_id:
        r.delete(f"session:{session_id}")
    response.delete_cookie(COOKIE_NAME)
    return {"result": "ok", "msg": "로그아웃 완료"}

  • 기존 myuserapi 컨테이너 삭제
    • docker rm -f myuserapi
  • fastapi 컨터이너 생성
    • docker run -d --name myuserapi -p 8013:80 --net php-mysql -v ~/fastapi/user:/app myfastapiimage
  • 개발 단계에서 테스트를 위해 get 허용
20251105103320_c0ee6dfabfc9cddfc61c5f9ece40468e_st4g.png

3529946166_K2owDcvB_9c6474192eb0bd31aa606447585bfe6918b205fa.png

php 세션 공유 프로그램

  • redis_session.php 파일 수정
    • session_name("GCTASKID"); 추가
20251105102335_c0ee6dfabfc9cddfc61c5f9ece40468e_ow6f.png
  • /task/redis_session_get.php 파일 변경

<?php
include_once("./redis_session.php");

session_start(); // 세션 사용하는 페이지 제일 상단 반드시 호출(세션 활성화)

echo "사용자 이름 : {$_SESSION['username']}<br>";
echo "사용자 아이디 : {$_SESSION['useremail']}<br>";

3529946166_wl1Sd8NM_159aca73e4cae86f6dd75abed30f722f862e691f.png
20251105105941_c0ee6dfabfc9cddfc61c5f9ece40468e_wgvk.png 20251105105800_c0ee6dfabfc9cddfc61c5f9ece40468e_syvj.png

task app 수정
  • ~/html/login.html


<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>로그인 페이지</title>
<link
    href='https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css'
    rel='stylesheet'>
<link href='#' rel='stylesheet'>
<script type='text/javascript'
    src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>
<style>
::-webkit-scrollbar {
    width: 8px;
}
/* Track */
::-webkit-scrollbar-track {
    background: #f1f1f1;
}
/* Handle */
::-webkit-scrollbar-thumb {
    background: #888;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
    background: #555;
}
body {
    background-color: #f9f9fa;
}
.flex {
    -webkit-box-flex: 1;
    -ms-flex: 1 1 auto;
    flex: 1 1 auto
}
@media ( max-width :991.98px) {
    .padding {
        padding: 1.5rem
    }
}
@media ( max-width :767.98px) {
    .padding {
        padding: 1rem
    }
}
.padding {
    padding: 5rem
}
.card {
    background: #fff;
    border-width: 0;
    border-radius: .25rem;
    box-shadow: 0 1px 3px rgba(0, 0, 0, .05);
    margin-bottom: 1.5rem
}
.card-header {
    background-color: transparent;
    border-color: rgba(160, 175, 185, .15);
    background-clip: padding-box
}
.card-body p:last-child {
    margin-bottom: 0
}
.card-hide-body .card-body {
    display: none
}
.form-check-input.is-invalid ~.form-check-label, .was-validated .form-check-input:invalid
    ~.form-check-label {
    color: #f54394
}
</style>
</head>
<body className='snippet-body'>
    <div id="content" class="flex">
        <div class="">
            <div class="page-content page-container" id="page-content">
                <div class="padding">
                    <div class="row">
                        <div class="col-md-6">
                            <div class="card">
                                <div class="card-header">
                                    <strong>Login to your account</strong>
                                </div>
                                <div class="card-body">
                                    <form>
                                        <div class="form-group">
                                            <label class="text-muted" for="myEmail">Email
                                                address</label><input type="email" class="form-control"
                                                id="myEmail" aria-describedby="emailHelp"
                                                placeholder="Enter email"> <small id="emailHelp"
                                                class="form-text text-muted">We don't share email
                                                with anyone</small>
                                        </div>
                                        <div class="form-group">
                                            <label class="text-muted" for="myPassword">Password</label><input
                                                type="password" class="form-control"
                                                id="myPassword" placeholder="Password"> <small
                                                id="passwordHelp" class="form-text text-muted">your
                                                password is saved in encrypted form</small>
                                        </div>
                                        <button type="submit" class="btn btn-primary">Submit</button>
                                    </form>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script type='text/javascript'
        src='https://stackpath.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.bundle.min.js'></script>
    <script>
        $(function(){
            $.ajax({
                url: '/user/check',
                type: 'GET',
                dataType: "json", // 서버에서 받는 데이터 타입
                success: function(data) {
                    if(data.result == "ok"){
                        alert("로그인이 되어 있습니다.")
                        window.location.href = "/";
                    }else{
                       
                    }
                }
            });  
       
            $("form").on("submit", function(event) {
                event.preventDefault(); // 기본 동작 중단
       
                // 입력 값을 가져옴
                var email = $("#myEmail").val();
                var password = $("#myPassword").val();
       
                // AJAX로 POST 요청을 보냄
                $.ajax({
                    url: "/user/login", // 로그인 처리를 하는 서버의 URL
                    type: "POST",
                    dataType: "json", // 서버에서 받는 데이터 타입
                    data: {
                        email: email,
                        password: password
                    },
                    success: function(data) {
                        // 로그인 성공 시 처리
                        alert(data.msg);
                        console.log(data);
                        if(data.result == "ok"){
                            window.location.href = "/";
                        }
                        console.log("Login successful:", data);
                    },
                    error: function(xhr, status, error) {
                        // 로그인 실패 시 처리
                        console.log("Login failed:", error);
                    }
                });
            });        
           
        });
    </script>
</body>
</html>

20251105123842_c0ee6dfabfc9cddfc61c5f9ece40468e_lg4z.png

 
  • ~/html/index.html

위 생략
.
.
.
    <script src="https://code.jquery.com/jquery-3.3.1.min.js"
        integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
        crossorigin="anonymous"></script>
    <script>
        $(function(){
            $("#logout").click(function(){
                $.ajax({
                    url: '/user/logout', //fastapi logout으로 변경
                    type: 'POST',
                    dataType: "json", // 서버에서 받는 데이터 타입
                    success: function(data) {
                        //const tasks = JSON.parse(response);
                       
                        if(data.result == "ok"){
                            alert(data.msg)
                            window.location.href = "/login.html";
                        }else{
                            alert(data.msg);
                        }
                    }
                });            
            });
        });
    </script>
    <!-- Frontend Logic -->
    <script src="app.js"></script>
.
.
.
아래 생략

20251105124125_c0ee6dfabfc9cddfc61c5f9ece40468e_c5mt.png

  • nano ~/fastapi/user/main.py
    • login 게이트 get 방식을 post 방식으로 변경


위 생략
.
.
.

# -------------------------------
# 세션 쿠키 이름
# -------------------------------
COOKIE_NAME = "GCTASKID"
# ================================================================
# 1. 로그인 API
# URL: http://gctask.com/user/login
# ================================================================
@app.post("/login")
#@app.get("/login")
async def login(request: Request, response: Response):
    """
    PHP 세션 구조(useremail|s:len:"...";username|s:len:"...";)에 맞춰 Redis에 저장
    PHP 서버(taskapi)와 완전 호환.
    """
    email = None
    password = None
    # 1. JSON 또는 Form 방식 구분 없이 데이터 읽기
    
    try:
        form = await request.json()
    except:
        form = await request.form()

    email = form.get("email")
    password = form.get("password")
    
    # (테스트용 GET 허용 — 개발 중에만 사용)
    #email = email or request.query_params.get("email")
    #password = password or request.query_params.get("password")

    # 2. 필수값 검증
    if not email or not password:
        return JSONResponse(status_code=400, content={"result": "no", "msg": "이메일 또는 비밀번호 누락"})
    # 3. DB 조회
    conn = get_db()
    with conn.cursor() as cur:
        cur.execute("SELECT * FROM user WHERE email=%s AND pass=%s", (email, password))
        user = cur.fetchone()
    conn.close()
    if not user:
        return JSONResponse(status_code=401, content={"result": "no", "msg": "로그인 정보가 틀립니다."})
    # 4. 세션 ID 생성
    session_id = str(uuid.uuid4())
    # 5. PHP 직렬화 포맷으로 세션 문자열 생성
    session_data = php_session_encode({
        "useremail": email,
        "username": user["name"]
    })
    # 6. Redis 저장 (TTL 1시간)
    r.setex(f"session:{session_id}", 3600, session_data)
    # 7. 쿠키 발급
    response.set_cookie(
        key=COOKIE_NAME,
        value=session_id,
        httponly=True,
        samesite="Lax"
    )
    # 8. 결과 반환
    return {
        "result": "ok",
        "msg": "정상 로그인이 되었습니다.",
        "email": email,
        "username": user["name"]
    }
.
.
.
아래 생략


20251105125214_c0ee6dfabfc9cddfc61c5f9ece40468e_mcnu.png

20251105125250_c0ee6dfabfc9cddfc61c5f9ece40468e_qro7.png

20251105125306_c0ee6dfabfc9cddfc61c5f9ece40468e_ofz8.png

댓글목록0

등록된 댓글이 없습니다.
게시판 전체검색