Oggigiorno è una pratica comune fare molto affidamento sulle API (interfacce di programmazione delle applicazioni). Non solo i grandi servizi come Facebook e Twitter li utilizzano: le API sono molto popolari grazie alla diffusione di strutture client-side come React, Angular e molti altri. Ruby on Rails sta seguendo questa tendenza e l'ultima versione presenta una nuova funzionalità che consente di creare applicazioni solo API.
Inizialmente questa funzionalità era racchiusa in una gemma separata chiamata rails-api, ma dal rilascio di Rails 5, ora fa parte del nucleo del framework. Questa funzione, insieme a ActionCable, era probabilmente la più attesa, e quindi oggi ne discuteremo.
Questo articolo spiega come creare applicazioni Rails solo API e spiega come strutturare i percorsi e i controller, rispondere con il formato JSON, aggiungere serializzatori e configurare CORS (Cross-Origin Resource Sharing). Imparerai anche alcune opzioni per proteggere l'API e proteggerla dall'abuso.
La fonte di questo articolo è disponibile su GitHub.
Per iniziare, eseguire il seguente comando:
rotaie nuove RailsApiDemo --api
Creerà una nuova applicazione Rails solo API chiamata RailsApiDemo
. Non dimenticare che il supporto per il --api
l'opzione è stata aggiunta solo in Rails 5, quindi assicurati di aver installato questa o una versione più recente.
Apri il Gemfile e nota che è molto più piccolo del solito: gemme come coffee-rails
, turbolinks
, e Sass-rails
sono andati.
Il config / application.rb il file contiene una nuova riga:
config.api_only = true
Significa che Rails sta per caricare un piccolo insieme di middleware: ad esempio, non ci sono cookie e supporto alle sessioni. Inoltre, se provi a generare uno scaffold, le viste e le risorse non verranno create. In realtà, se controlli il Visto / layout directory, noterai che il application.html.erb manca anche il file.
Un'altra importante differenza è che il ApplicationController
eredita dal ActionController :: API
, non ActionController :: Base
.
Questo è praticamente tutto - tutto sommato, questa è un'applicazione di Rails di base che hai visto molte volte. Ora aggiungiamo un paio di modelli in modo che abbiamo qualcosa su cui lavorare:
rails g model Nome utente: string rails g model Titolo del post: string body: text user: belongs_to rails db: migrate
Niente di speciale sta succedendo qui: un post con un titolo e un corpo appartiene a un utente.
Assicurati che le associazioni appropriate siano configurate e fornisci anche alcuni semplici controlli di convalida:
has_many: i post convalidano: name, presence: true
belongs_to: l'utente conferma: title, presence: true validates: body, presence: true
Brillante! Il prossimo passo è caricare un paio di record di esempio nelle nuove tabelle create.
Il modo più semplice per caricare alcuni dati è utilizzando il seeds.rb file all'interno del db directory. Tuttavia, sono pigro (come molti programmatori lo sono) e non voglio pensare a nessun contenuto di esempio. Pertanto, perché non sfruttiamo la gemma falsa che può produrre dati casuali di vario tipo: nomi, e-mail, parole a vita bassa, testi "lorem ipsum" e molto altro.
gruppo: sviluppo do gemma 'finta' fine
Installa la gemma:
installazione bundle
Ora modificare il seeds.rb:
5.times do user = User.create (nome: Faker :: Name.name) user.posts.create (title: Faker :: Book.title, body: Faker :: Lorem.sentence) end
Infine, carica i tuoi dati:
rotaie db: seme
Ora, ovviamente, abbiamo bisogno di alcuni percorsi e controller per creare la nostra API. È prassi comune annidare i percorsi dell'API sotto il API /
sentiero. Inoltre, gli sviluppatori di solito forniscono la versione dell'API nel percorso, ad esempio api / v1 /
. In seguito, se è necessario introdurre alcune modifiche urgenti, è sufficiente creare un nuovo spazio dei nomi (v2
) e un controller separato.
Ecco come possono essere visualizzati i tuoi percorsi:
namespace 'api' fa namespace 'v1' fa risorse: posts resources: users end end
Questo genera percorsi come:
api_v1_posts GET /api/v1/posts(.:format) api / v1 / posts # indice POST /api/v1/posts(.:format) api / v1 / posts # crea api_v1_post GET / api / v1 / posts /: id (.: format) api / v1 / posts # show
Puoi usare a scopo
metodo invece del namespace
, ma di default cercherà il UsersController
e PostsController
dentro il controllori directory, non dentro il controllori / api / v1, quindi sii attento.
Crea il api cartella con la directory nidificata v1 dentro il controllori. Compilalo con i tuoi controller:
modulo Api modulo V1 classe UsersController < ApplicationController end end end
modulo Api modulo V1 classe PostsController < ApplicationController end end end
Si noti che non solo è necessario nidificare il file del controller sotto il api / v1 percorso, ma anche la classe stessa deve essere denominata nello spazio Api
e V1
moduli.
La prossima domanda è come rispondere correttamente con i dati in formato JSON? In questo articolo proveremo queste soluzioni: le gemme di jBuilder e active_model_serializers. Quindi, prima di passare alla sezione successiva, rilasciarli nella Gemfile:
gem 'jbuilder', '~> 2.5' gem 'active_model_serializers', '~> 0.10.0'
Quindi esegui:
installazione bundle
jBuilder è una gemma popolare gestita dal team Rails che fornisce un semplice DSL (linguaggio specifico del dominio) che consente di definire strutture JSON nelle viste.
Supponiamo di voler visualizzare tutti i post quando un utente preme il indice
azione:
def index @posts = Post.order ('created_at DESC') fine
Tutto ciò che devi fare è creare la vista che prende il nome dall'azione corrispondente con il .json.jbuilder estensione. Si noti che la vista deve essere posizionata sotto api / v1 percorso pure:
json.array! @posts do | post | json.id post.id json.title post.title json.body post.body end
json.array!
attraversa il @posts
schieramento. json.id
, json.title
e json.body
generare le chiavi con i nomi corrispondenti impostando gli argomenti come valori. Se navighi su http: // localhost: 3000 / api / v1 / posts.json, vedrai un output simile a questo:
["id": 1, "titolo": "Titolo 1", "corpo": "Corpo 1", "id": 2, "titolo": "Titolo 2", "corpo": "Corpo 2 "]
E se volessimo mostrare l'autore per ogni post? È semplice:
json.array! @posts do | post | json.id post.id json.title post.title json.body post.body json.user do json.id post.user.id json.name post.user.name end end
L'output cambierà in:
["id": 1, "titolo": "Titolo 1", "corpo": "Corpo 1", "utente": "id": 1, "nome": "Nome utente"]
Il contenuto del .JBuilder i file sono semplici codice Ruby, quindi puoi utilizzare tutte le operazioni di base come al solito.
Nota che jBuilder supporta i partial come qualsiasi normale visualizzazione di Rails, quindi potresti anche dire:
json.partial! parziale: "post / post", collezione: @posts, come:: post
e quindi creare il views / api / v1 / messaggi / _post.json.jbuilder file con il seguente contenuto:
json.id post.id json.title post.title json.body post.body json.user do json.id post.user.id json.name post.user.name end
Quindi, come vedi, jBuilder è facile e conveniente. Tuttavia, in alternativa, puoi utilizzare i serializzatori, quindi discutiamoli nella prossima sezione.
La gemma rails_model_serializers è stata creata da un team che inizialmente gestiva le rails-api. Come indicato nella documentazione, rails_model_serializers porta la convenzione sulla configurazione alla tua generazione JSON. Fondamentalmente, si definiscono i campi da utilizzare in caso di serializzazione (ovvero generazione JSON).
Ecco il nostro primo serializzatore:
class PostSerializer < ActiveModel::Serializer attributes :id, :title, :body end
Qui diciamo che tutti questi campi dovrebbero essere presenti nel JSON risultante. Adesso metodi come to_json
e as_json
chiamato un post utilizzerà questa configurazione e restituirà il contenuto corretto.
Per vederlo in azione, modifica il indice
azione come questa:
def index @posts = Post.order ('created_at DESC') render json: @posts end
as_json
verrà automaticamente chiamato su @posts
oggetto.
E gli utenti? I serializzatori consentono di indicare le relazioni, proprio come fanno i modelli. Inoltre, i serializzatori possono essere nidificati:
class PostSerializer < ActiveModel::Serializer attributes :id, :title, :body belongs_to :user class UserSerializer < ActiveModel::Serializer attributes :id, :name end end
Ora, quando serializzi il post, conterrà automaticamente l'annidato utente
chiave con il suo id e nome. Se in seguito si crea un serializzatore separato per l'utente con : id
attributo escluso:
class UserSerializer < ActiveModel::Serializer attributes :name end
poi @ user.as_json
non restituirà l'id dell'utente. Ancora, @ post.as_json
restituirà sia il nome che l'ID dell'utente, quindi ricordalo.
In molti casi, non vogliamo che nessuno esegua alcuna azione utilizzando l'API. Quindi presentiamo un semplice controllo di sicurezza e costringiamo i nostri utenti a inviare i propri token durante la creazione e l'eliminazione di post.
Il token avrà una durata illimitata e verrà creato al momento della registrazione dell'utente. Prima di tutto, aggiungi un nuovo gettone
colonna al utenti
tavolo:
rails g migrazione add_token_to_users token: string: index
Questo indice dovrebbe garantire l'unicità in quanto non possono esserci due utenti con lo stesso token:
add_index: users,: token, unique: true
Applica la migrazione:
rails db: migrate
Ora aggiungi il before_save
richiama:
before_create -> self.token = generate_token
Il generate_token
il metodo privato creerà un token in un ciclo infinito e controllerà se è unico o meno. Non appena viene trovato un token univoco, restituiscilo:
private def generate_token loop do token = SecureRandom.hex return token a meno che User.exists? (token: token) end end
È possibile utilizzare un altro algoritmo per generare il token, ad esempio basato sull'hash MD5 del nome dell'utente e un po 'di sale.
Ovviamente, dobbiamo anche consentire agli utenti di registrarsi, perché altrimenti non saranno in grado di ottenere il loro token. Non voglio introdurre alcuna vista HTML nella nostra applicazione, quindi aggiungiamo un nuovo metodo API:
def create @user = User.new (user_params) se @ user.save lo stato di rendering:: creato else render json: @ user.errors, stato:: unprocessable_entity end end private def user_params params.require (: user) .permit (: nome) fine
È una buona idea restituire codici di stato HTTP significativi in modo che gli sviluppatori capiscano esattamente cosa sta succedendo. Ora è possibile fornire un nuovo serializzatore per gli utenti o utilizzare un .json.jbuilder file. Preferisco quest'ultima variante (è per questo che non passo il : json
opzione per il rendere
metodo), ma sei libero di sceglierne uno. Nota, tuttavia, che il token non deve essere sempre serializzato, ad esempio quando si restituisce un elenco di tutti gli utenti, dovrebbe essere tenuto al sicuro!
json.id @ user.id json.name @ user.name json.token @ user.token
Il prossimo passo è verificare se tutto funziona correttamente. È possibile utilizzare il arricciare
comando o scrivi del codice Ruby. Dato che questo articolo parla di Ruby, andrò con l'opzione di codifica.
Per eseguire una richiesta HTTP, impiegheremo la gemma di Faraday, che fornisce un'interfaccia comune su molti adattatori (l'impostazione predefinita è Net :: HTTP
). Creare un file Ruby separato, includere Faraday e configurare il client:
richiede client 'faraday' = Faraday.new (url: 'http: // localhost: 3000') do | config | config.adapter Faraday.default_adapter end response = client.post do | req | req.url '/ api / v1 / users' req.headers ['Content-Type'] = 'application / json' req.body = '"utente": "name": "test user"' end
Tutte queste opzioni sono piuttosto auto-esplicative: scegliamo l'adattatore predefinito, impostiamo l'URL di richiesta a http: // localhost: 300 / api / v1 / users, cambia il tipo di contenuto in application / json
, e fornire il corpo della nostra richiesta.
La risposta del server conterrà JSON, quindi per analizzarla userò la gemma Oj:
richiede 'oj' # client qui ... puts Oj.load (response.body) mette response.status
Oltre alla risposta analizzata, visualizzo anche il codice di stato per scopi di debug.
Ora puoi semplicemente eseguire questo script:
ruby api_client.rb
e memorizza il token ricevuto da qualche parte, lo useremo nella prossima sezione.
Per applicare l'autenticazione del token, il authenticate_or_request_with_http_token
metodo può essere utilizzato. Fa parte del modulo ActionController :: HttpAuthentication :: Token :: ControllerMethods, quindi non dimenticare di includerlo:
classe PostsController < ApplicationController include ActionController::HttpAuthentication::Token::ControllerMethods #… end
Aggiungi un nuovo before_action
e il metodo corrispondente:
before_action: autenticate, solo: [: create,: destroy] # ... private # ... def autenticate authenticate_or_request_with_http_token do | token, opzioni | @user = User.find_by (token: token) end end
Ora se il token non è impostato o se non è possibile trovare un utente con tale token, verrà restituito un errore 401, interrompendo l'esecuzione dall'esecuzione.
Si noti che la comunicazione tra il client e il server deve essere effettuata tramite HTTPS, altrimenti i token potrebbero essere facilmente falsificati. Ovviamente, la soluzione fornita non è l'ideale e in molti casi è preferibile utilizzare il protocollo OAuth 2 per l'autenticazione. Ci sono almeno due gemme che semplificano notevolmente il processo di supporto di questa funzione: Doorkeeper e oPRO.
Per vedere la nostra autenticazione in azione, aggiungi il creare
azione per il PostsController
:
def create @post = @ user.posts.new (post_params) se @ post.save restituisce json: @post, stato:: creato else render json: @ post.errors, stato:: unprocessable_entity end end
Approfittiamo del serializzatore qui per visualizzare il JSON corretto. Il @utente
era già ambientato nel before_action
.
Ora prova tutto usando questo semplice codice:
client = Faraday.new (url: 'http: // localhost: 3000') do | config | config.adapter Faraday.default_adapter config.token_auth ('127a74dbec6f156401b236d6cb32db0d') end response = client.post do | req | req.url '/ api / v1 / posts' req.headers ['Content-Type'] = 'application / json' req.body = '"post": "title": "Title", "body": "Testo" 'fine
Sostituisci l'argomento passato a token_auth
con il token ricevuto al momento della registrazione ed eseguire lo script.
ruby api_client.rb
La cancellazione di un post viene eseguita allo stesso modo. Aggiungi il distruggere
azione:
def destroy @post = @ user.posts.find_by (params [: id]) se @post @ post.destroy else restituisce json: post: "non trovato", stato:: not_found end end
Permettiamo solo agli utenti di distruggere i post che possiedono. Se il post viene rimosso con successo, verrà restituito il codice di stato 204 (nessun contenuto). In alternativa, puoi rispondere con l'ID del post che è stato eliminato poiché sarà ancora disponibile dalla memoria.
Ecco la parte di codice per testare questa nuova funzionalità:
response = client.delete do | req | req.url '/ api / v1 / posts / 6' req.headers ['Content-Type'] = 'application / json' end
Sostituisci l'ID del post con un numero che funzioni per te.
Se vuoi consentire ad altri servizi web di accedere alla tua API (dal lato client), allora CORS (Cross-Origin Resource Sharing) dovrebbe essere configurato correttamente. Fondamentalmente, CORS consente alle applicazioni Web di inviare richieste AJAX ai servizi di terze parti. Fortunatamente, c'è una gemma chiamata rack-cors che ci consente di impostare facilmente tutto. Aggiungilo al Gemfile:
gemma 'rack-cors'
Installalo:
installazione bundle
E quindi fornire la configurazione all'interno del config / inizializzatori / cors.rb file. In realtà, questo file è già stato creato per te e contiene un esempio di utilizzo. Puoi anche trovare una documentazione abbastanza dettagliata sulla pagina della gemma.
La seguente configurazione, ad esempio, consentirà a chiunque di accedere all'API utilizzando qualsiasi metodo:
Rails.application.config.middleware.insert_before 0, Rack :: Cors consente l'uso delle origini '*' resource '/ api / *', intestazioni:: any, metodi: [: get,: post,: put,: patch, : delete,: options,: head] end end
L'ultima cosa che accennerò a questa guida è come proteggere la tua API dagli attacchi di abuso e denial of service. C'è una bella gemma chiamata rack-attack (creata da persone di Kickstarter) che ti permette di inserire nella blacklist o nella lista bianca i client, prevenire l'inondazione di un server con richieste e altro.
Lascia cadere la gemma Gemfile:
gemma "rack-attack"
Installalo:
installazione bundle
E quindi fornire la configurazione all'interno del rack_attack.rb file di inizializzazione. La documentazione della gem elenca tutte le opzioni disponibili e suggerisce alcuni casi d'uso. Ecco la configurazione di esempio che limita chiunque ad accedere al servizio e limita il numero massimo di richieste a 5 al secondo:
class Rack :: Attack safelist ('allow from localhost') do | req | # Le richieste sono consentite se il valore restituito è truey '127.0.0.1' == req.ip || ':: 1' == req.ip end throttle ('req / ip',: limit => 5,: periodo => 1.secondo) do | req | req.ip end end
Un'altra cosa che deve essere fatta è includere RackAttack come middleware:
config.middleware.use Rack :: Attack
Siamo arrivati alla fine di questo articolo. Speriamo che ormai ti senti più sicuro di creare API con Rails! Si noti che questa non è l'unica opzione disponibile - un'altra soluzione popolare che esiste da un po 'di tempo è il framework Grape, quindi potresti essere interessato a verificarlo.
Non esitare a pubblicare le tue domande se qualcosa ti è sembrato poco chiaro. Ti ringrazio per essere stato con me e felice codifica!