이번 게시글에서는 golang echo framework에 jwt 인증방식을 적용시키는 방법에 대해 정리합니다.
먼저 jwt에 대해 간단하게 알아보고 기성 인증방식인 세션방식과 비교한 후 소스코드를 정리합니다.
1. JWT란
JWT(Json Web Token)란 cliam 기반의 web token으로 토큰자체에 정보를 포함해 인증 인가를 처리하는데 사용합니다.
2. 기성 인증방식(세션)과의 차이
기성 인증방식은 서버가 한대라면 세션에 사용자 정보를 담고 보관하다가 인증이 필요한 요청에 세션정보를 확인합니다.
서버가 여러대라면 시간복잡도가 좋은 디비(redis)를 뒤져 세션정보를 찾아 인증하는 형식입니다.
그러나 jwt는 secret키를 알고있으면 어느 서버에서나 인증, 인가를 수행할 수 있는 state less 방식입니다.
여러대의 서버 인스턴스가 secret키만 알고있으면 빠르게 인증가능합니다.
3. JWT 구성요소
JWT는 Header, Payload, Signiture 3개의 파트가 있으며 json 형태로 표현되어 있습니다.
이를 base64url encoder로 인코딩해서 jwt를 만듭니다.
(1). Header
jwt는 json 데이터를 이용하므로 json으로 암호화 알고리즘과 type을 지정합니다.
암호화 알고리즘은 HS256, RSA 등을 많이 사용하며 HS256은 대칭키 암호화 방법으로 jwt 토큰 발급시 일반적으로 사용된다고 알려져 있습니다.
{
"alg": "HS256",
"typ": "JWT"
}
(2). Payload
payload에는 claim정보가 들어갑니다.
등록된(약속된) 클레임은 아래와 같습니다.
필요에 따라 key-value 데이터를 추가해 공개 클레임으로 사용할 수 있습니다.
{
"iss": "토큰발급자",
"sub": "토큰이름",
"aud": "토큰대상자",
"exp": 만료시간(unix time),
"nbf": 토큰이 활성화될 시간(unix time),
"iat": 발급된 시간(unix time),
"jti": "중복방지를 위한 id"
}
(3). Signiture
비밀키를 가지며 헤더에서 정한 알고리즘을 이용해 암호화 한 후 base64url로 인코딩해주는 역활을 합니다.
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
4. golang echo framework를 사용한 구현
필수요소만 채워 JWT를 발급하는 예시입니다.
header에 지정하는 알고리즘은 HS256을 사용하며
{
"alg": "HS256",
"typ": "JWT"
}
payload의 claim에는 등록된(약속된) claim인 만료시간과 custom claim으로 사용자 이름을 넣는 서버입니다.
{
"user_name": "admin",
"exp": 1686041503
}
5. 프로젝트 구조
├── client
│ ├── go.mod
│ └── main.go
├── go.work
├── go.work.sum
└── server
├── auth
│ └── auth.go
├── env
├── go.mod
├── go.sum
├── handler
│ ├── auth_handler.go
│ ├── default_handler.go
│ └── user_handler.go
└── main.go
6. auth/auth.go
jwt 발급 코드입니다.
package auth
import (
"github.com/golang-jwt/jwt"
"github.com/labstack/echo"
"net/http"
"os"
"time"
)
type UserAndPassword struct {
Username string `json:"username"`
Password string `json:"password"`
}
type MyClaim struct {
UserName string `json:"user_name"`
jwt.StandardClaims
}
func Login(c echo.Context) error {
loginInfo := UserAndPassword{}
err := c.Bind(&loginInfo)
if len(loginInfo.Username) < 1 || len(loginInfo.Password) < 1 {
return echo.ErrBadRequest
}
if !VerifyUser(loginInfo) {
return echo.ErrUnauthorized
}
// 본 게시글에 2번 항목에 있는 payload를 채우는 로직입니다.
// 토큰의 만료시간을 지정해 claims를 지정합니다.
// 사용자가 로그아웃하는경우 만료시간까지 해당토큰으로 접근하지 못하게 조치해줘야 합니다.
// go jwt에서 제공하는 standard claims는 아래와 같으며 발행자, 주제 등을 설정할 수 있지만 필수는 아닙니다.
// 사용자 클레임을 추가할 수도 있으며 예시에서는 username을 추가했습니다.
//type StandardClaims struct {
// Audience string `json:"aud,omitempty"`
// ExpiresAt int64 `json:"exp,omitempty"`
// Id string `json:"jti,omitempty"`
// IssuedAt int64 `json:"iat,omitempty"`
// Issuer string `json:"iss,omitempty"`
// NotBefore int64 `json:"nbf,omitempty"`
// Subject string `json:"sub,omitempty"`
//}
claims := &MyClaim{
loginInfo.Username,
jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Minute * 1).Unix(),
},
}
// 본 게시글의 header에 설명된 암호화 알고리즘을 채우는 부분입니다.
// 암호화 알고리즘은 HS256, RSA를 많이 사용하며
// HS256은 대칭키 암호화 방법으로 jwt 토큰 발급시 일반적으로 사용됩니다.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 본 게시글의 signiture 부분입니다.
// jwt는 secret키를 알고있으면 어느 서버에서나 인증, 인가를 수행할 수 있는 stateless 방식입니다.
// 즉 디비를 뒤져 세션정보를 찾아 인증하는 형식이 아니므로
// 여러대의 서버 인스턴스가 secret키만 알고있으면 빠르게 인증가능합니다.
// secret키는 절대 유출되면 안됩니다.
// 예시에서는 .env파일을 통해 환경변수를 로드했으나
// 컨테이너 빌드나, 오케스트레이션 배포시 env를 주입해주거나 신뢰할수 있는 안전한 저장소에 저장합니다.
signedToken, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
if err != nil {
return err
}
return c.JSON(http.StatusOK, echo.Map{
"token": signedToken,
})
}
func VerifyUser(userInfo UserAndPassword) bool {
// 예시를 위해 admin만 로그인에 성공시켰지만
// 회원가입시 encoder 라이브러리를 사용해 암호화된 비밀번호를 db에 저장해놓고
// 로그인시 입력한 비밀번호가 암호화된 비밀번호가 될수있는지를 확인하는게 일반적입니다.
// https통신은 비대칭키를 사용한 암호화 방식으로 1차적으로 외부에서 확인할 수 없으나
// 클라이언트의 요청도 개발자도구에서 보이지 않도록 base64등으로 인코딩하거나 암호화를 하는게 좋습니다.
if userInfo.Username == "admin" && userInfo.Password == "admin12!@" {
return true
}
return false
}
7. handler/user_handler.go
원하는 api그룹에 jwt인증을 하는 middleware를 끼워줍니다.
클라이언트가 auth헤더에 토큰을 보내주면 토큰의 claim을 확인하여 본인의 이름을 내려주는 api입니다.
package handler
import (
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"net/http"
"os"
"server/auth"
)
type UserHandler struct{}
func (UserHandler) Init(e *echo.Echo) {
authApiGroup := e.Group("/api/user")
// 에코 프레임워크를 사용하여 secret과 헤더의 알고리즘을 이용해 인증합니다.
authApiGroup.Use(middleware.JWTWithConfig(middleware.JWTConfig{
Claims: &auth.MyClaim{},
SigningKey: []byte(os.Getenv("JWT_SECRET")),
}))
authApiGroup.GET("/me", UserHandler{}.getUserInfo)
}
func (UserHandler) getUserInfo(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*auth.MyClaim)
name := claims.UserName
return c.JSON(http.StatusOK, echo.Map{"name": name})
}
8. 클라이언트 실행 결과
client/main.go 를 실행하면 로그인해서 토큰을 발급 받은후 header를 채워 사용자 정보를 가져옵니다.
9. 서버가 발급해준 토큰을 직접 확인
클라이언트에서 발급한 토큰을 jwt 토큰 디코딩 사이트에 가서 디코딩하면 토큰의 세부정보를 확인할 수 있습니다.
사용자 이름이 담겨있는걸 확인할 수 있습니다.
'golang' 카테고리의 다른 글
[golang] golang reverse proxy 예제 (0) | 2023.04.06 |
---|---|
[golang] gqlgen을 사용하여 golang graphql 서버 구축하기(마무리) (0) | 2022.12.10 |
[golang] gqlgen을 사용하여 golang graphql 서버 구축하기(3) (0) | 2022.12.08 |
[golang] gqlgen을 사용하여 golang graphql 서버 구축하기(2) (0) | 2022.12.07 |
[golang] gqlgen을 사용하여 golang graphql 서버 구축하기(1) (0) | 2022.12.07 |
댓글