Autentikáció
Importáld az auth-ot, hogy JWT-t bocsáss ki, és bearer tokenből identityt csinálj:
import "github.com/gp-system/auth"
Az auth egy önálló, stateless JWT-modul, semmilyen más gp-system csomagtól nem függ, még az errs-től vagy a httperr-től sem: az egyetlen függősége a golang-jwt/jwt/v5. Két dolgot ad: egy token-kibocsátót (JWT aláírás és ellenőrzés) és az Identify-t, ami egy bearer tokenből rbac.Identity-t csinál. Innen minden authorizációs döntés (a szerep- és permission-ellenőrzés, a policy-k) már a tokentől függetlenül, az Identity fölött történik.
auth sosem ír HTTP választ, és az Identify még csak *http.Request-et sem vár: egy fejléc-értéket vár. Ez bármilyen transportról használhatóvá teszi, nem csak net/http-ről: egy WebSocket handshake-ből, egy gRPC interceptorból, ami metaadatot olvas, egy üzenetsor-fogyasztóból, ami a tokent egy üzenetfejlécben ellenőrzi. Minden hiba egy sima Go error (sentinel vagy kis típusos érték); a hívó dönti el, hogyan jelenítse meg. Teljes, futtatható példákért lásd az Önálló használat oldalt, HTTP-vel és anélkül is. A gpsystem kit erre épít rá egy net/http middleware-t és RFC 9457 Problem-renderelést a server csomagjában.Az autentikáció alapja itt a stateless JWT: a login végpont aláírt tokent ad, minden további kérés a tokent hordozza, és a szerver semmilyen session-tárat nem tart: a token önmagában hordozza a subjectet, a szerepeket és a jogosultságokat.
jwt.Claims interfészt implementálják, a saját claims struct a jwt.RegisteredClaims-t ágyazza be. A modul itt centralizál, nem absztrahál: a JWT-ökoszisztéma tudásod egy az egyben használható.Telepítés
go get github.com/gp-system/auth@v0.1.0 # a kit is ezt a taget használja
# vagy: go get github.com/gp-system/auth@latest
Az auth egyetlen függősége a golang-jwt/jwt/v5; az auth/rbac és az auth/policy alcsomag ugyanabban a modulban szállít, külön go get nem kell hozzájuk. Semmi nem igényli itt a gpsystem kitet vagy bármelyik más gp-system modult: lásd az Önálló használat oldalt egy olyan projekthez, ami csak a github.com/gp-system/auth-ot importálja.
Konfiguráció
Az auth.Config env-ből töltődik; a generált projekt configjában a // gpsystem:config anchor alá teszed, JWT_ prefixszel:
// internal/platform/config/config.go
type Config struct {
Server server.Config
DB dbx.Config `envPrefix:"DB_"`
JWT auth.Config `envPrefix:"JWT_"`
// gpsystem:config
}
| Env-változó | Mező | Alapértelmezés |
|---|---|---|
JWT_SECRET | Secret | kötelező |
JWT_ACCESS_TOKEN_TTL | AccessTTL | 15m |
JWT_REFRESH_TOKEN_TTL | RefreshTTL | 168h (7 nap) |
JWT_ISSUER | Issuer | üres (az issuer-ellenőrzés kikapcsolva) |
JWT_AUDIENCE | Audience | üres (az audience-ellenőrzés kikapcsolva) |
Issuer-t és az Audience-t, és service-enként és környezetenként különSecret-et használj: így egy másik service-nek vagy a staging környezetnek kiállított token nem játszható vissza a production API ellen. Ha az Issuer/Audience be van állítva, a parse megköveteli az egyező iss/aud claimet. A teljes env-referencia: Konfiguráció.Token-kibocsátás: a login flow
A kit standard claims-alakja a DefaultClaims: a rbac.Identity mezőivel kompatibilis, és a jwt.RegisteredClaims-t ágyazza be:
type DefaultClaims struct {
Username string `json:"username,omitempty"`
Roles []string `json:"roles,omitempty"`
Permissions []string `json:"permissions,omitempty"`
jwt.RegisteredClaims
}
A NewDefaultClaims a registered claimeket (iat, exp, iss, sub, és ha konfigurált, aud) a configból bélyegzi ki; a TokenIssuer írja alá és parseolja a tokent. Így néz ki a shop login service-e vázlatosan:
// internal/modules/shop/surfaces/admin/service/auth.go (vázlat)
type Service struct {
jwt auth.Config
issuer *auth.TokenIssuer[auth.DefaultClaims]
users UserRepo
tokens RefreshTokenRepo
}
func New(jwtCfg auth.Config, users UserRepo, tokens RefreshTokenRepo) *Service {
return &Service{
jwt: jwtCfg,
issuer: auth.NewTokenIssuer[auth.DefaultClaims](jwtCfg),
users: users,
tokens: tokens,
}
}
func (s *Service) Login(ctx context.Context, email, password string) (LoginResult, error) {
user, err := s.users.FindByEmail(ctx, email)
if err != nil || !user.CheckPassword(password) {
return LoginResult{}, ErrInvalidCredentials
}
claims := auth.NewDefaultClaims(s.jwt, user.ID, user.Name,
user.Roles, user.Permissions) // iat/exp/iss/sub kitöltve a configból
access, err := s.issuer.Sign(claims)
if err != nil {
return LoginResult{}, err
}
refresh, err := auth.NewRefreshToken() // 32 bájt crypto/rand, hexben
if err != nil {
return LoginResult{}, err
}
expiresAt := time.Now().Add(s.jwt.RefreshTTL)
if err := s.tokens.Store(ctx, user.ID, sha256Hex(refresh), expiresAt); err != nil {
return LoginResult{}, err
}
return LoginResult{AccessToken: access, RefreshToken: refresh}, nil
}
NewRefreshToken csak egy kriptográfiailag véletlen stringet ad, a kit nem tárolja. Szerveroldalon csak a hash-ét tárold (mint a jelszavaknál), és a RefreshTTL-t használd lejáratnak (a fenti sha256Hex a te helper-ed).Identify: tokenből identity
Az Identify azt a munkát végzi, amit egy middleware általában elrejt: kiveszi a bearer tokent egy Authorization fejléc-értékből, a TokenIssuer-rel validálja (aláírás, lejárat, issuer/audience), és a claimekből *rbac.Identity-t épít. Fejléc-értéket vár, nem requestet vagy response writert, és sikertelenség esetén sima errort ad vissza:
id, err := auth.Identify(issuer, auth.DefaultIdentity, authorizationHeader)
switch {
case err == nil:
// id kitöltve; mehet tovább
case errors.Is(err, auth.ErrMissingToken):
// nincs bearer token
case errors.Is(err, auth.ErrInvalidToken):
// rossz aláírás, lejárt, rossz issuer/audience
case errors.Is(err, auth.ErrInvalidClaims):
// a token érvényes, de a toIdentity elutasította a claimeket
}
Az auth.DefaultIdentity a DefaultClaims toIdentity függvénye; egy saját claims-típushoz saját toIdentity-t írsz (lásd Saját claims alább).
A kittel: server.AuthMiddleware
A gpsystem kit becsomagolja az Identify-t egy net/http middleware-be, és a hibáit RFC 9457 problem+json válaszként rendereli. A shop admin surface-e így kerül JWT + szerepvédelem mögé: a middleware-t a generált Register<Surface> függvénybe fűzöd be, a többi surface érintése nélkül (lásd add surface):
func RegisterAdmin(router chi.Router, deps Dependencies) {
adminSvc := adminservice.New()
adminHandler := adminhttp.New(adminSvc)
adminPolicies := adminpolicy.New()
router.Route("/admin/shop", func(r chi.Router) {
r.Use(server.AuthMiddleware(deps.Auth), server.RequireRole("admin"))
shopadmingen.HandlerWithOptions(/* ... a generált strict-handler bekötés változatlan ... */)
})
}
Az auth.Config-ot a modul Dependencies structján keresztül adod be (a // gpsystem:dependencies anchornál egy Auth auth.Config mező, a main.go-ban Auth: cfg.JWT). Így a register.go nem függ a platform-configtól. A server.AuthMiddleware/server.RequireRole/server.RequirePermission a kitben él, nem az auth-ban: a teljes server.WriteError leképzésért lásd a kit servere oldalt.
Vegyes surface-ök: publikus és védett műveletek egymás mellett
A server.AuthMiddleware minden token nélküli kérést 401-el; egy csupa-védett surface-re (mint az admin) pont ez kell. A shop api surface-e viszont vegyes: a terméklistázás anonim, a rendelés-műveletek viszont identityt igényelnek. Ilyenkor a token-parseolást tedd feltételessé, a taggelt műveletek 401-ét pedig bízd a policy-enforcerre, ami identity nélkül úgyis elutasít:
authOptional := server.AuthMiddleware(deps.Auth)
r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == "" {
next.ServeHTTP(w, r) // anonim böngészés mehet; a taggelt műveleteket az enforcer 401-eli
return
}
authOptional(next).ServeHTTP(w, r) // ha van token, legyen érvényes
})
})
Saját claims
Ha a DefaultClaims kevés (például tenant-azonosítót hordozol a tokenben), definiáld a saját claims-típusodat, és írj saját toIdentity függvényt az Identify-hoz (vagy a server.AuthMiddlewareFor-hoz, ami ennek a kit-drótozott net/http párja):
type MyClaims struct {
TenantID string `json:"tenant"`
jwt.RegisteredClaims
}
toIdentity := func(c MyClaims) (*rbac.Identity, error) {
return &rbac.Identity{
Subject: c.Subject,
Extra: map[string]any{"tenant": c.TenantID},
}, nil
}
issuer := auth.NewTokenIssuer[MyClaims](cfg.JWT)
mw := server.AuthMiddlewareFor(issuer, toIdentity) // kit net/http middleware
id, err := auth.Identify(issuer, toIdentity, authorizationHeader) // vagy közvetlenül hívva
A server.AuthMiddleware pontosan a server.AuthMiddlewareFor, DefaultClaims-szel és auth.DefaultIdentity-vel előre bekötve, így mindkettő ugyanazt a func(http.Handler) http.Handler alakot és Identity-építési kontraktust osztja; az AuthMiddlewareFor-hoz csak akkor nyúlj, ha a DefaultClaims nem elég.
HS256 és a titok kezelése
Az aláírás HS256 (megosztott titok). A parse rögzíti az algoritmust (WithValidMethods), megköveteli a lejáratot (WithExpirationRequired), és ha konfigurált, ellenőrzi az issuert és az audience-t, így az alg-confusion támadások és az aláíratlan tokenek elutasítódnak. A TokenIssuer API claims-generikus, ezért aszimmetrikus algoritmusok később törés nélkül jöhetnek.
A titok legyen hosszú és véletlen (pl. openssl rand -hex 32), és secret-kezelőből érkezzen: sose kerüljön a repóba. A .env csak lokális fejlesztésre való; lásd a konfiguráció oldalt.
Önállóan, kit nélkül
A fentiekhez semmi nem kell a server.Run-ból vagy bármelyik más kit-csomagból. Az Identify és az rbac/policy sentinel errorok minden, ami egy csupasz net/http service-hez (vagy egy nem-HTTP-shez) kell, saját hibarendereléssel: teljes, futtatható példákért lásd az Önálló használat oldalt.
A következő réteg: a tokenből lett Identity fölötti szerep- és permission-ellenőrzések, és a kérésenkénti policy-k. 3rd-party (Discord/Facebook/Apple/Google) bejelentkezéshez az add auth generált moduljának saját social login rétege tartozik, ugyanezt a token-kibocsátást hívva.
Használt patternek
Generikusok a claims-típus fölött (TokenIssuer[C jwt.Claims], Identify[C]): Design patternek.