728x90
반응형

계기

  • 이전에 만들었던 프로젝트들을 리팩토링 중, 검증 방법이 헤더의 Authorization 으로 토큰 검증을 하게 해 뒀던 걸 발견했다.
  • 뭐 잘못된 방법은 아니지만, 쿠키를 활용해서 검증하는 방법으로 변경하였다.

개요

  • 로그인 요청을 받았을 때, 이메일/패스워드 정보를 검증 후 유저 정보를 Redis 서버에서 체크한다.
    • Redis 서버에 해당 정보가 조회될 경우, 새로운 환경에서 로그인 요청을 한 것으로 판단한다.
      • 이전 로그인은 종료시키기 위해, 해당 정보를 삭제한다.
    • 조회된 정보가 없다면 이전 로그인은 만료된 것이므로 무시한다.
  • 이후 새로운 accessToken과 refreshToken을 생성한다.
    • refreshToken을 상위 키로, 하위 키로는 유저 id 로 삼아 로그인 정보를 세팅한다.
      • 키: refresh:{refreshToken}:{userId}
  • 해당 키들을 응답 쿠키에 각각 도메인, 만료시간, 토큰, secure, httpOnly 를 세팅한다.
    • accessToken의 만료 시간은 한시간, refreshToken의 만료 시간은 일주일로 세팅한다.
  • 이후 로그인 및 유저 정보가 필요한 요청은 미들웨어에서 토큰 검증 및 데이터 추출하여 사용한다.
  • 만료시간이 끝난 경우, 토큰 갱신 요청을 날려서 새로운 access token을 발급 받는다.

코드 구현

  • 백엔드는 golang, 프론트는 nextjs app router를 사용하였다.

백엔드

  • 주요 로직은 주석처리, 토큰 세팅 및 쿠키 세팅하는 로직만 유지
// 컨트롤러가 아닌, 비즈니스 로직을 담은 서비스
func LoginService(request LoginRequest, response http.ResponseWriter, request *http.Request) {    
    /* 
        DB 에서 유저 정보 조회 및 패스워드 매칭
    */

    // Access Token 생성
    token, tokenErr := auth.CreateAccessToken(userData.UserId, uuid.String(), userData.Email, fmt.Sprintf("%d", userData.UserStatus), fmt.Sprintf("%d", userData.IsAdmin), time.Hour)

    if tokenErr != nil {
        log.Printf("[LOGIN] Create Token Error: %v", tokenErr)

        return LoginResponse{
            Status:  http.StatusInternalServerError,
            Code:    "ULG09",
            Message: "Create JWT Error",
        }
    }

    // Refresh Token 생성
    refreshToken, refreshTokenErr := auth.CreateRefreshToken("",, time.Hour*24*7)

    if refreshTokenErr != nil {
        log.Printf("[LOGIN] Create Refresh Token Error: %v", refreshTokenErr)

        return LoginResponse{
            Status:  http.StatusInternalServerError,
            Code:    "ULG09",
            Message: "Create JWT Error",
        }
    }

    // Redis 서버에서 유저 정보 조회 - 이전 로그인 만료 여부 체크
    userDataFromRedis, getRedisErr := GetRefreshUserInfoFromRedis(refreshToken, userData.UserId)

    if getRedisErr != nil {
        return LoginResponse{
            Status:  http.StatusNotFound,
            Code:    "ULG08",
            Message: "Get User Data from redis Error",
        }
    }

    // 유저 정보가 조회되었을 시
    if (userDataFromRedis != LoginUserInfo{}) {
        // 조회된 Redis 정보 삭제
        delerr := DeleteAlreadySetToken(refreshToken, userData.UserId)

        if delerr != nil {
            return LoginResponse{
                Status:  http.StatusInternalServerError,
                Code:    "ULG08",
                Message: "Delete Already Set Data from redis Error",
            }
        }
    }

    // Redis 서버에 새로운 로그인 정보 세팅
    setErr := SetRefreshToken(refreshToken, userData.Email, fmt.Sprintf("%d", userData.UserStatus), userData.UserId, userData.UserName, fmt.Sprintf("%d", userData.IsAdmin))

    if setErr != nil {
        return LoginResponse{
            Status:  http.StatusInternalServerError,
            Code:    "ULG10",
            Message: "Set JWT Error",
        }
    }

    accessTokenCookie := http.Cookie{
        Name:     "accessToken",
        Value:    token,
        Path:     "/",
        Secure:   true,
        HttpOnly: true,
        SameSite: http.SameSiteNoneMode,
    }

    refreshTokenCookie := http.Cookie{
        Name:     "refreshToken",
        Value:    refreshToken,
        Path:     "/",
        Secure:   true,
        HttpOnly: true,
        SameSite: http.SameSiteNoneMode,
    }

    http.SetCookie(response, &accessTokenCookie)
    http.SetCookie(response, &refreshTokenCookie)

    return LoginResponse{
        Status:       http.StatusOK,
        Code:         "0000",
        AccessToken:  token,
        RefreshToken: refreshToken,
        Message:      "Login Success",
    }

프론트

  • App Router의 Server Side Component를 이용해서 쿠키 및 토큰을 관리한다.
    • 로그인 응답으로 토큰을 받고 서버 사이드 쿠키에 세팅한다.
export async function POST(request: NextRequest) {
  // const cookieStore = cookies();

  // 로그인 데이터 확인 - 요청 조건 검증
  const loginSchema = z.object({
    email: z.string().email(),
    password: z.string().min(6).max(16),
  });

  try {
    const _loginData = await request.json();
    const loginData = await loginSchema.parseAsync(_loginData);

    const loginResponse = await login({ ...loginData });

    if (!loginResponse.ok) {
      return NextResponse.json({ message: 'unauthorized' }, { status: 401 });
    }
});

    const response = <LoginResponse>(await loginResponse.json());

    if (response.status !== 200) {
      return NextResponse.json({ message: 'Login Failed' }, { status: 302 });
    }

    // 브라우저에 리턴할 쿠키 설정
    const ONE_HOUR = 60 * 60 * 1000;
    const ONE_WEEK = 60 * 60 * 1000 * 24 * 7;

    const res = NextResponse.json({ message: 'Login Success' }, { status: 200 });

    // 쿠키 설정
    res.cookies.set('accessToken', response.accessToken, {
      secure: true, // 프로덕션 환경에서는 true로 설정
      httpOnly: true,
      domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN,
      expires: new Date(Date.now() + ONE_HOUR),
    });

    res.cookies.set('refreshToken', response.refreshToken, {
      secure: true,
      httpOnly: true,
      domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN,
      expires: new Date(Date.now() + ONE_WEEK),
    });

    return res;
  } catch (error) {
    console.log(error);
    return NextResponse.json({ message: 'server error' }, { status: 500 });
  }
}
  • 서버 사이드 컴포넌트. 받은 쿠키들을 이용해 이후 요청을 한다.
    • 만료시 토큰 리프레시를 한다.

export async function POST(request: NextRequest) {
    try {
        let tokenCookie = request.cookies.get("accessToken");
        let refreshToken = request.cookies.get("refreshToken");

        let cookie: string;
        let refreshCookie: string;

        if (!tokenCookie) {
            /** 
                refresh token 갱신 요청 로직
            */

            const ONE_HOUR = 60 * 60 * 1000;
            const ONE_WEEK = 60 * 60 * 1000 * 24 * 7;

            cookieStore.set('accessToken', response.accessToken, {
                secure: true,
                httpOnly: true,
                path:"/",
              //   domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN,
                expires: new Date(Date.now() + ONE_HOUR),
              });

              cookieStore.set('refreshToken', response.refreshToken, {
                secure: true,
                httpOnly: true,
                path:"/",
              //   domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN,
                expires: new Date(Date.now() + ONE_WEEK),
              });


            cookie = response.accessToken;
            refreshCookie = response.refreshToken;
        } else {
            cookie = tokenCookie.value;
            refreshCookie = refreshToken.value;

            // 기술통계 요청 데이터 확인
            const exampleScheme = z.object({
                // 요청 검증 필드
            });

            const _exampleData = await request.json();
            const exampleData = await exampleScheme.parseAsync(_exampleData);

            const exampleResponse = await example({ ...exampleData }, cookie, refreshCookie);

            if (!exampleResponse.ok) {
                return NextResponse.json({ message: 'unauthorized' }, { status: 401 });
            }

            const dataResponse = <ExampleResponse>await exampleResponse.json();

            const response = NextResponse.json(dataResponse);

            return response;
        }
    } catch (error) {
        console.log(error);
        return NextResponse.json({ message: 'server error' }, { status: 500 });

    }
}
  • Middleware를 적용하여, 쿠키가 없을 시 로그인 페이지로 자동 리디렉션 시킨다.
import { NextFetchEvent, NextMiddleware, NextRequest, NextResponse } from "next/server";

// 인증 없이 방문 불가 페이지
const PROTECTED_ROUTE: string[] = [
  // 페이지 리스트들
];

export type MiddlewareFactory = (middleware: NextMiddleware) => NextMiddleware;

export const authMiddleware: MiddlewareFactory = (next: NextMiddleware) => {
  return async (request: NextRequest, event: NextFetchEvent) => {
    // URL 에서 언어 값 분리
    const [ , , ...segments ] = request.nextUrl.pathname.split('/');

    const cookies = request.cookies.get("accessToken");

    // 체크사항
    // 1. 로그인 유무
    // 2. 인증 없이 방문 불가 페이지
    const isProtecteRoute = PROTECTED_ROUTE.includes(request.nextUrl.pathname);

    if (!cookies && isProtecteRoute) {
      return NextResponse.redirect(new URL('/login', request.url));
    }

    if (cookies && isProtecteRoute) {
      if (cookies.value === "") {
        return NextResponse.redirect(new URL('/login', request.url));
      }
    }
    // 인증 성공, 통과
    return next(request, event);
  };
};
728x90
반응형

'기록 > 사이드 프로젝트' 카테고리의 다른 글

[NEXTJS] 사이드 컴포넌트 쿠키 처리  (0) 2025.01.22
[OAUTH] 구글 OAuth  (0) 2025.01.02
728x90
반응형

Nextjs App Router 서버 사이드 요청

  • Nextjs의 App Router를 통해 서버사이드에서 요청 날리게끔 구성되어 있다.
  • 요청에 쿠키를 담아 서버쪽에서 검증 받는 흐름이지만, 쿠키가 계속 안 들어왔다.
    • credentials: "include" 인데도 들어오지 않는 것에 의문을 가졌다.
  • 결론은 서버 사이드에서의 요청일 경우 직접 쿠키를 담아줘야 한다는 것이다.

쿠키 양식

"Cookie": "accessToken=tokendata; refreshToken=refreshToken"

변경된 코드

  • 헤더에 직접 입력
  • API 라우트에서 쿠키를 뽑아와 인자로 던짐

클라이언트 사이드 요청 함수

  • react query를 이용해 요청 및 관리
  • 이 때는 쿠키를 담을 수 없다.
    • 클라이언트 사이드에서는 쿠키를 가져올 수 없음. 브라우저 범위이기 때문
export interface ReferenceListResponse {
    status: number;
    code: string;
    message: string;
    result: ReferenceListItem[];
    totalCount: number;
}

export interface ReferenceListItem {
    referenceSeq: string;
    referenceName?: string;
    referenceUrl: string;
    referenceMemo?: string;
    referenceCategory: string;
    createdAt: string;
}

export const referenceListRequest = async (props: ReferenceListRequest) => {

    const response = await jsFetch<ReferenceListResponse>(`/api/reference/list`, {
        method: 'post',
        cache: 'no-store',
        body: JSON.stringify(props),
        headers: {
            'content-type': 'application/json',

        },        
    });

    return response;

};

서버 사이드 요청 함수

export interface ReferenceListRequest {
    category: string;
    page: number;
    pageSize: number;
}


export const referenceList = async (request: ReferenceListRequest, accessToken: string, refreshToken: string) => {

    const response = await fetch(
    `${ process.env.NEXT_PUBLIC_BASE_API }/reference/list`, {
        method: 'post',
        cache: 'no-store',
        body: JSON.stringify({ category: request.category, page: request.page, pageSize: request.pageSize }),
        headers: {
            'content-type': 'application/json',
            "Cookie": `accessToken=${ accessToken }; refreshToken=${ refreshToken }`
        },
        credentials: 'include', // 쿠키를 포함해서 요청 전송
        next: { tags: [ 'search-word' ] },
    });

    return response;
};
728x90
반응형

'기록 > 사이드 프로젝트' 카테고리의 다른 글

[AUTH] 검증 방법 변경 - Http Only Cookie  (0) 2025.01.31
[OAUTH] 구글 OAuth  (0) 2025.01.02
728x90
반응형

개요

  • 로그인 시 발급되는 access token을 쿠키에 세팅
  • 미들웨어에서 쿠키의 access token 을 검증

Cookie 세팅

  • 생성된 토큰을 쿠키에 세팅한다
func Login(res http.ResponseWriter, req *http.Request) {

    /*
        로그인 로직
    */

    accessTokenCookie := http.Cookie{
        Name: "accessToken",
        Value: accessToken,
        Path: "/",
        Secure: true, // HTTPS 만
        HttpOnly: true, // 브라우저에서 쿠키 조작 불가하게 세팅
        SameSite: http.SameSiteNoneMode,
    }

    refreshTokenCookie := http.Cookie{
        Name: "refreshToken",
        Value: refreshToken,
        Path: "/",
        Secure: true, // HTTPS 만
        HttpOnly: true, // 브라우저에서 쿠키 조작 불가하게 세팅
        SameSite: http.SameSiteNoneMode,
    }

    http.SetCookie(res, &accessTokenCookie)
    http.SetCookie(res, &refreshTokenCookie)

    /*
        응답 보내기
    */
}

검증 미들웨어

  • 쿠키에서 토큰을 추출해서 컨텍스트에 담는다
  • 담은 컨텍스트의 정보를 컨트롤러에서 사용한다.
// 사용자 정의 키 타입을 사용하여 컨텍스트 충돌 방지

type contextKey string

const (
    // JWT 서명에 사용할 비밀 키 (환경 변수로 관리하는 것이 좋습니다)

    // jwtSecret = configs.GlobalConfig.JwtKey // 실제 배포 시 환경 변수로 관리

    // 컨텍스트에 사용자 정보를 저장할 키
    userContextKey = contextKey("user")
)

// 사용자 정보 구조체

type User struct {
    UserId string
    UserEmail string
    UserStatus string
}

var excludeRouteList = []string{
    "/", "/api", 
    "/user/signup", "/user/login",
}

// AuthMiddleware는 accessToken 쿠키를 추출하고 JWT를 검증하는 미들웨어입니다.
func AuthMiddleware(next http.Handler) http.Handler {

    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

        // 제외할 경로는 바로 다음 핸들러로 넘김
        for _, route := range excludeRouteList {
            if r.URL.Path == route {
                log.Printf("Found Match Route: %s", route)
                next.ServeHTTP(w, r)
                return
            }
        }

        // accessToken 쿠키 추출
        cookie, err := r.Cookie("accessToken")

        log.Printf("Cookie: %s", cookie)

        if err != nil {
            log.Printf("Get Cookie Error :%v", err)  

            if err == http.ErrNoCookie {
                response.Response(w, response.CommonResponseWithMessage{
                    Status: http.StatusUnauthorized,
                    Code: "AUTH001",
                    Message: "No access token provided",    
                })

                return
            }

            // 다른 쿠키 에러 처리

            response.Response(w, response.CommonResponseWithMessage{
                Status: http.StatusUnauthorized,
                Code: "AUTH002",
                Message: "Invalid cookie format",
            })

            return
        }

        accessToken := cookie.Value

        userId, userEmail, userStatus, validateErr := auth.ValidateJwtTokenFromString(accessToken)


        if validateErr != nil {
            log.Printf("ValidateErr Cookie Error :%v", validateErr)

            // 토큰 만료에 대한 응답
            if strings.Contains(validateErr.Error(), "token expired") {
                response.Response(w, response.CommonResponseWithMessage{
                    Status: http.StatusUnauthorized,
                    Code: "AUTH003",
                    Message: "Token expired",
                })

                return
            }

            // 일반적인 JWT 검증 실패 응답
            response.Response(w, response.CommonResponseWithMessage{
                Status: http.StatusUnauthorized,
                Code: "AUTH004",
                Message: "Invalid token",
            })

            return        
        }

        // 사용자 정보를 구조체로 생성

        user := User{
            UserId: userId,
            UserEmail: userEmail,
            UserStatus: userStatus,
        }

        // 사용자 정보를 컨텍스트에 추가
        ctx := context.WithValue(r.Context(), userContextKey, user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}



// 사용자 정보를 핸들러 및 컨트롤러에서 가져오는 헬퍼 함수
func GetUserFromContext(ctx context.Context) (User, bool) {
    user, ok := ctx.Value(userContextKey).(User)
    return user, ok
}

컨트롤러에서 추출된 정보들 사용


func SampleController(res http.ResponseWriter, req *http.Request) {
    user, ok := middlewares.GetUserFromContext(req.Context())

    if !ok {    
        dto.SetErrorResponse(res, 401, "01", "JWT Verifying Error", nil)

        return
    }
    /*
        이후 로직
    */

    dto.SetResponse(res, 200, "01")
}
728x90
반응형
728x90
반응형

기존

  • 기존에는 직접 CORS 체크 미들웨어를 작성해 사용했다.
func CorsMiddlewares(next http.Handler) http.Handler {
    return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {

        origin := req.Header.Get("Origin")

        if origin == "" {
            origin = "unknown"
        }

        for _, o := range originList {
            if o == origin {    
                log.Printf("Allowed Origin: %s", origin)
                res.Header().Set("Access-Control-Allow-Origin", origin)
                res.Header().Set("Access-Control-Max-Age", "86400")
                res.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
                res.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
                res.Header().Set("Access-Control-Allow-Credentials", "true")
                break
            }
        }

        // Handle preflight request
        if req.Method == http.MethodOptions {
            res.WriteHeader(http.StatusOK)
            return
        }

        next.ServeHTTP(res, req)
    })
}

변경

  • 그러나 조금 더 심플하고 편하게 설정할 수 있는 패키지가 있어 가져와 사용했다.
get -u github.com/rs/cors
  • 이후 세팅은 이렇게 된다.
func CorsHanlder() *cors.Cors {
    corHandler := cors.New(cors.Options{
        AllowedOrigins: originList,
        AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodOptions},
        AllowedHeaders: []string{"Origin", "Accept", "Content-Type", "Authorization"},
        AllowCredentials: true,
        MaxAge: 86400,        
        Debug: false,
    })

    return corHandler
}

미들웨어 등록하기

  • 현재 표준 라이브러리인 net/http 에서 gorilla/mux 로 변경한 상태이다.
func OpenServer() *http.Server {
    router := mux.NewRouter()

    routers.DefaultRouter(router)

    routers.UploadImageController(router)

    routers.AdminUserRouter(router)

    routers.AdminPostRouter(router)

    routers.UserRouter(router)

    routers.PostRouter(router)

    router.Use(middlewares.AuthMiddleware)

    // CORS 핸들러
    handler := middlewares.CorsHanlder().Handler(router)

    // 기존 방식  
    // handler := middlewares.CorsMiddlewares(router)

    serving := &http.Server{
        Handler: handler,
        Addr: configs.GlobalConfig.AppHost,
        WriteTimeout: 30 * time.Second,
        ReadTimeout: 30 * time.Second,
    }

    return serving
}
728x90
반응형
728x90
반응형

TLS 핸드세이크

  • 클라이언트 - 서버가 교환하는 일련의 데이터그램 == 메시지
  • 사용되는 키 교환 알고리즘의 종류 및 양측에서 지원하는 암호 모음에 따라 달라짐
    • 1.3 버전 이전에는 RSA 키 교환 알고리즘을 사용했지만, 안전하지 않음

TLS 핸드세이크 작동 방식

  • 비대칭 암호화(공개 키 - 개인 키) 사용

  • TLS 핸드세이크
    • 클라이언트 헬로 메시지
      • 클라이언트가 서버로 Hello 메시지 전송하여 핸드세이크 개시
      • 이 때 TLS 버전, 지원 암호 제품군, 클라이언트 무작위 바이트 문자열 포함
    • 서버 헬로 메시지
      • 클라이언트 헬로 메시지에 대한 응답으로 서버의 SSL 인증서, 서버에서 선택한 암호 제품군, 그리고 서버에서 생성한 무작위 문자열 바이트 포함한 메시지 전송
    • 인증
      • 클라이언트가 서버의 SSL 인증서를 인증서 발행 기관을 통해 검증
      • 인증서에 명시된 서버인지, 상호작용중인 서버가 실제 해당 도메인의 소유자인지 확인
    • 예비 마스터 암호
      • 클라이언트가 무작위 바이트 문자열 하나 더 전송
      • 공개키로 암호화 되며, 서버 개인 키로만 해독 가능
      • 공개키는 서버의 SSL 인증서를 통해 클라이언트에 전달됨
    • 개인키 사용
      • 서버가 예비 마스터 암호를 해독함
    • 세션 키 생성
      • 클라이언트와 서버 모두 클라이언트 무작위, 서버 무작위, 예비 마스터 암호 이용해 세션 키 생성
      • 모두 동일한 결과가 나와야 함
    • 클라이언트 준비 완료
      • 클라이언트가 세션 키로 암호화된 완료 메시지 전송
    • 서버 준비 완료
      • 서버가 세션 키로 암호화된 완료 메시지 전송
    • 안전한 대칭 암호화 성공
      • 핸드세이크 완료, 세션키를 통한 통신이 진행
  • 데이터가 암호화되고 인증되면, 메시지 인증 코드(MAC: Message Authorization Code)와 함께 서명됨
728x90
반응형

'네트워크' 카테고리의 다른 글

[TLS] TLS 란?  (0) 2025.01.13
728x90
반응형

TLS(Transport Layer Security)란?

  • 인터넷 상 커뮤니케이션을 위한 개인 정보 및 데이터 보안 용이하게 위한 하기 보안 프로토콜
  • 즉 웹 브라우저/어플리케이션과 서버간 커뮤니케이션을 암호화 하는 포로토콜
    • 이메일, 메세지, VoIP(보이스오버) 등

TLS 역할

  • 암호화: 제 3자로부터 전송되는 데이터 숨김
  • 인증: 정보 교환 당사자가 요청된 당사자임을 보장
  • 무결성: 데이터가 위조되거나 변조되지 않았는지 확인

사용 이유

  • 데이터 유출 및 공격으로부터 웹 어플리케이션 보호 용도

TLS vs SSL

  • SSL (Secure Sockets Layer): 보안 소켓 계층
  • TLS는 SSL에서 발전한 버전
  • 두개를 혼동하여 사용하는 경우가 많음. 현재는 TLS 를 주로 사용함

TLS vs HTTPS

  • HTTPS는 HTTPS 프로토콜 상위에서 TLS 암호화를 구현한 것

TLS 인증서?

  • 웹 어플리케이션이 TLS를 사용하기 위해선 원본 서버에 TLS 인증서가 설치되어야 함
    • SSL 인증서라고 혼동돼서 불리기도 함
    • 인증 기관이 도메인을 소유한 사람 혹은 비즈니스에 TLS 인증서 발행
  • 서버의 공개키 및 도메인 소유자에 대한 정보 포함함
  • 서버의 신원 확인하는데 사용됨

암호 제품군?

  • 안전한 통신 연결 수립을 위한 알고리즘 세트
728x90
반응형

'네트워크' 카테고리의 다른 글

[TLS] TLS 핸드세이크?  (0) 2025.01.14
728x90
반응형

Nullable 데이터

  • DB의 필드 중에 NULL 값이 들어갈 수 있는 값들이 있다. 이 값들을 쿼리해서 코드단에서 처리할 때 어떻게 해야 할까?
  • 다른 언어들은 그냥 쿼리해서 사용하면 null 값으로 처리되던데...

Struct 정의

  • 우선 쿼리된 결과를 원하는 값들에 할당해준다. 아래는 기존에 사용하던 코드
type SampleQueryItem struct {
  UserId string `json:"userId"`
  UserName string `json:"userName"`
  UserEmail string `json:"userEmail"`
}
  • 이렇게 하면 받아오는 데이터 세개 전부 값이 있다고 가능하는 셈이다.
    • 이렇게 된다면 null 값들은 처리할 수 없다.

포인터 사용하기

type SampleQueryItem struct {
  UserId string `json:"userId"`
  UserName string `json:"userName"`
  UserEmail *string `json:"userEmail"` // 포인터 사용
}
  • 이렇게 처리하면 userEmail 값은 null 로 받아올 수 있다.

번외; 단건 조회 데이터가 없을 때

  • 한 건 조회할 때, 쿼리 데이터가 없다면?
  • golang은 심플함을 강조하기 때문에, 이러한 상황들을 직접 핸들링 해 줘야 한다.

기존 코드

var dataItem SampleQueryItem

if scanErr := queryResult.Scan(&dataItem.UserId, &dataItem.UserName, &dataItem.UserEmail);
    scanErr != nil {
        log.Printf("Scan Error: %v", scanErr)
        return scanErr
    }
  • 이렇게 해 주면 scan 에러가 발생한다. 그 이유는 쿼리된 데이터가 없다는 것

수정된 코드

var dataItem SampleQueryItem

if scanErr := queryResult.Scan(&dataItem.UserId, &dataItem.UserName, &dataItem.UserEmail);
    scanErr != nil {
        // 데이터가 없는 에러는 error 에러를 리턴하지 않게 해서 에러로 빠지지 않게 한다.
        if scanErr == sql.NoRowError {
            return SampleQueryItem{}, nil
        }

        log.Printf("Scan Error: %v", scanErr)
        return SampleQueryItem{}, scanErr
    }
  • 조회된 데이터가 없는 것은 에러로 처리된다. scan 할 데이터가 없기 때문
    • 위 처럼 쿼리 후 에러 핸들링을 하면, 에러 처리로 넘기지 않고 빈 데이터를 넘길 수 있다.
728x90
반응형
728x90
반응형
  • 사이드 프로젝트를 하나 더 만들며, 이번 기회에 보안 뿐만 아니라 편리성 측면에서 SSO 의 혜택을 누리고자 구글 OAuth 를 사용해 보았다.

로직 개요

  • 프론트에서 Google OAuth 를 이용해 서비스 가입 및 유저 정보 획득
  • 획득된 유저 정보를 백엔드로 전달
  • 백엔드는 전달된 유저 정보를 DB에 입력
  • 생성된 데이터들을 기반으로 JWT 생성 및 세션 생성
  • 생성된 JWT 를 프론트엔드에 전달
  • 프론트는 전달된 JWT를 매 요청의 헤더에 넣어서 사용

구글 OAuth 설정

  • Google Cloud 에서 콘솔 클릭
  • 콘솔에서 프로젝트 생성
    • 생성된 프로젝트의 API 및 서비스 메뉴로 이동
    • OAuth 동의 화면 설정 진행
      • 서비스 정보 입력
    • 사용할 정보 추가
      • 이메일 및 개인정보 확인만 체크했음
    • 테스트 사용자 추가
      • 테스트 모드인 동안은 테스트 계정만 사용 가능
  • 사용자 인증 정보 만들기
    • OAuth 클라이언트 ID 선택
    • 웹 어플리케이션 선택
    • 승인된 리디렉션 URI 추가
      • google 에서 인증받은 후 리디렉션 되는 경로
      • 프론트에서 사용할 값이므로 프론트의 주소를 사용하도록 등록함
        • 이 때 next-auth를 사용했으므로, next-auth 공식문서에서 제시한 Url Path로 설정
        • /api/auth/callback/google

Oauth 값들 기록

  • clientId: 발급받은 클라이언트 ID
  • clientSecret: 발급 받은 클라이언트 비밀번호
  • redirectUri: 리디렉션 URI
728x90
반응형
728x90
반응형

Centos9 에 Oracle 컨테이너 구동

    # 필요한 버전을 -v 로 실행시키기
    ./buildContainerImage.sh -v 19.3.0 -e
  • docker-compose.yml
services:
    oracle-db:
        shm_size: 8g
        image: oracle/database:19.3.0-ee
        container_name: oracle19c
        ports:
            - "1521:1521" # Oracle Listener Port
            - "5500:5500" # EM Express Port
        environment:
            ORACLE_SID: "ORCLCDB" # Replace with your SID
            ORACLE_PDB: "ORCLPDB1" # Replace with your PDB name
            INIT_SGA_SIZE: "1024" # Replace with your desired SGA size in MB
            INIT_PGA_SIZE: "256" # Replace with your desired PGA size in MB    
            INIT_CPU_COUNT: "2" # Replace with desired CPU count
            INIT_PROCESSES: "300" # Replace with processes init parameter
            ORACLE_EDITION: "standard"
            ORACLE_CHARACTERSET: "AL32UTF8" # Character set for the database
            ENABLE_ARCHIVELOG: "true" # Enable archive log
            ENABLE_FORCE_LOGGING: "true" # Enable force logging    
            ENABLE_TCPS: "true" # Enable TCPS protocol
        volumes:
            - oradata:/opt/oracle/oradata
        ulimits:
            nofile:
                soft: 1024
                hard: 65536
            nproc:
                soft: 2047
                hard: 16384
            stack:
                soft: 10485760
                hard: 33554432
            memlock:
                soft: 3221225472
                hard: 3221225472

volumes:
    oradata:
        external: true
  • 이제 컨테이너 실행시키기
    • 생각보다 인스턴스 실행에 시간이 많이 소요됨
  • 계정 생성
    • 컨테이너 내부 접속하면 패스워드 세팅 스크립트가 있음
    • 그걸로 일단 디폴트 유저 세팅 한 다음이 sqlplus로 유저 생성
    sudo docker exec -it oracle19c bash

    # 컨테이너 내부에서 sqlplus 사용
    sqlplus / as sysdba
  • Identify Available Pluggable Databases (PDBs)

      SHOW PDBS;
      ALTER SESSION SET CONTAINER = <데이터베이스 명>;
  • 필요한 유저 생성은 sqlplus에서 아래 쿼리문으로

    • SYSTEM 계정으로 실행
-- Create a user
CREATE USER youruser IDENTIFIED BY yourpassword;

--Grant permissions
GRANT CONNECT, RESOURCE, DBA TO demo;
  • 유저 권한 부여를 위해서 세션 연결을 한 후에 하더라
  • 사용자가 이미 존재할 때 테이블 생성 명령문
-- 테이블 생성 권한 부여
GRANT CREATE TABLE FROM user;

-- 모든 권한 부여
GRANT ALL PRIVILEGES TO user;
  • 덤프는 뜬 거로 임포트 했는데 덤프 데이터에 대한 에러들이 발생함
imp userid=user/password file=./backup.dmp

imp user/password file=./backup.dmp

imp userid=user/password tables=TABLE1 file=./backup.dmp

imp user/password tables=TABLE1 file=./backup.dmp

imp userid=user/password tables=TABLE1,TABLE2,TABLE3 file=./backup.dmp

imp user/password tables=TABLE1,TABLE2,TABLE3  file=./backup.dmp
728x90
반응형
728x90
반응형

Standalone 이란

  • 독립 / 자립 이라는 의미
  • 파일 자체로 완전하게 독립적으로 구동된다는 의미
    • 다른 장비의 도움 없이, 그 자체로 구현이 가능한 것
      • 이건 네트워크도 해당될 수 있음. 즉 오프라인에서도 구동가능한 프로그램을 의미하기도 함
      • 어떤 소프트웨어 번들의 일부분이 아님

예시

  • 예시로 자바의 war 파일을 그 자체로 서버로서 사용하는 것
  • 혹은 Go 의 빌드된 바이너리 파일을 구동시켜 서버로 사용하는 것
  • 도커를 이용한 서버 구동은 stand alone이라 하기 어려움

리눅스 Standalone 네트워크 서비스

  • 리눅스 서버에서의 standalone 방식 네트워크 서비스 제공 방식
    • 서버 부팅 시 데몬을 작동시켜 백그라운드에서 대기 시킴
    • 요청 들어올 때 혼자서 처리하는 프로그램
    • 항상 서비스 가능하며, 다른 의존성 없이 바로 네트워크 서비스 데몬이 클라이언트 요청을 처리
  • 항상 메모리에 상주해야 하기 때문에 메모리 점유율이 높음

번외 inetd

  • 서비스 요청이 있을 때 마다 데몬을 실행시키는 방식
    • 요청이 없을 때에는 대기(잠자는 중)
  • 적은 시스템 리소스로 여러가지 서비스를 돌릴수 있다
  • 대신 요청이 들어올 때 데몬이 프로그램을 실행시켜야 하므로 속도가 느리다.

예시

  • Standalone: Apache 웹서버
  • inetd: telnet
728x90
반응형

'데브옵스 devOps > Server' 카테고리의 다른 글

[파일전송] Rsync  (0) 2024.12.11
[배포전략] 무중단 배포란?  (1) 2024.10.05

+ Recent posts