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 의 자세한 내용은 공식 홈에서.
인증 미들웨어 작성
이번에는 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
다음 글
'Programming > Go' 카테고리의 다른 글
[GO 인증 구현 with JWT 3] 인증 미들웨어 구현 및 테스트 코드 작성 (0) | 2022.04.11 |
---|---|
[GO 인증 구현 with JWT 2] 토큰 구현 및 테스트 코드 작성 (0) | 2022.04.11 |
Go 로 MinIO 에 파일 업다운로드 (0) | 2022.04.06 |
고 채널을 이용해서 옵저버 패턴 구현해보기 (0) | 2022.04.05 |
자바 개발자의 고 적응기 (0) | 2022.04.03 |
댓글