Az auth önálló használata
Az előző három oldalon minden működik a gpsystem kit nélkül, server.WriteError nélkül, és bármelyik más gp-system/* modul nélkül. Ez az oldal ennek az állításnak a konkrét változata: egy projekt, aminek a go.mod-ja csak a github.com/gp-system/auth-ot igényli, a saját net/http middleware-ét, saját szerep-ellenőrzését, saját policy-registryjét és saját hibarendelését kézzel drótozva.
go mod init example.com/standalone-demo
go get github.com/gp-system/auth
require github.com/gp-system/auth v0.1.0
Nincs errs, nincs httperr, nincs dbx, semmi más: az auth saját go.mod-ja csak a golang-jwt/jwt/v5-t igényli, és ez az egyetlen dolog, ami a tiedben is landol.
Token kibocsátás és ellenőrzés
package main
import (
"time"
"github.com/gp-system/auth"
)
var issuer = auth.NewTokenIssuer[auth.DefaultClaims](auth.Config{
Secret: "dev-secret-change-me", // valós kódban: env-ből/secret-kezelőből
AccessTTL: 15 * time.Minute,
Issuer: "standalone-demo",
})
func issueToken(userID, username string, roles, permissions []string) (string, error) {
claims := auth.NewDefaultClaims(auth.Config{Issuer: "standalone-demo", AccessTTL: 15 * time.Minute},
userID, username, roles, permissions)
return issuer.Sign(claims)
}
Kézzel írt net/http middleware
Az Identify végzi a parseolást és az identity-leképzést; te döntöd el, mi történjen sikertelenség esetén. Itt magunk írunk egy kis JSON body-t, errors.Is-szel választva státuszkódot, httperr nélkül:
package main
import (
"encoding/json"
"errors"
"net/http"
"github.com/gp-system/auth"
"github.com/gp-system/auth/rbac"
)
func requireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id, err := auth.Identify(issuer, auth.DefaultIdentity, r.Header.Get("Authorization"))
if err != nil {
writeAuthError(w, err)
return
}
next.ServeHTTP(w, r.WithContext(rbac.WithIdentity(r.Context(), id)))
})
}
func writeAuthError(w http.ResponseWriter, err error) {
status := http.StatusInternalServerError
switch {
case errors.Is(err, auth.ErrMissingToken),
errors.Is(err, auth.ErrInvalidToken),
errors.Is(err, auth.ErrInvalidClaims):
status = http.StatusUnauthorized
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
}
Ez nem az egyetlen lehetséges alak: egy middleware ugyanígy rendere lhet text/plain-t http.Error-ral, vagy újrahasznosíthatja a kódbázisod már meglévő hibakezelési konvencióját. Az auth-nak nincs erről véleménye; csak azt garantálja, hogy az errors.Is működik a három sentinel ellen.
Szerep-ellenőrzés middleware nélkül
Az rbac.CheckRole/CheckPermission sima függvények; közvetlenül hívhatók egy handlerben, guard middleware nélkül is:
func adminOnly(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := rbac.FromContext(r.Context())
if err := rbac.CheckRole(id, "admin"); err != nil {
status := http.StatusForbidden
if errors.Is(err, rbac.ErrUnauthenticated) {
status = http.StatusUnauthorized
}
http.Error(w, err.Error(), status)
return
}
next(w, r)
}
}
Fűzd be a requireAuth után:
mux := http.NewServeMux()
mux.Handle("/admin/reports", requireAuth(adminOnly(handleReports)))
http.ListenAndServe(":8080", mux)
Policy-registry, kézzel bekötve
A policy.Enforcer az oapi-codegen strict-server alakját célozza, de semmi nem akadályozza, hogy egy kézzel épített végponthoz közvetlenül egy Registry-t hívj:
package main
import (
"context"
"errors"
"net/http"
"github.com/gp-system/auth/policy"
"github.com/gp-system/auth/rbac"
)
var orderPolicies = func() *policy.Registry {
reg := policy.NewRegistry()
reg.Register("orders.view", func(_ context.Context, id *rbac.Identity, req any) error {
orderOwnerID := req.(string) // valós kódban: a rendelés-rekordból kikeresve
if id.HasRole("admin") || orderOwnerID == id.Subject {
return nil
}
return policy.Deny("you can only view your own orders")
})
return reg
}()
func handleGetOrder(w http.ResponseWriter, r *http.Request) {
id := rbac.FromContext(r.Context())
fn, _ := orderPolicies.Get("orders.view")
if err := fn(r.Context(), id, lookupOrderOwner(r)); err != nil {
var d *policy.Denial
if errors.As(err, &d) {
http.Error(w, d.Detail, http.StatusForbidden)
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
// ... a rendelés kiírása
}
Semmi HTTP: egy WebSocket handshake
Az Identify fejléc-értéket vár, nem *http.Request-et, így ugyanaz a hívás működik bárhol, ahol egy bearer token felbukkan: egy WebSocket handshake Sec-WebSocket-Protocol trükkjében, egy első üzenetben a socketen, gRPC metaadatban, bármiben.
func acceptConnection(authorizationValue string) (*rbac.Identity, error) {
id, err := auth.Identify(issuer, auth.DefaultIdentity, authorizationValue)
if err != nil {
return nil, err // a hívó dönti el, hogyan zárja a kapcsolatot / küldjön control frame-et
}
if err := rbac.CheckPermission(id, "realtime.connect"); err != nil {
return nil, err
}
return id, nil
}
Itt nincs mit response-ba írni, tehát nincs mit adaptálni: a fenti HTTP-példák errors.Is/errors.As szókincse változatlanul alkalmazható.
Amit a kit hozzáad
A gpsystem kit server csomagja (server.AuthMiddleware, server.RequireRole/RequirePermission, server.WriteError) pontosan a fenti requireAuth/adminOnly/hibarenderelő kód, egyszer megírva és minden generált projekt között megosztva, RFC 9457 problem+json-ként renderelve az itt bemutatott ad hoc JSON/plain-text helyett. Ehhez a bekötéshez lásd az Autentikáció oldalt. Az auth, az rbac és a policy semmiben nem változik, amikor hozzáadod a kitet: ugyanazokra a sentinelekre és ugyanarra a *policy.Denial-ra illeszkedik a kit glue-ja is.
Policy-k
Kérésenkénti, user-szintű engedélyezés a szerepek fölött: a kontraktusban deklarálva, generált kódból kikényszerítve.
Social login goth-tal, önállóan
A markbates/goth (Discord, Facebook, Google) bekötése egy sima net/http alkalmazásba, ami tokenekhez csak a gp-system/auth-ot igényli, gpsystem tooling nélkül.