















const ( ScopeActivation = "activation" ScopeAuthentication = "authentication" ) // Token holds the data for a token. type Token struct { Plaintext string `json:"token"` Hash []byte `json:"-"` UserID int64 `json:"-"` Expiry time.Time `json:"expiry"` Scope string `json:"-"` }
token.go:
package main import ( "errors" "net/http" "time" "greenlight.zzh.net/internal/data" "greenlight.zzh.net/internal/validator" ) func (app *application) createAuthenticationTokenHandler(w http.ResponseWriter, r *http.Request) { var input struct { Email string `json:"email"` Password string `json:"password"` } err := app.readJSON(w, r, &input) if err != nil { app.badRequestResponse(w, r, err) return } v := validator.New() data.ValidateEmail(v, input.Email) data.ValidatePassword(v, input.Password) if !v.Valid() { app.failedValidationResponse(w, r, v.Errors) return } user, err := app.models.User.GetByEmail(input.Email) if err != nil { switch { case errors.Is(err, data.ErrRecordNotFound): app.invalidCredentialsResponse(w, r) default: app.serverErrorResponse(w, r, err) } return } match, err := user.Password.Matches(input.Password) if err != nil { app.serverErrorResponse(w, r, err) return } if !match { app.invalidCredentialsResponse(w, r) return } token, err := app.models.Token.New(user.ID, 24*time.Hour, data.ScopeAuthentication) if err != nil { app.serverErrorResponse(w, r, err) return } err = app.writeJSON(w, http.StatusCreated, envelope{"authentication_token": token}, nil) if err != nil { app.serverErrorResponse(w, r, err) } }
zzh@ZZHPC:~$ BODY='{"email": "ZhangZhihuiAAA@126.com", "password": "pa55word"}'
zzh@ZZHPC:~$ curl -i -d "$BODY" localhost:4000/v1/tokens/authentication
HTTP/1.1 201 Created
Content-Type: application/json
Date: Fri, 22 Nov 2024 02:50:12 GMT
Content-Length: 143
{
"authentication_token": {
"token": "TXRO7FJ6L7XBXFK7O7SG4LWULM",
"expiry": "2024-11-23T10:50:12.324997803+08:00"
}
}
zzh@ZZHPC:~$ BODY='{"email": "alice@example.com", "password": "wrong pa55word"}'
zzh@ZZHPC:~$ curl -i -d "$BODY" localhost:4000/v1/tokens/authentication
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Date: Fri, 22 Nov 2024 02:56:01 GMT
Content-Length: 54
{
"error": "invalid authentication credentials"
}
greenlight=> SELECT * FROM token WHERE scope = "authentication";
ERROR: column "authentication" does not exist
LINE 1: SELECT * FROM token WHERE scope = "authentication";
^
greenlight=> SELECT * FROM token WHERE scope = 'authentication';
hash | user_id | expiry | scope
--------------------------------------------------------------------+---------+------------------------+----------------
\xd8a13a29dc02a6b756887f61e21fecee3f83f66a936abd48a60e990d3c0c9343 | 9 | 2024-11-23 02:50:12+00 | authentication
(1 row)




var AnonymousUser = &User{} // User represents an individual user. type User struct { ID int64 `json:"id"` CreatedAt time.Time `json:"created_at"` Name string `json:"name"` Email string `json:"email"` Password password `json:"-"` Activated bool `json:"activated"` Version int `json:"-"` } // IsAnonymous checks if a User instance is the AnonymousUser. func (u *User) IsAnonymous() bool { return u == AnonymousUser }


package main import ( "context" "net/http" "greenlight.zzh.net/internal/data" ) type glContextKey string // Convert the string "user" to a glContextKey type and assign it to the userContextKey constant. // We'll use this constant as the key for getting and setting user information in the request // context. const userContextKey = glContextKey("user") // contextSetUser returns a new copy of the request with the provided User struct added to its // embedded context. func (app *application) contextSetUser(r *http.Request, user *data.User) *http.Request { ctx := context.WithValue(r.Context(), userContextKey, user) return r.WithContext(ctx) } // contextGetUser retrieves the User struct from the request context. The only time that we'll use // this helper is when we logically expect there to be User struct value in the context, and if it // doesn't exist it will firmly be an 'unexpected' error. It's OK to panic in those circumstances. func (app *application) contextGetUser(r *http.Request) *data.User { user, ok := r.Context().Value(userContextKey).(*data.User) if !ok { panic("missing user value in request context") } return user }
In middleware.go:
func (app *application) authenticate(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Add the "Vary: Authorization" header to the response. This indicates to any caches that // the response may vary based on the value of the Authorization header in the request. w.Header().Add("Vary", "Authorization") // Retrieve the value of the Authorization header from the request. // This will return the empty string "" if there is no such header. authorizationHeader := r.Header.Get("Authorization") // If there is no Authorization header found, add the AnonymousUser to the request // context. Then we call the next handler in the chain and return without executing // any of the code below. if authorizationHeader == "" { r = app.contextSetUser(r, data.AnonymousUser) next.ServeHTTP(w, r) return } // Otherwise, try to split the Authorization header into its constituent parts. If the // header isn't in the expected format, we return a 401 Unauthorized response. headerParts := strings.Split(authorizationHeader, " ") if len(headerParts) != 2 || headerParts[0] != "Bearer" { app.invalidAuthenticationTokenResponse(w, r) return } token := headerParts[1] v := validator.New() if data.ValidateTokenPlaintext(v, token); !v.Valid() { app.invalidAuthenticationTokenResponse(w, r) return } user, err := app.models.User.GetForToken(data.ScopeAuthentication, token) if err != nil { switch { case errors.Is(err, data.ErrRecordNotFound): app.invalidAuthenticationTokenResponse(w, r) default: app.serverErrorResponse(w, r, err) } return } r = app.contextSetUser(r, user) next.ServeHTTP(w, r) }) }
In routes.go:
// Wrap the router with middleware. return app.recoverPanic(app.rateLimit(app.authenticate(router)))
In errors.go:
func (app *application) invalidAuthenticationTokenResponse(w http.ResponseWriter, r *http.Request) { w.Header().Set("WWW-Authenticate", "Bearer") message := "invalid or missing authentication token" app.errorResponse(w, r, http.StatusUnauthorized, message) }


zzh@ZZHPC:~$ curl -i localhost:4000/v1/healthcheck
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Authorization
Date: Fri, 22 Nov 2024 04:05:42 GMT
Content-Length: 123
{
"status": "available",
"system_info": {
"environment": "development",
"version": "1.0.0"
}
}

zzh@ZZHPC:~$ curl -i -d '{"email": "ZhangZhihuiAAA@126.com", "password": "pa55word"}' localhost:4000/v1/tokens/authentication
HTTP/1.1 201 Created
Content-Type: application/json
Vary: Authorization
Date: Fri, 22 Nov 2024 04:08:02 GMT
Content-Length: 143
{
"authentication_token": {
"token": "RULKBNEGFJBHRE3ADD7HUGVHSQ",
"expiry": "2024-11-23T12:08:02.227320413+08:00"
}
}
zzh@ZZHPC:~$ curl -i -H "Authorization: Bearer RULKBNEGFJBHRE3ADD7HUGVHSQ" localhost:4000/v1/healthcheck
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Authorization
Date: Fri, 22 Nov 2024 04:09:29 GMT
Content-Length: 123
{
"status": "available",
"system_info": {
"environment": "development",
"version": "1.0.0"
}
}

zzh@ZZHPC:~$ curl -i -H "Authorization: Bearer XXXXXXXXXXXXXXXXXXXXXXXXXX" localhost:4000/v1/healthcheck
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Vary: Authorization
Www-Authenticate: Bearer
Date: Fri, 22 Nov 2024 04:11:55 GMT
Content-Length: 59
{
"error": "invalid or missing authentication token"
}
zzh@ZZHPC:~$ curl -i -H "Authorization: INVALID" localhost:4000/v1/healthcheck
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Vary: Authorization
Www-Authenticate: Bearer
Date: Fri, 22 Nov 2024 04:12:24 GMT
Content-Length: 59
{
"error": "invalid or missing authentication token"
}

浙公网安备 33010602011771号