우리는 간단하게 유저가 인증되었는 지 아닌 지를 authenticatedUserID
가 있는 지 없는 지로 체크하였다.
func (app *application) isAuthenticated(r *http.Request) bool {
return app.session.Exists(r, "authenticatedUserID")
}
여기에 추가하여 우리는 authenticatedUserID
를 database에서 조회하여 실제로 존재하는 데이터인지 아닌 지 확인해야한다. 또한, 해당 유저가 active한 유저인 지 아닌지도 확인하여야 한다.
그러나 여기에는 추가적으로 database check를 해야하는 약간의 문제가 있다. 우리의 isAuthenticated()
helper는 매 요청 사이클마다 계속해서 불린다. 이러는 와중에 계속해서 database에서 확인 요청을 한다면 overhead가 너무 크다.
대안으로 특정 middleware에서 현재의 요청이 인증되었는 지 또는 active인 상태인지를 확인한다음, 하위의 핸들러 chain에게 넘겨주어야 한다.
즉, request context
를 설정하고, request context
에서 인증 정보를 확인한 다음 handler에게 정보를 전달하면 된다.
handler를 처리하는 모든 http.Request
는 context.Context
객체를 포함하고 있다. 이는 request의 과정 동안에 정보를 저장할 수 있도록 하는 객체이다.
web application에서 context.Context
객체를 사용하는 대표적인 use-case는 middleware와 다른 handler들 사이에 정보(또는 데이터)를 전달하는 데 사용하는 것이다.
우리는 context.Context
를 사용하여 만약 user가 특정 middleware를 통해 인증되었고 active 상태인지 아닌지를 확인하고, 만약 그렇다면 인증되었다는 정보를 우리의 모든 middleware와 handler들에게 전달하여 사용할 수 있도록 한다.
요청의 context에 information을 추가하는 기본적인 코드는 다음과 같다.
ctx := r.Context()
ctx = context.WithValue(ctx, "isAuthenticated", true)
r = r.WithContext(ctx)
하나하나 확인해보도록 하자.
1. r.Context()
메서드는 request에 있는 context를 불러오고 ctx
변수에 할당해준다.
2. context.WithValue()
메서드를 사용하여 request에 있는 context의 새로운 copy를 만들고, key값으로 isAuthenticated
에 true
값을 넘겨준다.
3. 그런다음 마지막으로 r.WithContext()
메서드로 우리의 새로운 context를 포함한 request의 copy를 만든다.
직접적으로 request에 있는 context를 변경하지 않았다. 새로운 context
를 만들고 http.Request
객체의 새로운 copy를 만들었다.
위의 code를 좀 더 간단하게 만들면 다음과 같다.
ctx = context.WithValue(r.Context(), "isAuthenticated", true)
r = r.WithContext(ctx)
이제 request context에 데이터를 넣는 방법을 알게되었다. 그럼 어떻게 데이터를 가져올 수 있을까?
request context 값들은 interface{}
타입으로 저장된다. 이는 context로 부터 값을 가져오면 기존의 타입을 단언해주어야 한다는 것이다.
데이터를 가져오기 위해서는 r.Context().Value()
메서드를 사용하면 된다.
isAuthenticated, ok := r.Context().Value("isAuthenticated").(bool)
if !ok {
return errors.New("could not convert value to bool")
}
그런데 만약 context
에 isAuthenticated
키를 누군가 사용하고 있다면 어떻게될까? third-party application에서 isAuthenticated
을 사용하고 있으면 문제가 될 것이다. 때문에 이러한 충돌을 막기 위해 key값에 해당하는 새로운 type을 만드는 것이 좋다.
type contextKey string
const contextKeyIsAuthenticated = contextKey("isAuthenticated")
...
ctx := r.Context()
ctx = context.WithValue(ctx, contextKeyIsAuthenticated, true)
r = r.WithContext(ctx)
...
isAuthenticated, ok := r.Context().Value(contextKeyIsAuthenticated).(bool)
if !ok {
return errors.New("could not convert value to bool")
}
이제 request context 기능을 우리의 application에 추가하도록 하자.
pkg/models/mysql/users.go
파일에 가서, UserModel.Get()
메서드를 수정하도록 하여 database에서 특정 user를 가져오도록 하자.
pkg/models/mysql/users.go
...
func (m *UserModel) Get(id int) (*models.User, error) {
u := &models.User{}
stmt := `SELECT id, name, email, created, active FROM users WHERE id = ?`
err := m.DB.QueryRow(stmt, id).Scan(&u.ID, &u.Name, &u.Email, &u.Created, &u.Active)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, models.ErrNoRecord
} else {
return nil, err
}
}
return u, nil
}
...
이제 id
를 통해서 user 데이터를 가져올 수 있으므로, cmd/web/main.go
파일에 가서 우리의 custom contextKey
타입을 정의하고 contextKeyIsAuthenticated
변수를 만들자. 이를 통해 우리는 request context로 부터 unique한 값을 불러오고 저장할 수 있다.
cmd/web/main.go
type contextKey string
const contextKeyisAuthenticated = contextKey("isAuthenticated")
type application struct {
errorLog *log.Logger
infoLog *log.Logger
session *sessions.Session
snippets *mysql.SnippetModel
templateCache map[string]*template.Template
users *mysql.UserModel
}
이제 새로운 함수인 authenticate()
middleware를 만들어 user id를 session data로부터 fetch하도록 한다음 database로 부터 id가 valid하고 user가 active한 지를 확인하자. 이후 request context에 해당 데이터를 넣어주자.
cmd/web/middleware.go
...
func (app *application) authenticate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if a authenticatedUserID value exists in the session. If this *isn't
// present* then call the next handler in the chain as normal.
exists := app.session.Exists(r, "authenticatedUserID")
if !exists {
next.ServeHTTP(w, r)
return
}
// Fetch the details of the current user from the database. If no matching
// record is found, remove the (invalid) authenticatedUserID value from their
// session and call the next handler in the chain as normal.
user, err := app.users.Get(app.session.GetInt(r, "authenticatedUserID"))
if errors.Is(err, models.ErrNoRecord) {
app.session.Remove(r, "authenticatedUserID")
next.ServeHTTP(w, r)
return
} else if err != nil {
app.serverError(w, err)
return
}
// Likewise, if the the current user is has been deactivated remove the
// authenticatedUserID value from their session and call the next handler in
// the chain as normal.
if !user.Active {
app.session.Remove(r, "authenticatedUserID")
next.ServeHTTP(w, r)
return
}
// Otherwise, we know that the request is coming from a active, authenticated,
// user. We create a new copy of the request, with a true boolean value
// added to the request context to indicate this, and call the next handler
// in the chain *using this new copy of the request*.
ctx := context.WithValue(r.Context(), contextKeyisAuthenticated, true)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
...
다음의 코드를 분석해보면, 만약 우리는 인증된 또는 active user를 찾지못하면 원래 즉, 변화가 없는 *http.Request
를 다음 next handler chain에 전달한다.
만약 인증되고 active한 user를 찾으면 우리는 contextKeyIsAuthenticated
key에 인증되었다는 정보인 true
를 넣고 이전 request
의 복사본에 넣어준다. 그리고 *http.Requst
의 복사본을 다음 handler에 전달한다.
이제 middleware를 적용하기위해서 cmd/web/routes.go
파일에 가서 authenticate()
미들웨어를 dynamic
middleware 체인에 넣어주도록 하자.
cmd/web/routes.go
package main
import (
"net/http"
"github.com/bmizerany/pat"
"github.com/justinas/alice"
)
func (app *application) routes() http.Handler {
standardMiddleware := alice.New(app.recoverPanic, app.logRequest, secureHeaders)
// Add the authenticate() middleware to the chain.
dynamicMiddleware := alice.New(app.session.Enable, noSurf, app.authenticate)
authenticatedMiddleware := dynamicMiddleware.Append(app.requireAuthentication)
mux := pat.New()
mux.Get("/", dynamicMiddleware.ThenFunc(app.home))
mux.Get("/snippet/create", authenticatedMiddleware.ThenFunc(app.createSnippetForm))
mux.Post("/snippet/create", authenticatedMiddleware.ThenFunc(app.createSnippet))
mux.Get("/snippet/:id", dynamicMiddleware.ThenFunc(app.showSnippet))
// Add the five new routes.
mux.Get("/user/signup", dynamicMiddleware.ThenFunc(app.signupUserForm))
mux.Post("/user/signup", dynamicMiddleware.ThenFunc(app.signupUser))
mux.Get("/user/login", dynamicMiddleware.ThenFunc(app.loginUserForm))
mux.Post("/user/login", dynamicMiddleware.ThenFunc(app.loginUser))
mux.Post("/user/logout", authenticatedMiddleware.ThenFunc(app.logoutUser))
fileServer := http.FileServer(http.Dir("./ui/static/"))
mux.Get("/static/", http.StripPrefix("/static", fileServer))
return standardMiddleware.Then(mux)
}
이제 middleware를 넣어주었으니 마지막으로 해줘야할 것은 isAuthenticated()
helper 함수를 수정하여야 한다. request context를 통해서 해당 데이터가 인증되었는 지 아닌 지를 확인하기만 하면 된다.
/cmd/web/helpers.go
...
func (app *application) isAuthenticated(r *http.Request) bool {
isAuthenticated, ok := r.Context().Value(contextKeyisAuthenticated).(bool)
if !ok {
return false
}
return isAuthenticated
}
context
에 contextKeyIsAuthenticated
key로 value가 없거나, value가 bool타입이 아닌 지에 대해서 처리해주어야 한다. 이는 인증에 실패한 것이므로 false
라고 처리해주어야 한다.
추가적으로 request에 있는 context
는 request chain process 이외에 다른 부분에 사용해서는 안된다. 다음은 go docs에서 context.Context
에 대해 경고한 것이다.
Use context Values only for reuqest-scoped data that trainsits processes and APIs.
즉, context
를 request 생애주기 이외의 다른 객체의 dependencies로 넘기지말라는 것이다. 즉, 우리가 cache나 logger, connection pool을 dependencies로 관리하였지만 context는 그렇게 관리하지 말라는 것이다.
이는 context
가 request chain에서 벗어나게되면 코드의 디버깅이 어려워지고 clarity가 무너지기 때문이다. 즉, request에 국한되지 않은 context는 의존성으로 관리되어 다른 request가 또 다른 request에 영향을 주는 문제가 있기 때문이다.