본문 바로가기
소소한 일상

[Infra] 좌충우돌 메인 로그인 서버 구축하기

by MFDO 2024. 11. 5.

약 두 달 동안의

좌충우돌로 개발했던 나홀로의 싸움을 기록한다.

 

 

 

9월 말

아래 구조와 같이 소비자가 다양한 클라이언트를 활용하는 서비스가 존재한다.

먼저 개별적으로 작업하고 있던 각 개발자 분들에게 공통 DB의 필요성을 제시하였다.

중요성이 받아들여져 이후 10월 논의로 잡혔다.

 

 

 

10월 둘째 주

하지만 1인 기획이라 DB를 구성하기에 빠져있는 내용이 많았기에

기획 담당자 분을 찾아가 서비스를 구체화 하며 DB 초안을 작성했다.

이 과정에서 다국어 서비스에 대한 DB 구성법과 소셜 로그인 다중 계정 지원에 대해 공부했다. 

RDB에 저장되지 않을 요소는 테이블로만 정의하였다.

 

 

Azure 가상환경을 생성하여 최종 배포처를 생성하였다.

추가적으로 mongo와 postgreSQL DB를 구축하였으며,

해당 DB 접근을 위한 GUI 서비스를 연결하여 알렸다.

 

 

 

10월 셋째 주

Spring Boot로 개발을 본격적으로 시작하였다.

Spring Security 경험이 없어 추가적인 플로우를 공부하였다.

레벨링 시스템에서 외부 API가 필요해 조사하였는데,

구현 불가한 조건임이 판별나 레벨 시스템 개편안을 제출했다.

 

 

10월 넷째 주

Spring Security 환경 설정을 마쳤으며,

소셜 로그인 테이블을 작성해 Spring Boot와 연결 하였다.

JWT 토큰 발급 및 해석 코드를 작성했으며

기존 유저(DB 존재 유저)에 대한 구글 로그인 구현을 마쳤다.

추가적으로 유저 생성 구현 및 테스트코드 작성하였다.

이 때는 내가 잘못 생각한 줄 몰랐다.

 

 

10월 다섯째 주

Spring Boot 서버가 로그인 이후 반환값을 웹페이지로 보내려고 한다는 것을 파악했다.

이 설정은 내가 웹 페이지 구축시에만 유효하다는 것을 파악해

로그인 서버로서의 기능으로 대체하고자 클라이언트 코드가 필요함을 알게되었다.

 

이를 위한 전체적인 구조 개편을 이루었으며,

파이썬 클라이언트 코드를 작성하여 테스트를 마무리 지었다.

최종적으로 위와 같은 구조로 로그인 및 JWT 토큰 발급을 마무리 했다.

 

 

 

파이썬으로 구동한 테스트에서도 각 과정을 훌륭히 수행함을 볼 수 있었다.

import os
import requests
import requests
from google.oauth2 import id_token
from google_auth_oauthlib.flow import InstalledAppFlow
import jwt

destination = "localhost:8080" # 로컬

# 클라이언트 ID 및 시크릿 JSON 파일 경로 : Google 로그인 요청을 위해서는 필수적임
CLIENT_SECRETS_FILE = "./client_secret_-.apps.googleusercontent.com.json"  # GCP에서 다운받은 JSON 파일 경로

# OAuth 2.0 범위
SCOPES = ["openid", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"]

# 3) JWT 토큰 검증을 요청합니다. 
def verify_jwt(token):
    
    print("=== JWT 토큰 값을 검증해봅니다. ===")

    # Spring Boot 토큰 검증 서버 URL
    # 요청 값 : Get / token(String) - Spring Boot에서 받은 JWT Token값
    url = "http://" + destination +"/auth/token/verify"
    payload = {
        "token": token
    }

    # JWT를 디코드하고 유효성을 검증합니다.
    # {
    #   "data": {                                                   # token값이 유효하면(= valid값이 true면), 해당 데이터의 각 값이 존재
    #     "userId": 111,                                            # 유저 고유 번호
    #     "userName": "황진주",
    #     "password": "",
    #     "nickname": "MFDO",
    #     "phone": "010-1111-1111",
    #     "email": "mfdo7722@gmail.com",                            # 가입 시 작성한 email임 로그인 계정 email 아님
    #     "profileImageUrl": "https://example.com/profile.jpg",
    #     "birthday": [2000, 4, 2],
    #     "premiumExpiryDate": null,                                # 프리미엄 유저 시에만 해당 값이 존재, 값이 존재해도 기간이 지남 여부는 판별해야함
    #     "suspensionDate": null,                                   # 계정 정지 기한, 해당 기간이 미래면 정지된 유저 상태임
    #     "accountStatus": 0,                                       # 계정 상태 코드, 일반 유저(0) 휴면 유저(1) 탈퇴 유저(2) 
    #     "followingCount": 0,
    #     "followerCount": 0,
    #     "bio": "아이엠그라운드 자기 소개 하기~!",                   # 한 줄 자기소개글
    #     "defaultMotion": "wave",                                  # 페르소나 대화할 때의 기본 모션 (해당 값은 미정으로 추후 변경가능성 존재)
    #     "youtubeUrl": "https://youtube.com/channel/xyz",          # 각 활동 플랫폼 링크, 없으면 "" 값이 올 수 있음
    #     "twitchUrl": "https://twitch.tv/channel/xyz",
    #     "chzzkUrl": "https://chzzk.com/user/xyz",
    #     "afreecaTvUrl": "https://afreecatv.com/user/xyz",
    #     "externalLink": {
    #     "instagram": "http://instagram.com/younghee"              # 그 외의 활동 링크 유저가 key-value 모두 다 입력한 것
    #     },
    #     "tnutBalance": 0,                                         # 현재 보유중인 티넛 수
    #     "createDt": [2024, 10, 22, 20, 1, 59, 199370000],         # 계정 생성일
    #     "updateDt": [2024, 10, 22, 20, 1, 59, 199370000],         # 계정 값 변경일
    #     "enabled": true,                                          # 계정 활성화 임시값 추후 삭제할 예정이라 이용X 
    #     "authorities": [],                                        # 사용자가 가진 권한들을 저장할 것임 현재 미구현
    #     "accountNonExpired": true,                                # data 중 아랫값들은 현재 어떤 일이 있어도 true가 나오도록 해놓았음 (추후개발 - 삭제 가능성 존재)
    #     "credentialsNonExpired": true,                            # (추후개발 - 삭제 가능성 존재)
    #     "accountNonLocked": true,                                 # (추후개발 - 삭제 가능성 존재)
    #     "username": "mfdo7722@gmail.com"                          # (추후개발 - 삭제 가능성 존재)
    #   },
    #   "message": "토큰이 유효합니다.",                              # token 검증 정보에 대한 설명 
    #   "valid": true                                                # token의 유효성 판별, 이 값이 false면 data 값은 존재하지 않음
    # }

    response = requests.get(url, json=payload)
    data = response.json()

    print(response.text)
    if(bool(data.get("valid"))) :
        print("토큰이 유효합니다.\n")
        print("===유저정보===")
        print(data.get("data"))
    else :
        print("유효하지 않은 토큰입니다.")

### 1) 구글 로그인을 시도하여 해당 정보를 가져옵니다.
def get_google_user_info():
    # OAuth 인증 흐름 시작
    flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRETS_FILE, SCOPES)
    # 구글로그인을 시작함 : 웹페이지가 열림
    credentials = flow.run_local_server(port=0)

    # 유저 정보 요청
    token = credentials.token
    user_info_endpoint = "https://www.googleapis.com/oauth2/v3/userinfo"
    headers = {"Authorization": f"Bearer {token}"}
    
    # 구글 로그인 반환값 예제
    # User Information: 
    # { 
    #   'sub': '102894117853501228662', # 구글 서비스에서 제공하는 유저 고유값, 이 값을 Spring Boot에 oauthId에 입력해야함.
    #   'name': '메프도',                # 사용자 이름 
    #   'given_name': '도', 
    #   'family_name': '메프', 
    #   'picture': 'https://lh3.googleusercontent.com/a/ACg8ocJ7HIqj3MhHifNltk5lPPQUX10iQjTqRPlASDRHNTt_H0OqkZ_g=s96-c', # 사용자 프로필 사진
    #   'email': 'mfdo7722@gmail.com',  # 사용자 가입 email
    #   'email_verified': True          # 사용 email 실제 활용 검증 여부
    # }
    response = requests.get(user_info_endpoint, headers=headers)
    user_info = response.json()

    return user_info

### 2) 구글로그인을 통해 가져온 정보(고유 ID, email, 서비스 제공처)를 통하여 JWT 토큰을 받습니다.
def send_user_info_to_spring_boot(user_info):
    # Spring Boot 회원 존재 여부 확인 서버 URL
    # 요청 값 : POST / provider(String), oauthId(String), email(String)
    url = "http://" + destination + "/auth/login/google"

    # 로그인 서비스명, 유저 ID(sub), 이메일을 포함한 데이터 생성
    payload = {
        "provider": "google",         # OAuth 제공처      : 현재는 google, 이후 x, naver, kakao, apple 추가 예정
        "oauthId": user_info["sub"],  # 유저의 고유 식별자 : 구글로그인에서 주는 값, 해당 유저의 고유 번호
        "email": user_info["email"]   # OAuth 가입 email  : 구글로그인에서 주는 값, 유저가 구글 계정에서 쓰는 email 
    }

    # JWT 요청 결과 값을 받아옵니다.
    # 반환 값 : 
    # - exist(Boolean) : 해당 유저의 신규/기존 유저 여부를 밝힙니다. false면 신규 유저임을 알려, 회원가입이 필요함을 의미합니다. true면 기존 유저임을 알리고, jwt 토큰을 발행합니다.
    # - token(String) : JWT 토큰값입니다. 기존 유저인 경우에만 값이 존재합니다.
    # 반환값 예제
    # { 
    #   "token":"eyJhbGciOiJIUzUxMiJ9... 후략",  # exist 값이 true 경우만 해당 값이 존재합니다. false인 경우 빈 내용이 반환됩니다.
    #   "exist":true                             # 회원가입을 한 경우(True), 회원가입을 하지 않은 경우(False) 
    # }
    response = requests.post(url, json=payload)
    
    print("response:", response.text)  # JWT 반환
    print()
    
    # 응답 상태와 내용 확인
    if response.status_code == 200:
        data = response.json()  # JSON으로 변환
        isExist = bool(data.get("exist"))
        print("기존 가입 여부 : ", isExist)
        if(isExist) :
            print("이미 가입한 유저이므로 JWT가 반환됩니다.")
            print("JWT Token:", data.get("token"))
            print()
            # 3) 토큰이 있는 유저이기 때문에 토큰 검증을 통해 정보를 획득합니다.
            verify_jwt(data.get("token"))
        else :
            print("신규 유저이므로 JWT가 반환되지 않으며 회원가입이 필요합니다.")
            print("JWT Token:", data.get("token"))
    else:
        print("Failed to send user info:", response.status_code, response.text)

### 코드 시작 지점
if __name__ == "__main__":
    ### 1) 구글로그인을 시도합니다.
    user_info = get_google_user_info()
    print("===구글 로그인 결과입니다.===")
    print("User Information:", user_info)
    print()

    ### 2) 구글 로그인을 통해 취득한 정보로 token을 획득합니다.
    print("===TIH 서비스 로그인 결과입니다.===")
    send_user_info_to_spring_boot(user_info)

 

 

이를 위해

해당 가상머신에 대한 도메인 등록을 수행하였으며

Spring Boot 서버에 대한 CI/CD를 진행하였다.

 

54번의 시행착오 끝에야 훌륭히 제역할을 해내어주었다.

Jenkins를 이용한 배포에는 나름 익숙해진 듯 하다.

 

 

 

기본 플로우에 대해 진행한 것이라 아질 할 일이 많겠지만

이슈화하여 정리하니 차근차근 진행하는데 도움이 되는 것 같다...

 

 

늘 한 번에 되지 않았던 빌드에 대한 설움

 

댓글