[실습] 도커 실습 10 - Redis을 활용한 이기종 서버간 세션 공유
필기자
2025-11-04 19:10
682
0
본문
목차
- 최종 목표 시스템 구성도
- PHP 서버간 세션 처리 FLOW
- gctask.com php 쿠키 확인
- 파이썬 커스텀 도커 이미지 생성
- fastapi 프레임워크 로그인/로그아웃 app 개발
- php 세션 공유 프로그램
- task app 수정
최종 목표 시스템 구성도
![]()
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에서 동일한 세션 복원 |
| ⑦ | 결과적으로 동일 로그인 상태 유지 | 컨테이너 간 완전한 세션 공유 |







gctask.com php 쿠키 확인
- 크롬으로 http://gctask.com/login.html 접속 후 로그인
- F12로 개발자 모드 접근 -> Application 탭
- 좌측 Cookies -> http://gctask.com 클릭 -> PHPSESSION

파이썬 커스텀 도커 이미지 생성
- 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 허용
- http://gctask.com/user/login?email=aaa@aaa.com&password=2323 세션 생성
- 테스트 이후에는 반드시 get 방식을 post 방식으로 변경

php 세션 공유 프로그램
- redis_session.php 파일 수정
- session_name("GCTASKID"); 추가
- /task/redis_session_get.php 파일 변경
<?php
include_once("./redis_session.php");
session_start(); // 세션 사용하는 페이지 제일 상단 반드시 호출(세션 활성화)
echo "사용자 이름 : {$_SESSION['username']}<br>";
echo "사용자 아이디 : {$_SESSION['useremail']}<br>";


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>

- ~/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>
.
.
.
아래 생략
- 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"]
}
.
.
.
아래 생략

댓글목록0