본문 바로가기
Programming/Go

[GO 인증 구현 with JWT 1] 전체적인 인터페이스 작성

by TinKerBellBass 2022. 4. 9.
728x90
반응형

Use Case

간단히 구현해 볼 인증의 시나리오

유저 생성 → 생성된 유저 정보로 토큰 발급 → 발급된 토큰에 들어있는 유저ID 를 이용해서 유저 정보 획득

 

유저 생성 Use Case

User ID, Password 를 입력받아 유저를 생성

생성된 유저를 영속화

 

생성된 유저 정보로 토큰 발급 Use Case

User ID, Password 를 입력받아 유저를 검증, 패스워드 비교

검증된 유저에게 토큰 발급, 토큰은 JWT

 

발급된 토큰에 들어있는 유저ID 를 이용해서 유저 정보 획득 Use Case

토큰을 입력 받아 토큰 검증

검증된 토큰으로부터 User ID 획득

User ID 를 키 값으로 검색해서 유저 정보 획득

 

엔티티, DTO 작성

엔티티인 User 작성

영속화할 때 자동 증가시킬 ID,

User ID, Password 로 간단히 엔티티 정의. 

패스워드는 암호화 하지 않을 예정이며, 패스워드를 가지지 않는 유저도 정의.

type User struct {
	ID       int
	UserID   string
	Password string
}

type UserWithoutPassword struct {
	ID     int
	UserID string
}

요청 정보를 담아 서비스로 퍼다 나르기 위한 User DTO

type UserDto struct {
	UserID   string
	Password string
}

응답을 위한 User Response

type UserResponse struct {
	ID     int    `json:"id"`
	UserID string `json:"user_id"`
}

 

User Repository Interface 작성

개인적인 취향이기는 한데 의존성 흐름의 제일 마지막부터 작성해 나가는 것이 편하기 때문에 저장소부터 작성.

유저를 입력받아 영속화 하는 Save 메서드,

자동 증가하는 ID 를 가지고 유저를 조회하는 FindByID 메서드,

User ID 를 가지고 유저를 조회하는 FindByUserID 메서드를 가지는 인터페이스.

고를 쓰면서 아직 헷갈리고 아리송하고 잘 모르겠는 게

어떤 상황에서 pass by reference 를 쓰고, 어떤 상황에서 pass by value 를 쓸 것인가 인데

메서드의 반환값을 다 포인터로 한 이유는 유저 조회 후 유저가 없을 경우의 케이스를 생각해서

nil 을 이용한 비교가 가능하기 때문이다.

type UserRepositoryIF interface {
	Save(user User) *User
	FindByID(id int) (*User, error)
	FindByUserID(userID string) (*User, error)
}

 

User Service Interface 작성

UserUseCase 라고 이름 지을까 하다가 그냥 리포지토리하고 통일해서 IF 를 붙이는 걸로 결정.

요청 정보인 UserDto 를 입력받아 응답 정보인 UserResponse 를 반환하는 SignUp 메서드,

요청 정보인 UserDto 를 입력받아 응답 정보인 토큰을 반환하는 SignIn 메서드,

요청 정보인 userID 를 입력받아 응답 정보인 UserResponse 를 반환하는 GetUserByUserID 메서드를 가지는 인터페이스. 

type UserServiceIF interface {
	SignUp(userDto UserDto) (UserResponse, error)
	SignIn(userDto UserDto) (string, error)
	GetUserByUserID(userID string) (UserResponse, error)
}

 

Token 처리 메서드 작성

심플하게 만들어 볼 생각이라 보안적인 부분은 생각하지 않는걸로.

Claim 은 간단히 주제, 생성일자, 파기 일자만 사용할 예정으로

GO JWT 에서 제공해 주는 StandardClaims 를 사용.

 

JWT 페이로드인 Claim 을 생성하는 NewClaim 메서드,

Claim 을 입력받아 토큰을 생성해서 반환하는 GenerateToken 메서드,

토큰을 입력받아 검증하고 검증이 통과한 토큰에서 User ID 를 추출해서 반환하는 ValidateToken 메서드. 

// TODO 환경변수 등록 후 사용 등 보안적으로 보완해야 함
const secret string = "secret"

func NewClaim(userID string) *jwt.StandardClaims {
	now := time.Now()
	return &jwt.StandardClaims{
		Subject:   userID,
		IssuedAt:  now.Unix(),
		ExpiresAt: now.Add(time.Hour * 24).Unix(),
	}
}

func GenerateToken(claim *jwt.StandardClaims) (string, error) {
	// TODO 구현할 것!
	panic("not implemented")
}

func ValidateToken(token string) (string, error) {
	// TODO 구현할 것!
	panic("not implemented")
}

 JWT 의 자세한 내용은 공식 홈에서.

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

 

인증 미들웨어 작성

이번에는 go chi 를 사용해서 라우터를 구성할 것이고

인증은 미들웨어로 제공해서 라우팅 전에 인증하도록 할 생각.

인증 로직이 들어갈 AuthenticateMiddleware 메서드,

Authentication 헤더에서 토큰을 추출할 getTokenFromRequest 메서드.

JWT 를 사용한 인증이므로 인증 스키마는 Basic 이 아닌 Bearer 을 사용.

func AuthenticateMiddleware(next http.Handler) http.Handler {
	// TODO 구현할 것!
	panic("not implemented")
}

const (
	AuthSchema string = "Bearer "
)

func getTokenFromRequest(r *http.Request) (string, error) {
	// TODO 구현할 것!
	panic("not implemented")
}

 

UserHandler 작성

유저 핸들러를 주입받는 곳은 없기에 인터페이스 없이 구현체를 사용.

User Service Interface 를 주입받아 처리를 위임.

유저 등록 요청을 핸들링할 SignUp 메서드,

유저 검증 후 토큰 발급 요청을 핸들링 할 SignIn 메서드,

토큰에서 User ID 를 추출해서 유저 정보 조회 요청을 핸들링할 GetSelfUser 메서드.

type UserHandler struct {
	userService UserServiceIF
}

func NewUserHandler(auth UserServiceIF) *UserHandler {
	return &UserHandler{userService: auth}
}

func (u *UserHandler) SignUp(w http.ResponseWriter, r *http.Request) {
	// TODO 구현할 것!
	panic("not implemented")
}

func (u *UserHandler) SignIn(w http.ResponseWriter, r *http.Request) {
	// TODO 구현할 것!
	panic("not implemented")
}

func (u *UserHandler) GetSelfUser(w http.ResponseWriter, r *http.Request) {
	// TODO 구현할 것!
	panic("not implemented")
}

 

Error Handler 작성

 예상할 수 있는 에러 메시지를 상수로 정의하고

응답 시 Http Status 코드를 조회하는 ErrStatusCode 메서드.

const (
	ErrUserNotFound                = "user not found"
	ErrUserAlreadyExists           = "user already exists"
	ErrInvalidPassword             = "invalid password"
	ErrTokenGenerationFailed       = "fail generate token"
	ErrAuthorizationHeaderRequired = "authorization header required"
	ErrInvalidBearerScheme         = "invalid Bearer scheme"
	ErrInvalidToken                = "invalid token"
)

func ErrStatusCode(err error) int {
	switch err.Error() {
	case ErrUserNotFound:
		return http.StatusNotFound
	case ErrUserAlreadyExists:
		return http.StatusBadRequest
	case ErrInvalidPassword:
		return http.StatusBadRequest
	case ErrTokenGenerationFailed:
		return http.StatusInternalServerError
	case ErrAuthorizationHeaderRequired:
		return http.StatusBadRequest
	case ErrInvalidBearerScheme:
		return http.StatusBadRequest
	case ErrInvalidToken:
		return http.StatusUnauthorized
	default:
		return http.StatusInternalServerError
	}
}

 

시나리오와 Use Case 를 바탕으로 전체적인 인터페이스를 구성해 보았는데

비지니스 요구사항은 끊임없이 변화하고, 기술적인 면에서도 변화가 많아서

초기 작업한 인터페이스가 끝까지 가는 경우는 없는 것 같다.

다음은 하나씩 구현해 보려고 한다.

전체 소스는 https://github.com/jongwon-hyun/go_auth_jwt

 

다음 글

 

[GO 인증 구현 with JWT 2] 토큰 구현 및 테스트 코드 작성

이전글 [GO 인증 구현 with JWT 1] 전체적인 인터페이스 작성 Use Case 간단히 구현해 볼 인증의 시나리오 유저 생성 → 생성된 유저 정보로 토큰 발급 → 발급된 토큰에 들어있는 유저ID 를 이용해서

tinkerbellbass.tistory.com

 

728x90
반응형

댓글