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
반응형

Graph DB 핸들링

  • MATCH 로 조회한 데이터는 전부 Map으로 매핑된 데이터들
    • 따라서 쿼리한 데이터를 가져올 때 GET 메서드로 데이터를 가져와야 함
    • 아래는 처음에 짰던 코드
      • 코드가 길고 가독성도 별로라서 다른 방안 고민 중 - 아래에 수정 내용 추가
result, queryErr := dbCon.Query(GetWordAndRelatedWord, map[string]interface{}{"word": word})

if queryErr != nil {
    log.Printf("[SEARCH] Query Word Error: %v", queryErr)
    return []SearchWordResult{}, queryErr
}

var queryResult []SearchWordResult

for _, data := range result {

    name, isName := data.Get("name")

    if !isName {
        log.Printf("[SEARCH] No Name Found. Ignore. Word: %s", word)
        continue
    }

    description, isDescription := data.Get("description")

    if !isDescription {
        log.Printf("[SEARCH] No Description Found. Ignore. Word: %s", word)
        continue
    }

    createdAt, isCreatedAt := data.Get("created_at")

    if !isCreatedAt {
        log.Printf("[SEARCH] No CreatedAt Found. Ignore. Word: %s", word)
        continue
    }

    updatedAt, isUpdatedAt := data.Get("updated_at")  

    if !isUpdatedAt {
        log.Printf("[SEARCH] No UpdatedAt Found. Ignore. Word: %s", word)
        continue
    }

    category, isCategory := data.Get("category")  

    if !isCategory {

        log.Printf("[SEARCH] No Category Found. Ignore. Word: %s", word)

        continue

    }

    createdBy, isCreatedBy := data.Get("created_by")

    if !isCreatedBy {
        log.Printf("[SEARCH] No CreatedBy Found. Ignore. Word: %s", word)
        continue
    }

    var node = SearchWordResult{
        Name: name.(string),
        Category: category.(string),
        Description: description.(string),
        CreatedBy: createdBy.(string),
        CreatedAt: createdAt.(string),
        UpdatedAt: updatedAt.(string),
    }
    queryResult = append(queryResult, node)

}

데이터 핸들링 수정

  • 처음에는 위처럼 데이터를 핸들링 했지만, 읽는 노드 개수가 여러개가 됨에 따라 그 데이터 형태가 달라졌다.
    • 따라서 쿼리 방식을 바꿔야 했고, 데이터 형태 파악부터 해야 했다.
      • 데이터는 배열 안에 각각 노드 Label에 따라 노드들이 배열에 담겨있었다.
      • 따라서 쿼리 결과를 iteration 하며, 각각 label과 함께 나온 데이터를 핸들링하게 끔 수정했다.
    • 쿼리 결과를 GET해서 가져왔을 때, 그 타입이 any 였다.
      • 때문에 이를 dbtype.Node로 캐스팅하고, Node의 Properties를 뽑아와 사용했다.
        • dbType은 neo4j에서 제공하는 노드 타입
        • properties는 map[string]any 타입

responseMap := make(map[string]*SearchWordResult)

for _, data := range result {
    wNode, isNode := data.Get("word")

    if !isNode {
        log.Printf("[SEARCH] No Node Found")
        continue
    }

    targetNode := wNode.(dbtype.Node)

    rNode, isRelated := data.Get("related")

    if !isRelated {
        log.Printf("[SEARCH] No Related Node Found")
        continue    
    }

    relatedNodeList := rNode.(dbtype.Node)

    wordeNode := SearchWordItem{
        Name: targetNode.GetProperties()["name"].(string),
        Category: targetNode.GetProperties()["category"].(string),
        Description: targetNode.GetProperties()["description"].(string),
        CreatedBy: targetNode.GetProperties()["created_by"].(string),
        CreatedAt: targetNode.GetProperties()["created_at"].(string),
        UpdatedAt: targetNode.GetProperties()["updated_at"].(string),
    }

    relatedNode := SearchWordItem{
        Name: relatedNodeList.GetProperties()["name"].(string),
        Category: relatedNodeList.GetProperties()["category"].(string),
        Description: relatedNodeList.GetProperties()["description"].(string),
        CreatedBy: relatedNodeList.GetProperties()["created_by"].(string),
        CreatedAt: relatedNodeList.GetProperties()["created_at"].(string),
        UpdatedAt: relatedNodeList.GetProperties()["updated_at"].(string),
    }

    if _, exists := responseMap[wordeNode.Name]; !exists {
        responseMap[wordeNode.Name] = &SearchWordResult{
            Word: wordeNode,
            Related: []SearchWordItem{},
        }
    }

    responseMap[wordeNode.Name].Related = append(responseMap[wordeNode.Name].Related, relatedNode)

}

var responseList []SearchWordResult

for _, response := range responseMap {
    responseList = append(responseList, *response)
}

노드 및 관계 읽기

  • 아래 쿼리 방식은 관계를 통한 조회이다.
    • relations가 RELATED 혹은 PARENT 인 노드 관계들을 조회하고
    • 1..2 는 hop에 관한 내용이다
      • 1 은 직접적으로 연관된 노드들
      • 2는 중간에 노드가 하나 있는 노드들
    • 조회에 성공한 노드들 정보를 리턴한다
      • SQL과 동일하게 DISTINCT 해서 리턴
      • word의 경우 name 프로퍼티에 대한 조건 쿼리랑 동일하기 때문에 하나 혹은 동일한 이름을 가진 노드들 리턴
      • related의 경우, 특정 이름을 가진 노드와 1홉 혹은 2홉의 관계를 가진 모든 노드들
MATCH (w:Word {name: $word})-[:RELATED|PARENT*1..2]-(related:Word)
RETURN DISTINCT word, related

관계 property 설정

  • 실제 짜서 사용중인 코드이다. 아래처럼 관계를 설정한다.
    • 방향이 설정되지 않은 관계는 RELATED
    • 부모/자식 노드들의 관계를 PARENT / CHILDREN이라는 관계로 정의함
// Create Relations
var CreateWordRelatinon = `
    MATCH (w1:Word{name: $name1}), (w2:Word{name: $name2})
    MERGE (w1)-[r:RELATED]-(w2)
    SET r.weight = $weight
`

// Create Relations; w1 is Parent, w2 is Children
var CreateWordParent = `
    MATCH (w1:Word{name: $name1}), (w2:Word{name: $name2})
    MERGE (w1)-[r:PARENT]->(w2)
    SET r.weight = $weight
`

// Create Relations; w2 is Parent, w1 is Children
var CreateWordChildren = `
    MATCH (w1:Word{name: $name1}), (w2:Word{name: $name2})
    MERGE (w1)-[r:CHILDREN]->(w2)
    SET r.weight = $weight
`
728x90
반응형
728x90
반응형

공식 문서

Golang으로 연결

  • neo4j 드라이버 설치
    • 연결의 인증은 여러 방식이 있음; BasicAuth, BearerAuth, AuthToken, CustomAuth, ...
    • 본 예시는 유저 이름과 패스워드로 인증하는 방식
go get -u github.com/neo4j/neo4j-go-driver/v5/neo4j
  • 드라이버 인스턴스 생성
import (
    "github.com/neo4j/neo4j-go-driver/v5/neo4j"
    "github.com/neo4j/neo4j-go-driver/v5/neo4j/db"
)

func main() {
    driver, err := neo4j.NewDriverWithContext(
        "neoj4://<DATABASE_URI>:<DATABASE_PORT>,
        neo4j.BasicAuth(<USER>, <PASSWD>, ""),
    )
}
  • 연결 체크
import (
    "context"

    "github.com/neo4j/neo4j-go-driver/v5/neo4j"
    "github.com/neo4j/neo4j-go-driver/v5/neo4j/db"
)

func main() {
    driver, err := neo4j.NewDriverWithContext(
        "neoj4://<DATABASE_URI>:<DATABASE_PORT>,
        neo4j.BasicAuth(<USER>, <PASSWD>, ""),
    )

    ctx := context.Background()

    err := driver.VerifyConnectivity(ctx)

    if err != nil {
        log.Printf("[GRAPH_DB] Check Connection Error: %v", err)
        return err
    }

    defer driver.Close(ctx)

    return nil
}

Golang으로 쿼리

  • 쿼리 실행
    • ExecuteQuery 메서드로 쿼리 실행
import (
    "context"

    "github.com/neo4j/neo4j-go-driver/v5/neo4j"
    "github.com/neo4j/neo4j-go-driver/v5/neo4j/db"
)

func main() {
    driver, err := neo4j.NewDriverWithContext(
        "neoj4://<DATABASE_URI>:<DATABASE_PORT>,
        neo4j.BasicAuth("<USER>", "<PASSWD>", ""),
    )

    ctx := context.Background()

    // 쿼리 실행
    result, queryErr := neo4j.ExecuteQuery(
        ctx, 
        driver, 
        "MERGE (n:Node {name:"$nodeName"})", // Node라는 유니크 노드 생성
        []map[string]interface{ // 파라미터 생성. nodeName에 들어갈 값
            nodeName: "New Node"
        }, 
        neo4j.EagerResultTransformer, // 쿼리 결과 변환
        neo4j.ExecuteQueryWithDatabase("neo4j"),
    )

    if queryErr != nil {
        return nil, queryErr
    }

    defer db.Close(ctx)

    for _, record := range result.Records {
        log.Printf("[GRAPH_DB] Query Result: %v", record.Values...)
    }

    // 쿼리 결과에 대한 요약
    log.Printf("The query `%v` returned %v records in %+v.\n",
        result.Summary.Query().Text(), len(result.Records),
        result.Summary.ResultAvailableAfter())
}
728x90
반응형

'백엔드 Backend > Golang' 카테고리의 다른 글

[gRPC] Go 언어로 gRPC 사용해보기  (2) 2024.12.24
[Neo4J] 데이터 핸들링하기  (1) 2024.12.09
[Neo4j] Golang 으로 쿼리 핸들링  (0) 2024.12.06
[GO] Ubuntu 서버에 설치  (1) 2024.10.30
[GO] Goroutine이란?  (0) 2024.10.02

+ Recent posts