Autenticazione¶
L'autenticazione è la verifica dell'identità di un utente. Ciò può avvenire attraverso la verifica di credenziali come un nome utente e una password o un tramite un token. L'autenticazione (talvolta chiamata auth/c) si distingue dall'autorizzazione (auth/z), che è l'atto di verificare i permessi di un utente precedentemente autenticato per permettergli di eseguire determinate operazioni.
Introduzione¶
Le API di autenticazione di Vapor supportano l'autenticazione di un utente tramite l'intestazione Authorization
della richiesta, utilizzando le autorizzazioni Basic e Bearer. È supportata anche l'autenticazione di un utente tramite la decodifica dei dati dall'API Content.
L'autenticazione viene implementata creando un Authenticator
che contiene la logica di verifica. Un autenticatore può essere utilizzato per proteggere singoli gruppi di route o un'intera applicazione. Vapor fornisce i seguenti autenticatori:
Protocollo | Descrizione |
---|---|
RequestAuthenticator /AsyncRequestAuthenticator |
Autenticatore di base in grado di creare middleware. |
BasicAuthenticator /AsyncBasicAuthenticator |
Autenticatore che verifica l'header Basic. |
BearerAuthenticator /AsyncBearerAuthenticator |
Autenticatore che verifica l'header Bearer. |
CredentialsAuthenticator /AsyncCredentialsAuthenticator |
Autenticatore che verifica le credenziali decodificate dal contenuto della richiesta. |
Se l'autenticazione ha successo, l'autenticatore aggiunge l'utente verificato a req.auth
. Si può quindi accedere a questo utente usando req.auth.get(_:)
nelle route protette dall'autenticatore. Se l'autenticazione fallisce, l'utente non viene aggiunto a req.auth
e qualsiasi tentativo di accesso fallirà.
Authenticatable¶
Per utilizzare l'API di autenticazione, ti occorre innanzitutto un tipo di utente conforme ad Authenticatable
. Questo può essere una struct
, una class
o anche un Model
Fluent. Gli esempi seguenti assumono una semplice struttura User
che ha una sola proprietà: name
.
import Vapor
struct User: Authenticatable {
var name: String
}
Ogni esempio che segue utilizzerà un'istanza di un autenticatore che abbiamo creato. In questi esempi, lo abbiamo chiamato UserAuthenticator
.
Route¶
Gli autenticatori sono middleware e possono essere utilizzati per proteggere le route.
let protected = app.grouped(UserAuthenticator())
protected.get("me") { req -> String in
try req.auth.require(User.self).name
}
Per recuperare l'utente autenticato, viene usato il metodo req.auth.require
. Se l'autenticazione fallisce, questo metodo lancia un errore, proteggendo la route.
Middleware di Guardia¶
Puoi anche usare GuardMiddleware
nel gruppo di route, per assicurarsi che un utente sia stato autenticato prima di raggiungere il gestore di route.
let protected = app.grouped(UserAuthenticator())
.grouped(User.guardMiddleware())
La richiesta di autenticazione non viene effettuata dal middleware dell'autenticatore per consentire la composizione degli autenticatori. Per saperne di più sulla composizione leggi più sotto.
Basic¶
L'autenticazione di base invia un nome utente e una password nell'intestazione Authorization
. Il nome utente e la password sono concatenati con i due punti (ad esempio, test:secret
), codificati in base 64 e preceduti da "Basic"
. La seguente richiesta di esempio codifica il nome utente test
con la password secret
.
GET /me HTTP/1.1
Authorization: Basic dGVzdDpzZWNyZXQ=
In genere, l'autenticazione di base viene utilizzata una sola volta per registrare un utente e generare un token. Questo riduce al minimo la frequenza di invio della password sensibile dell'utente. Non si dovrebbe mai inviare l'autorizzazione di base in chiaro o su una connessione TLS non verificata.
Per implementare l'autenticazione di base nella tua applicazione, puoi creare un nuovo autenticatore conforme a BasicAuthenticator
. Di seguito è riportato un esempio di autenticatore codificato per verificare la richiesta di cui sopra.
import Vapor
struct UserAuthenticator: BasicAuthenticator {
typealias User = App.User
func authenticate(
basic: BasicAuthorization,
for request: Request
) -> EventLoopFuture<Void> {
if basic.username == "test" && basic.password == "secret" {
request.auth.login(User(name: "Vapor"))
}
return request.eventLoop.makeSucceededFuture(())
}
}
Se stai usando async
/await
, puoi usare AsyncBasicAuthenticator
:
import Vapor
struct UserAuthenticator: AsyncBasicAuthenticator {
typealias User = App.User
func authenticate(
basic: BasicAuthorization,
for request: Request
) async throws {
if basic.username == "test" && basic.password == "secret" {
request.auth.login(User(name: "Vapor"))
}
}
}
Questo protocollo richiede che implementi il metodo authenticate(basic:for:)
, che sarà richiamato quando una richiesta in arrivo contiene l'intestazione Authorization: Basic ...
. Al metodo viene passata una struct BasicAuthorization
contenente il nome utente e la password.
In questo autenticatore di prova, il nome utente e la password vengono verificati rispetto ai valori codificati. In un autenticatore reale, potresti voler effettuare un controllo su un database o su un'API esterna, per questo motivo il metodo authenticate
consente di restituire una future.
Tip
Le password non devono mai essere memorizzate in un database in chiaro. Utilizzate sempre gli hash delle password per il confronto.
Se i parametri di autenticazione sono corretti, in questo caso corrispondono ai valori codificati, viene effettuato l'accesso a uno User
di nome Vapor. Se i parametri di autenticazione non corrispondono, non viene registrato alcun utente, il che significa che l'autenticazione è fallita.
Se aggiungi questo autenticatore alla tua applicazione e testi la route definita sopra, dovresti vedere il nome "Vapor"
restituito per un login riuscito. Se le credenziali non sono corrette, dovresti vedere un errore 401 Unauthorized
.
Bearer¶
L'autenticazione Bearer invia un token nell'intestazione Authorization
. Il token è preceduto dalla stringa "Bearer"
. La seguente richiesta di esempio invia un token di accesso secret
.
GET /me HTTP/1.1
Authorization: Bearer foo
L'autenticazione Bearer è comunemente usata per l'autenticazione degli endpoint API. L'utente in genere richiede un token Bearer inviando credenziali come nome utente e password a un endpoint di login. Questo token può durare minuti o giorni, a seconda delle esigenze dell'applicazione.
Finché il token è valido, l'utente può usarlo al posto delle proprie credenziali per autenticarsi con l'API. Se il token non è valido, è possibile generarne uno nuovo utilizzando l'endpoint di login.
Per implementare l'autenticazione Bearer nella tua applicazione, puoi creare un nuovo autenticatore conforme a BearerAuthenticator
. Di seguito è riportato un esempio di autenticatore codificato per verificare la richiesta di cui sopra.
import Vapor
struct UserAuthenticator: BearerAuthenticator {
typealias User = App.User
func authenticate(
bearer: BearerAuthorization,
for request: Request
) -> EventLoopFuture<Void> {
if bearer.token == "foo" {
request.auth.login(User(name: "Vapor"))
}
return request.eventLoop.makeSucceededFuture(())
}
}
Se stai usando async
/await
, puoi usare AsyncBearerAuthenticator
:
import Vapor
struct UserAuthenticator: AsyncBearerAuthenticator {
typealias User = App.User
func authenticate(
bearer: BearerAuthorization,
for request: Request
) async throws {
if bearer.token == "foo" {
request.auth.login(User(name: "Vapor"))
}
}
}
Questo protocollo richiede l'implementazione di authenticate(bearer:for:)
che verrà richiamata quando una richiesta in arrivo contiene l'intestazione Authorization: Bearer ...
. Al metodo viene passata una struct BearerAuthorization
contenente il token.
In questo autenticatore di prova, il token viene testato rispetto a un valore codificato. In un vero autenticatore, potresti voler verificare il token confrontandolo con un database o usando misure crittografiche, come si fa con JWT. Ecco perché il metodo authenticate
consente di restituire una future.
Tip
Quando si implementa la verifica dei token, è importante considerare la scalabilità orizzontale. Se l'applicazione deve gestire molti utenti contemporaneamente, l'autenticazione può essere un potenziale collo di bottiglia. Considera il modo in cui il tuo progetto scalerà su più istanze dell'applicazione in esecuzione contemporaneamente.
Se i parametri di autenticazione sono corretti, e in questo caso corrispondono al valore codificato, viene effettuato l'accesso a un Utente
di nome Vapor. Se i parametri di autenticazione non corrispondono, non viene registrato alcun utente, il che significa che l'autenticazione è fallita.
Se aggiungi questo autenticatore alla tua applicazione e testi la route definita sopra, dovresti vedere il nome "Vapor"
restituito per un login riuscito. Se le credenziali non sono corrette, dovresti vedere un errore 401 Unauthorized
.
Composizione¶
Puoi comporre (combinare insieme) più autenticatori per creare un'autenticazione dell'endpoint più complessa. Poiché un middleware autenticatore non rifiuta la richiesta se l'autenticazione fallisce, puoi concatenare più di un middleware. Puoi concatenare più autenticatori in due modi diversi.
Composizione dei Metodi¶
Il primo metodo di composizione dell'autenticazione consiste nel concatenare più autenticatori per lo stesso tipo di utente. Prendi l'esempio seguente:
app.grouped(UserPasswordAuthenticator())
.grouped(UserTokenAuthenticator())
.grouped(User.guardMiddleware())
.post("login")
{ req in
let user = try req.auth.require(User.self)
// Fai qualcosa con l'utente.
}
Questo esempio presuppone due autenticatori UserPasswordAuthenticator
e UserTokenAuthenticator
che autenticano entrambi User
. Entrambi gli autenticatori sono aggiunti al gruppo di route. Infine, GuardMiddleware
viene aggiunto dopo gli autenticatori per richiedere che User
sia stato autenticato con successo.
Questa composizione di autenticatori dà come risultato una route a cui si può accedere sia tramite password che tramite token. Una route di questo tipo potrebbe consentire a un utente di effettuare il login e generare un token, per poi continuare a usare quel token per generare nuovi token.
Composizione di Utenti¶
Il secondo metodo di composizione dell'autenticazione consiste nel concatenare gli autenticatori per diversi tipi di utenti. Prendiamo il seguente esempio:
app.grouped(AdminAuthenticator())
.grouped(UserAuthenticator())
.get("secure")
{ req in
guard req.auth.has(Admin.self) || req.auth.has(User.self) else {
throw Abort(.unauthorized)
}
// Fai qualcosa.
}
Questo esempio presuppone due autenticatori AdminAuthenticator
e UserAuthenticator
che autenticano rispettivamente Admin
e User
. Entrambi gli autenticatori sono aggiunti al gruppo di route. Invece di usare GuardMiddleware
, viene aggiunto un controllo nel gestore di route per vedere se Admin
o User
sono stati autenticati. In caso contrario, viene lanciato un errore.
Questa composizione di autenticatori dà luogo a un percorso a cui possono accedere due tipi diversi di utenti con metodi di autenticazione potenzialmente diversi. Un percorso di questo tipo potrebbe consentire l'autenticazione di un utente normale, pur consentendo l'accesso a un super-utente.
Manualmente¶
Puoi anche gestire l'autenticazione manualmente, utilizzando req.auth
. Questo è particolarmente utile per i test.
Per accedere manualmente a un utente, puoi utilizzare req.auth.login(_:)
. A questo metodo può essere passato qualsiasi utente Authenticatable
.
req.auth.login(User(name: "Vapor"))
Per ottenere l'utente autenticato puoi usare req.auth.require(_:)
:
let user: User = try req.auth.require(User.self)
print(user.name) // String
Puoi anche usare req.auth.get(_:)
se non vuoi lanciare automaticamente un errore quando l'autenticazione fallisce.
let user = req.auth.get(User.self)
print(user?.name) // String?
Per effettuare il logout di un utente, puoi usare req.auth.logout(_:)
:
req.auth.logout(User.self)
Fluent¶
Fluent definisce due protocolli ModelAuthenticatable
e ModelTokenAuthenticatable
che possono essere aggiunti ai modelli esistenti. Conformare i modelli a questi protocolli consente di creare autenticatori per proteggere gli endpoint.
ModelTokenAuthenticatable
si autentica con un token Bearer. È quello che puoi usare per proteggere la maggior parte degli endpoint. ModelAuthenticatable
si autentica con nome utente e password ed è usato da un singolo endpoint per generare token.
Questa guida presuppone che tu abbia già familiarità con Fluent e che abbia configurato con successo la tua applicazione per utilizzare un database. Se non conosci Fluent, inizia dalla panoramica.
User¶
Per iniziare, è necessario un modello che rappresenti l'utente da autenticare. Per questa guida, useremo il modello seguente, ma puoi usare un qualsiasi modello esistente.
import Fluent
import Vapor
final class User: Model, Content {
static let schema = "users"
@ID(key: .id)
var id: UUID?
@Field(key: "name")
var name: String
@Field(key: "email")
var email: String
@Field(key: "password_hash")
var passwordHash: String
init() { }
init(id: UUID? = nil, name: String, email: String, passwordHash: String) {
self.id = id
self.name = name
self.email = email
self.passwordHash = passwordHash
}
}
Il modello deve essere in grado di memorizzare un nome utente, in questo caso un'e-mail, e un hash di password. Abbiamo anche impostato email
come campo unico, per evitare utenti duplicati. La migrazione corrispondente per questo modello di esempio è qui:
import Fluent
import Vapor
extension User {
struct Migration: AsyncMigration {
var name: String { "CreateUser" }
func prepare(on database: Database) async throws {
try await database.schema("users")
.id()
.field("name", .string, .required)
.field("email", .string, .required)
.field("password_hash", .string, .required)
.unique(on: "email")
.create()
}
func revert(on database: Database) async throws {
try await database.schema("users").delete()
}
}
}
Non dimenticare di aggiungere la migrazione a app.migrations
.
app.migrations.add(User.Migration())
Tip
Poiché gli indirizzi email non sono sensibili alle maiuscole e alle minuscole, puoi aggiungere un Middleware
che coercizzi l'indirizzo email in minuscolo prima di salvarlo nella base dati. Tieni presente, però, che ModelAuthenticatable
usa un confronto sensibile alle maiuscole e alle minuscole, quindi se fai questo devi assicurarti che l'input dell'utente sia tutto minuscolo, o con la coercizione delle maiuscole nel client o con un autenticatore personalizzato.
La prima cosa di cui hai bisogno è un endpoint per creare nuovi utenti. Useremo POST /users
. Crea una struttura Content che rappresenti i dati che questo endpoint si aspetta.
import Vapor
extension User {
struct Create: Content {
var name: String
var email: String
var password: String
var confirmPassword: String
}
}
Se vuoi, puoi conformare questa struttura a Validatable per aggiungere requisiti di validazione.
import Vapor
extension User.Create: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("name", as: String.self, is: !.empty)
validations.add("email", as: String.self, is: .email)
validations.add("password", as: String.self, is: .count(8...))
}
}
Ora puoi creare l'endpoint POST /users
.
app.post("users") { req async throws -> User in
try User.Create.validate(content: req)
let create = try req.content.decode(User.Create.self)
guard create.password == create.confirmPassword else {
throw Abort(.badRequest, reason: "Passwords did not match")
}
let user = try User(
name: create.name,
email: create.email,
passwordHash: Bcrypt.hash(create.password)
)
try await user.save(on: req.db)
return user
}
Questo endpoint convalida la richiesta in arrivo, decodifica la struttura User.Create
e controlla che le password corrispondano. Utilizza quindi i dati decodificati per creare un nuovo User
e lo salva nel database. La password in chiaro viene sottoposta a hash con Bcrypt
prima di essere salvata nel database.
Compila ed esegui il progetto, assicurandoti di eseguire prima le migrazioni sul database, quindi utilizza la seguente richiesta per creare un nuovo utente.
POST /users HTTP/1.1
Content-Length: 97
Content-Type: application/json
{
"name": "Vapor",
"email": "test@vapor.codes",
"password": "secret42",
"confirmPassword": "secret42"
}
Modello Authenticatable¶
Ora che hai un modello utente e un endpoint per creare nuovi utenti, conforma il modello a ModelAuthenticatable
. Questo ti permetterà di autenticare il modello usando nome utente e password.
import Fluent
import Vapor
extension User: ModelAuthenticatable {
static let usernameKey = \User.$email
static let passwordHashKey = \User.$passwordHash
func verify(password: String) throws -> Bool {
try Bcrypt.verify(password, created: self.passwordHash)
}
}
Questa estensione aggiunge la conformità ModelAuthenticatable
a User
. Le prime due proprietà specificano quali campi devono essere utilizzati per memorizzare rispettivamente il nome utente e l'hash della password. La notazione \
crea un percorso chiave per i campi che Fluent può usare per accedervi.
L'ultimo requisito è un metodo per verificare le password in chiaro inviate nell'intestazione di autenticazione Basic. Poiché usiamo Bcrypt per l'hash della password durante la registrazione, useremo Bcrypt per verificare che la password fornita corrisponda all'hash della password memorizzata.
Ora che l'utente User
è conforme a ModelAuthenticatable
, puoi creare un autenticatore per proteggere la route di login.
let passwordProtected = app.grouped(User.authenticator())
passwordProtected.post("login") { req -> User in
try req.auth.require(User.self)
}
ModelAuthenticatable
aggiunge un metodo statico authenticator
per creare un autenticatore.
Verifica che questo percorso funzioni inviando la seguente richiesta:
POST /login HTTP/1.1
Authorization: Basic dGVzdEB2YXBvci5jb2RlczpzZWNyZXQ0Mg==
Questa richiesta passa il nome utente test@vapor.codes
e la password secret42
tramite l'intestazione di autenticazione Basic. Dovrebbe essere restituito l'utente precedentemente creato.
Anche se in teoria si potrebbe usare l'autenticazione di base per proteggere tutti gli endpoint, è consigliato usare un token separato. In questo modo si riduce al minimo la frequenza di invio della password sensibile dell'utente su Internet. Inoltre, l'autenticazione è molto più veloce, poiché è sufficiente eseguire l'hashing della password durante l'accesso.
Token Utente¶
Crea un nuovo modello per rappresentare i token degli utenti.
import Fluent
import Vapor
final class UserToken: Model, Content {
static let schema = "user_tokens"
@ID(key: .id)
var id: UUID?
@Field(key: "value")
var value: String
@Parent(key: "user_id")
var user: User
init() { }
init(id: UUID? = nil, value: String, userID: User.IDValue) {
self.id = id
self.value = value
self.$user.id = userID
}
}
Questo modello deve avere un campo value
per memorizzare la stringa unica del token. Deve anche avere una relazione padre con il modello utente. Puoi aggiungere anche altre proprietà a questo token, come ad esempio una data di scadenza.
Quindi, crea una migrazione per questo modello.
import Fluent
extension UserToken {
struct Migration: AsyncMigration {
var name: String { "CreateUserToken" }
func prepare(on database: Database) async throws {
try await database.schema("user_tokens")
.id()
.field("value", .string, .required)
.field("user_id", .uuid, .required, .references("users", "id"))
.unique(on: "value")
.create()
}
func revert(on database: Database) async throws {
try await database.schema("user_tokens").delete()
}
}
}
Nota che questa migrazione rende unico il campo value
. Inoltre, crea un riferimento a chiave esterna tra il campo user_id
e la tabella utenti.
Non dimenticare di aggiungere la migrazione a app.migrations
.
app.migrations.add(UserToken.Migration())
Infine, aggiungi un metodo su User
per generare un nuovo token. Questo metodo sarà utilizzato durante il login.
extension User {
func generateToken() throws -> UserToken {
try .init(
value: [UInt8].random(count: 16).base64,
userID: self.requireID()
)
}
}
Qui usiamo [UInt8].random(count:)
per generare un valore casuale di token. Per questo esempio, vengono utilizzati 16 byte, o 128 bit, di dati casuali. Puoi modificare questo numero come ritieni opportuno. I dati casuali vengono poi codificati in base-64 per facilitarne la trasmissione nelle intestazioni HTTP.
Ora che puoi generare i token utente, aggiorna la route POST /login
per creare e restituire un token.
let passwordProtected = app.grouped(User.authenticator())
passwordProtected.post("login") { req async throws -> UserToken in
let user = try req.auth.require(User.self)
let token = try user.generateToken()
try await token.save(on: req.db)
return token
}
Verifica che questa route funzioni utilizzando la stessa richiesta di login di cui sopra. Ora dovresti ottenere un token al momento dell'accesso che assomigli a qualcosa di simile:
8gtg300Jwdhc/Ffw784EXA==
Conserva il token ottenuto: lo utilizzeremo a breve.
Modello Token Authenticatable¶
Conforma UserToken
a ModelTokenAuthenticatable
. Questo permetterà ai token di autenticare il modello User
.
import Vapor
import Fluent
extension UserToken: ModelTokenAuthenticatable {
static let valueKey = \UserToken.$value
static let userKey = \UserToken.$user
var isValid: Bool {
true
}
}
Il primo requisito del protocollo specifica quale campo memorizza il valore univoco del token. Questo è il valore che sarà inviato nell'intestazione di autenticazione Bearer. Il secondo requisito specifica la parentela con il modello User
. Questo è il modo in cui Fluent cercherà l'utente autenticato.
Il requisito finale è un booleano isValid
. Se è false
, il token sarà cancellato dal database e l'utente non sarà autenticato. Per semplicità, renderemo i token eterni, codificando in modo rigido questo valore a true
.
Ora che il token è conforme a ModelTokenAuthenticatable
, si può creare un autenticatore per proteggere le route.
Crea un nuovo endpoint GET /me
per ottenere l'utente attualmente autenticato.
let tokenProtected = app.grouped(UserToken.authenticator())
tokenProtected.get("me") { req -> User in
try req.auth.require(User.self)
}
Simile a User
, UserToken
ha ora un metodo statico authenticator()
che può generare un autenticatore. L'autenticatore cercherà di trovare un UserToken
corrispondente, utilizzando il valore fornito nell'intestazione di autenticazione del portatore. Se trova una corrispondenza, recupera il relativo User
e lo autentica.
Verifica che questa route funzioni inviando la seguente richiesta HTTP, dove il token è il valore salvato dalla richiesta POST /login
.
GET /me HTTP/1.1
Authorization: Bearer <token>
Dovresti vedere l'utente attualmente autenticato.
Sessioni¶
L'API delle Sessioni di Vapor può essere utilizzata per persistere automaticamente l'autenticazione dell'utente tra le richieste. Questo funziona memorizzando un identificatore univoco per l'utente nei dati di sessione della richiesta, dopo il successo del login. Nelle richieste successive, l'identificatore dell'utente viene recuperato dalla sessione e usato per autenticare l'utente prima di chiamare il gestore della route.
Le sessioni sono ottime per le applicazioni web front-end costruite in Vapor che servono HTML direttamente ai browser web. Per le API, si consiglia di utilizzare un'autenticazione stateless basata su token per conservare i dati dell'utente tra una richiesta e l'altra.
Session Authenticatable¶
Per utilizzare l'autenticazione basata sulla sessione, occorre un tipo conforme a SessionAuthenticatable
. Per questo esempio, useremo una semplice struct.
import Vapor
struct User {
var email: String
}
Per essere conformi a SessionAuthenticatable
, è necessario specificare un sessionID
. Questo è il valore che verrà memorizzato nei dati di sessione e deve identificare in modo univoco l'utente.
extension User: SessionAuthenticatable {
var sessionID: String {
self.email
}
}
Per il nostro tipo User
, useremo l'indirizzo e-mail come identificatore unico di sessione.
Autenticatore di Sessione¶
Poi, avrai bisogno di un SessionAuthenticator
per gestire la risoluzione delle istanze dell'utente dall'identificatore di sessione persistito.
struct UserSessionAuthenticator: SessionAuthenticator {
typealias User = App.User
func authenticate(sessionID: String, for request: Request) -> EventLoopFuture<Void> {
let user = User(email: sessionID)
request.auth.login(user)
return request.eventLoop.makeSucceededFuture(())
}
}
Se stai usando async
/await
, puoi usare AsyncSessionAuthenticator
:
struct UserSessionAuthenticator: AsyncSessionAuthenticator {
typealias User = App.User
func authenticate(sessionID: String, for request: Request) async throws {
let user = User(email: sessionID)
request.auth.login(user)
}
}
Poiché tutte le informazioni necessarie per inizializzare il nostro User
di esempio sono contenute nell'identificatore di sessione, possiamo creare e accedere all'utente in modo sincrono. In un'applicazione reale, è probabile che venga utilizzato l'identificatore di sessione per eseguire una ricerca nel database o una richiesta API per recuperare il resto dei dati dell'utente prima dell'autenticazione.
Quindi, crea un semplice autenticatore di portatori per eseguire l'autenticazione iniziale.
struct UserBearerAuthenticator: AsyncBearerAuthenticator {
func authenticate(bearer: BearerAuthorization, for request: Request) async throws {
if bearer.token == "test" {
let user = User(email: "hello@vapor.codes")
request.auth.login(user)
}
}
}
Questo autenticatore autenticherà un utente con l'email hello@vapor.codes
quando viene inviato il token portatore test
.
Infine, combina tutti questi pezzi insieme nell'applicazione.
// Crea un gruppo di route protette che richiedono autenticazione.
let protected = app.routes.grouped([
app.sessions.middleware,
UserSessionAuthenticator(),
UserBearerAuthenticator(),
User.guardMiddleware(),
])
// Aggiungi una route GET /me che restituisce l'email dell'utente.
protected.get("me") { req -> String in
try req.auth.require(User.self).email
}
Viene prima aggiunto SessionsMiddleware
, per abilitare il supporto alle sessioni nell'applicazione. Puoi trovare maggiori informazioni sulla configurazione delle sessioni nella sezione API di sessione.
Successivamente, viene aggiunto il SessionAuthenticator
. Questo gestisce l'autenticazione dell'utente se è attiva una sessione.
Se l'autenticazione non è ancora stata persistita nella sessione, la richiesta sarà inoltrata all'autenticatore successivo. L'autenticatore UserBearerAuthenticator
controllerà il token del portatore e autenticherà l'utente se è uguale a "test"
.
Infine, User.guardMiddleware()
assicura che User
sia stato autenticato da uno dei middleware precedenti. Se l'utente non è stato autenticato, verrà lanciato un errore.
Per testare questa route, invia prima la seguente richiesta:
GET /me HTTP/1.1
authorization: Bearer test
Questo farà sì che UserBearerAuthenticator
autentichi l'utente. Una volta autenticato, UserSessionAuthenticator
persisterà l'identificatore dell'utente nella memoria di sessione e genererà un cookie. Puoi poi utilizzare il cookie dalla risposta in una seconda richiesta alla route.
GET /me HTTP/1.1
cookie: vapor_session=123
Questa volta, UserSessionAuthenticator
autenticherà l'utente e dovrebbe essere restituita l'e-mail dell'utente.
Model Session Authenticatable¶
I modelli Fluent possono generare SessionAuthenticator
conformandosi a ModelSessionAuthenticatable
. Questo userà l'identificatore univoco del modello come identificatore di sessione ed eseguirà automaticamente una ricerca nel database per ripristinare il modello dalla sessione.
import Fluent
final class User: Model { ... }
// Consente di persistere il modello nelle sessioni.
extension User: ModelSessionAuthenticatable { }
Puoi aggiungere ModelSessionAuthenticatable
a qualsiasi modello esistente come conformità vuota. Una volta aggiunto, sarà disponibile un nuovo metodo statico per creare un SessionAuthenticator
per quel modello.
User.sessionAuthenticator()
Questo utilizzerà il database predefinito dell'applicazione per la risoluzione dell'utente. Per specificare un database, passa l'identificatore.
User.sessionAuthenticator(.sqlite)
Autenticazione per Sito Web¶
I siti web sono un caso particolare per l'autenticazione, perché l'uso di un browser limita il modo in cui è possibile collegare le credenziali a un browser. Questo porta a due diversi scenari di autenticazione:
- l'accesso iniziale tramite un form
- chiamate successive autenticate con un cookie di sessione
Vapor e Fluent forniscono diversi aiutanti per rendere tutto ciò semplice.
Autenticazione di Sessione¶
L'autenticazione di sessione funziona come descritto sopra. Devi applicare il middleware di sessione e l'autenticatore di sessione a tutte le route a cui l'utente accederà. Queste includono tutte le route protette, le route che sono pubbliche, ma per le quali vuoi accedere all'utente se è loggato (ad esempio, per visualizzare un pulsante per l'account), e le route di login.
È possibile attivarlo globalmente nella propria applicazione in configure.swift
in questo modo:
app.middleware.use(app.sessions.middleware)
app.middleware.use(User.sessionAuthenticator())
Questi middleware svolgono le seguenti funzioni:
- Il middleware delle sessioni prende il cookie di sessione fornito nella richiesta e lo converte in una sessione.
- l'autenticatore di sessione prende la sessione e verifica se esiste un utente autenticato per quella sessione. In caso affermativo, il middleware autentica la richiesta. Nella risposta, l'autenticatore di sessione vede se la richiesta ha un utente autenticato e lo salva nella sessione, in modo che sia autenticato nella richiesta successiva.
Note
Di default, il cookie di sessione non è impostato su secure
e/o httpOnly
. Per ulteriori informazioni su come configurare i cookie, consultare le API di sessione di Vapor.
Protezione delle Route¶
Quando si proteggono le route per un'API, tradizionalmente restituisci una risposta HTTP con un codice di stato come 401 Unauthorized se la richiesta non è autenticata. Tuttavia, questa non è una buona esperienza per l'utente che utilizza un browser. Vapor fornisce un RedirectMiddleware
per qualsiasi tipo Authenticatable
da utilizzare in questo scenario:
let protectedRoutes = app.grouped(User.redirectMiddleware(path: "/login?loginRequired=true"))
L'oggetto RedirectMiddleware
supporta anche il passaggio di una chiusura che restituisce il percorso di reindirizzamento come Stringa
durante la creazione, per una gestione avanzata degli url. Ad esempio, includendo il percorso di reindirizzamento come parametro di query alla destinazione del reindirizzamento per la gestione dello stato.
let redirectMiddleware = User.redirectMiddleware { req -> String in
return "/login?authRequired=true&next=\(req.url.path)"
}
Questo funziona in modo simile a GuardMiddleware
. Qualsiasi richiesta alle route registrate su protectedRoutes
che non sia autenticata sarà reindirizzata al percorso fornito. Questo permette di dire agli utenti di effettuare il login, invece di fornire semplicemente un 401 Unauthorized.
Assicurati di includere un Autenticatore di sessione prima del RedirectMiddleware
per garantire che l'utente autenticato sia caricato prima di passare attraverso il RedirectMiddleware
.
let protectedRoutes = app.grouped([User.sessionAuthenticator(), redirectMiddleware])
Form per il Login¶
Per autenticare un utente e le richieste future con una sessione, è necessario effettuare il login. Vapor fornisce un protocollo ModelCredentialsAuthenticatable
a cui conformarsi. Questo gestisce l'accesso tramite un modulo. Per prima cosa, conforma il tuo User
a questo protocollo:
extension User: ModelCredentialsAuthenticatable {
static let usernameKey = \User.$email
static let passwordHashKey = \User.$password
func verify(password: String) throws -> Bool {
try Bcrypt.verify(password, created: self.password)
}
}
Questo è identico a ModelAuthenticatable
e se lo User
è già conforme a questo, non è necessario fare altro. Quindi applica il middleware ModelCredentialsAuthenticator
alla richiesta POST del modulo di login:
let credentialsProtectedRoute = sessionRoutes.grouped(User.credentialsAuthenticator())
credentialsProtectedRoute.post("login", use: loginPostHandler)
Esso utilizza l'autenticatore di credenziali predefinito per proteggere il percorso di accesso. È necessario che invii username
e password
nella richiesta POST. Si può impostare il form in questo modo:
<form method="POST" action="/login">
<label for="username">Username</label>
<input type="text" id="username" placeholder="Username" name="username" autocomplete="username" required autofocus>
<label for="password">Password</label>
<input type="password" id="password" placeholder="Password" name="password" autocomplete="current-password" required>
<input type="submit" value="Sign In">
</form>
Il CredentialsAuthenticator
estrae username
e password
dal corpo della richiesta, trova l'utente dal nome utente e verifica la password. Se la password è valida, il middleware autentica la richiesta. Il SessionAuthenticator
autentica quindi la sessione per le richieste successive.
JWT¶
JWT fornisce un JWTAuthenticator
che può essere usato per autenticare i token web JSON nelle richieste in arrivo. Se non conosci JWT, dai un'occhiata alla panoramica.
Per prima cosa, crea un tipo che rappresenti un payload JWT.
// Esempio di payload JWT.
struct SessionToken: Content, Authenticatable, JWTPayload {
// Costanti
let expirationTime: TimeInterval = 60 * 15
// Dati del payload
var expiration: ExpirationClaim
var userId: UUID
init(userId: UUID) {
self.userId = userId
self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationTime))
}
init(with user: User) throws {
self.userId = try user.requireID()
self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationTime))
}
func verify(using signer: JWTSigner) throws {
try expiration.verifyNotExpired()
}
}
Successivamente, possiamo definire una rappresentazione dei dati contenuti in una risposta di login andata a buon fine. Per ora la risposta avrà solo una proprietà, una stringa che rappresenta un JWT firmato.
struct ClientTokenResponse: Content {
var token: String
}
Utilizzando il nostro modello per il token JWT e la risposta, possiamo usare una route di login protetta da password che restituisce un ClientTokenResponse
e include un SessionToken
firmato.
let passwordProtected = app.grouped(User.authenticator(), User.guardMiddleware())
passwordProtected.post("login") { req -> ClientTokenResponse in
let user = try req.auth.require(User.self)
let payload = try SessionToken(with: user)
return ClientTokenResponse(token: try req.jwt.sign(payload))
}
In alternativa, se non vuoi usare un autenticatore, puoi avere qualcosa di simile a questo:
app.post("login") { req -> ClientTokenResponse in
// Valida le credenziali dell'utente
// Ottieni lo userId dell'utente
let payload = try SessionToken(userId: userId)
return ClientTokenResponse(token: try req.jwt.sign(payload))
}
Conformando il payload a Authenticatable
e JWTPayload
, puoi generare un autenticatore di route usando il metodo authenticator()
. Aggiungilo a un gruppo di route per recuperare e verificare automaticamente il JWT prima che la route venga chiamata.
// Crea un gruppo di route che richiede il SessionToken JWT.
let secure = app.grouped(SessionToken.authenticator(), SessionToken.guardMiddleware())
L'aggiunta dell'opzionale middleware di guardia richiede che l'autorizzazione sia riuscita.
All'interno delle route protette, si può accedere al payload JWT autenticato usando req.auth
.
// Restituisce una risposta ok se il token fornito dall'utente è valido.
secure.post("validateLoggedInUser") { req -> HTTPStatus in
let sessionToken = try req.auth.require(SessionToken.self)
print(sessionToken.userId)
return .ok
}