Tegenwoordig is het een gebruikelijke praktijk om sterk afhankelijk te zijn van API's (application programming interfaces). Niet alleen grote diensten zoals Facebook en Twitter gebruiken ze - API's zijn erg populair vanwege de verspreiding van client-side frameworks zoals React, Angular en vele anderen. Ruby on Rails volgt deze trend en de nieuwste versie presenteert een nieuwe functie waarmee u API-only applicaties kunt maken.
Aanvankelijk was deze functionaliteit verpakt in een aparte edelsteen, rails-api genaamd, maar sinds de release van Rails 5 maakt het nu deel uit van de kern van het framework. Deze functie samen met ActionCable was waarschijnlijk de meest verwachte en daarom gaan we het vandaag bespreken.
In dit artikel wordt beschreven hoe u API-only Rails-toepassingen kunt maken en wordt uitgelegd hoe u uw routes en controllers kunt structureren, reageren met het JSON-formaat, serializers toevoegen en CORS (Cross-Origin Resource Sharing) instellen. U leert ook over enkele opties om de API te beveiligen en deze tegen misbruik te beschermen.
De bron voor dit artikel is beschikbaar op GitHub.
Voer om te beginnen de volgende opdracht uit:
rails nieuwe RailsApiDemo --api
Het gaat een nieuwe API-only Rails-applicatie maken genaamd RailsApiDemo
. Vergeet niet dat de ondersteuning voor de --api
optie is alleen toegevoegd in Rails 5, dus zorg ervoor dat deze of een nieuwere versie is geïnstalleerd.
Open de Gemfile en merk op dat het veel kleiner is dan normaal: edelstenen zoals coffee-rails
, turbolinks
, en sass-rails
zijn weg.
De config / application.rb bestand bevat een nieuwe regel:
config.api_only = true
Het betekent dat Rails een kleinere set middleware gaat laden: er is bijvoorbeeld geen ondersteuning voor cookies en sessies. Bovendien, als u een steiger probeert te genereren, zullen er geen views en assets worden aangemaakt. Eigenlijk, als je de views / layouts map, zult u merken dat de application.html.erb bestand ontbreekt ook.
Een ander belangrijk verschil is dat de ApplicationController
erft van de ActionController :: API
, niet ActionController :: Base
.
Dat is zo'n beetje alles - al met al is dit een eenvoudige Rails-applicatie die je vaak hebt gezien. Laten we nu een paar modellen toevoegen, zodat we iets hebben om mee te werken:
rails g model Gebruikersnaam: string rails g model Titel van bericht: string body: tekstgebruiker: belong_to rails db: migrate
Hier is niets speciaals aan de hand: een bericht met een titel en een lichaam is van een gebruiker.
Zorg ervoor dat de juiste associaties zijn ingesteld en geef ook enkele eenvoudige validatiecontroles:
has_many: berichten valideert: naam, aanwezigheid: waar
belong_to: user validates: title, presence: true validates: body, presence: true
Briljant! De volgende stap is het laden van een aantal voorbeeldrecords in de nieuw gemaakte tabellen.
De eenvoudigste manier om sommige gegevens te laden is door gebruik te maken van de seeds.rb bestand in de db directory. Ik ben echter lui (zoals veel programmeurs zijn) en wil geen monsterinhoud bedenken. Waarom maken we daarom geen gebruik van het favoriete juweeltje dat willekeurige gegevens van verschillende soorten kan produceren: namen, e-mails, hipsterwoorden, "lorem ipsum" -teksten en nog veel meer.
groep: ontwikkeling doe edel 'faker' einde
Installeer de edelsteen:
bundel installeren
Nu tweak seeds.rb:
5.times do user = User.create (name: Faker :: Name.name) user.posts.create (title: Faker :: Book.title, body: Faker :: Lorem.sentence) end
Laad tot slot uw gegevens:
rails db: zaad
Nu hebben we natuurlijk een aantal routes en controllers nodig om onze API te maken. Het is gebruikelijk om de routes van de API te nesten onder de api /
pad. Ontwikkelaars leveren meestal ook de API-versie in het pad, bijvoorbeeld api / v1 /
. Later, als er enkele brekingswijzigingen moeten worden geïntroduceerd, kunt u eenvoudig een nieuwe naamruimte maken (v2
) en een afzonderlijke controller.
Hier ziet u hoe uw routes eruit kunnen zien:
namespace 'api' do namespace 'v1' do resources: posts resources: users end end
Dit genereert routes zoals:
api_v1_posts GET /api/v1/posts(.:format) api / v1 / posts # index POST /api/v1/posts(.:format) api / v1 / berichten # create api_v1_post GET / api / v1 / posts /: id (.: format) api / v1 / posts # show
U mag een strekking
methode in plaats van de namespace
, maar dan standaard zal het zoeken naar UsersController
en PostsController
binnen in de controllers directory, niet in de controllers / api / v1, dus wees voorzichtig.
Maak het api map met de geneste map v1 binnen in de controllers. Vul het met uw controllers:
module Api module V1 klasse UsersController < ApplicationController end end end
module Api module V1 class PostsController < ApplicationController end end end
Merk op dat u niet alleen het bestand van de controller moet nesten onder de api / v1 pad, maar de klasse zelf moet ook worden namespaced binnen de Api
en V1
modules.
De volgende vraag is hoe u correct kunt reageren met de JSON-geformatteerde gegevens? In dit artikel zullen we deze oplossingen proberen: de jBuilder en active_model_serializers edelstenen. Dus voordat u doorgaat naar het volgende gedeelte, laat u ze vallen in de Gemfile:
gem 'jbuilder', '~> 2.5' gem 'active_model_serializers', '~> 0.10.0'
Voer dan uit:
bundel installeren
jBuilder is een populaire edelsteen die wordt onderhouden door het Rails-team dat een eenvoudige DSL (domeinspecifieke taal) biedt waarmee u JSON-structuren in uw weergaven kunt definiëren.
Stel dat we alle berichten willen weergeven wanneer een gebruiker de inhoudsopgave
actie:
def index @posts = Post.order ('created_at DESC') end
Het enige dat u hoeft te doen is de weergave te maken met de naam die verwijst naar de overeenkomstige actie met de .json.jbuilder uitbreiding. Merk op dat de weergave onder de api / v1 pad ook:
json.array! @posts do | post | json.id post.id json.title post.title json.body post.body end
json.array!
doorloopt het @posts
rangschikking. json.id
, json.title
en json.body
genereer de sleutels met de overeenkomstige namen die de argumenten instellen als de waarden. Als u naar http: // localhost: 3000 / api / v1 / posts.json navigeert, ziet u een vergelijkbare uitvoer als deze:
["id": 1, "title": "Title 1", "body": "Body 1", "id": 2, "title": "Title 2", "body": "Body 2 "]
Wat als we de auteur voor elke post ook wilden weergeven? Het is makkelijk:
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
De uitvoer zal veranderen in:
["id": 1, "title": "Title 1", "body": "Body 1", "user": "id": 1, "name": "Username"]
De inhoud van de .jbuilder bestanden zijn duidelijke Ruby-code, dus je kunt alle basishandelingen gebruiken zoals gewoonlijk.
Houd er rekening mee dat jBuilder partities ondersteunt, net als bij gewone Rails-weergave, dus u kunt ook zeggen:
json.partial! partial: 'posts / post', verzameling: @posts, as:: post
en maak vervolgens de views / api / v1 / berichten / _post.json.jbuilder bestand met de volgende inhoud:
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
Dus, zoals u ziet, is jBuilder gemakkelijk en handig. Als alternatief kunt u echter de serializers gebruiken, dus laten we ze in de volgende sectie bespreken.
De edelsteen rails_model_serializers is gemaakt door een team dat in eerste instantie de rails-api beheerde. Zoals vermeld in de documentatie, brengt rails_model_serializers conventie over configuratie naar uw JSON-generatie. Kortom, u definieert welke velden moeten worden gebruikt bij serialisatie (dat wil zeggen, JSON-generatie).
Dit is onze eerste serializer:
class PostSerializer < ActiveModel::Serializer attributes :id, :title, :body end
Hier zeggen we dat al deze velden aanwezig moeten zijn in de resulterende JSON. Nu methoden zoals to_json
en as_json
een beroep op een bericht zal deze configuratie gebruiken en de juiste inhoud retourneren.
Om het in actie te zien, wijzigt u de inhoudsopgave
actie als deze:
def index @posts = Post.order ('created_at DESC') render json: @posts end
as_json
wordt automatisch opgeroepen voor de @posts
voorwerp.
Hoe zit het met de gebruikers? Serializers laten je relaties aan, net als modellen. Wat meer is, serializers kunnen worden genest:
class PostSerializer < ActiveModel::Serializer attributes :id, :title, :body belongs_to :user class UserSerializer < ActiveModel::Serializer attributes :id, :name end end
Wanneer u nu het bericht serialiseert, zal het automatisch de geneste bevatten gebruiker
sleutel met zijn id en naam. Als u later een afzonderlijke serializer maakt voor de gebruiker met de :ID kaart
kenmerk uitgesloten:
class UserSerializer < ActiveModel::Serializer attributes :name end
dan @ user.as_json
zal de id van de gebruiker niet retourneren. Nog steeds, @ post.as_json
zal zowel de gebruikersnaam als de id van de gebruiker retourneren, dus houd hier rekening mee.
In veel gevallen willen we niet dat iemand gewoon een actie uitvoert met behulp van de API. Laten we daarom een eenvoudige beveiligingscontrole presenteren en onze gebruikers dwingen hun tokens te verzenden bij het maken en verwijderen van berichten.
Het token heeft een onbeperkte levensduur en wordt gemaakt na registratie van de gebruiker. Voeg eerst een nieuw toe blijk
kolom naar de gebruikers
tafel:
rails g migratie add_token_to_users token: string: index
Deze index moet uniek zijn omdat er niet twee gebruikers met hetzelfde token kunnen zijn:
add_index: gebruikers,: token, unique: true
Pas de migratie toe:
rails db: migreren
Voeg nu de before_save
Bel terug:
before_create -> self.token = generate_token
De generate_token
private methode maakt een token in een eindeloze cyclus en controleert of het uniek is of niet. Zodra een uniek token is gevonden, retourneert u het:
private def generate_token lus do token = SecureRandom.hex retourneer-token tenzij User.exists? (token: token) end end
U kunt een ander algoritme gebruiken om het token te genereren, bijvoorbeeld op basis van de MD5-hash van de naam van de gebruiker en wat zout.
Natuurlijk moeten we gebruikers ook toestaan zich te registreren, omdat ze anders hun token niet kunnen krijgen. Ik wil geen HTML-weergaven introduceren in onze applicatie, dus laten we in plaats daarvan een nieuwe API-methode toevoegen:
def create @user = User.new (user_params) if @ user.save status:: created else render json: @ user.errors, status:: unprocessable_entity end end private def user_params params.require (: user) .permit (: naam) einde
Het is een goed idee om betekenisvolle HTTP-statuscodes terug te geven, zodat ontwikkelaars precies begrijpen wat er aan de hand is. Nu kunt u een nieuwe serializer voor de gebruikers opgeven of een a .json.jbuilder het dossier. Ik geef de voorkeur aan de laatste variant (daarom passeer ik de : json
optie voor de geven
methode), maar u bent vrij om een van deze te kiezen. Merk echter op dat het token moet niet zijn altijd geserialiseerd, bijvoorbeeld wanneer u een lijst met alle gebruikers retourneert - deze moet veilig worden bewaard!
json.id @ user.id json.name @ user.name json.token @ user.token
De volgende stap is om te testen of alles goed werkt. U kunt de Krul
commando of schrijf wat Ruby-code. Aangezien dit artikel over Ruby gaat, ga ik met de codeeroptie.
Om een HTTP-verzoek uit te voeren, gebruiken we de Faraday-edelsteen, die een gemeenschappelijke interface biedt voor veel adapters (de standaardinstelling is Net :: HTTP
). Maak een apart Ruby-bestand, voeg Faraday toe en stel de client in:
vereisen 'faraday' client = 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 = '"user": "name": "test user"' end
Al deze opties spreken voor zich: we kiezen de standaardadapter, stellen de verzoek-URL in op http: // localhost: 300 / api / v1 / users, veranderen het inhoudstype in application / json
, en geef het lichaam van ons verzoek.
Het antwoord van de server zal JSON bevatten, dus om het te ontleden, gebruik ik de Oj-edelsteen:
vereist hier 'oj' # client ... puts Oj.load (response.body) plaatst response.status
Afgezien van de geparseerde reactie, toon ik ook de statuscode voor foutopsporing.
Nu kunt u eenvoudig dit script uitvoeren:
ruby api_client.rb
en bewaar het ontvangen token ergens - we zullen het in de volgende sectie gebruiken.
Voor het afdwingen van de token-authenticatie, de authenticate_or_request_with_http_token
methode kan worden gebruikt. Het is een onderdeel van de ActionController :: HttpAuthentication :: Token :: ControllerMethods-module, dus vergeet niet om het op te nemen:
class PostsController < ApplicationController include ActionController::HttpAuthentication::Token::ControllerMethods #… end
Voeg een nieuwe toe before_action
en de bijbehorende methode:
before_action: authenticate, only: [: create,: destroy] # ... private # ... def authenticate authenticate_or_request_with_http_token do | token, opties | @user = User.find_by (token: token) end end
Als het token niet is ingesteld of als een gebruiker met een dergelijk token niet kan worden gevonden, wordt een 401-fout geretourneerd, waardoor de actie niet kan worden uitgevoerd.
Houd er rekening mee dat de communicatie tussen de client en de server via HTTPS moet worden gemaakt, omdat de tokens anders gemakkelijk kunnen worden vervalst. Natuurlijk is de geboden oplossing niet ideaal, en in veel gevallen verdient het de voorkeur om het OAuth 2-protocol te gebruiken voor authenticatie. Er zijn ten minste twee edelstenen die het ondersteuningsproces van deze functie aanzienlijk vereenvoudigen: Doorkeeper en oPRO.
Om onze verificatie in actie te zien, voegt u de creëren
actie voor de PostsController
:
def create @post = @ user.posts.new (post_params) if @ post.save render json: @post, status:: created else render json: @ post.errors, status:: unprocessable_entity end end
We maken hier gebruik van de serializer om de juiste JSON weer te geven. De @gebruiker
was al ingesteld in de before_action
.
Test nu alles uit met behulp van deze eenvoudige code:
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": "Tekst" 'einde
Vervang het argument doorgegeven aan de token_auth
met het token ontvangen bij registratie en voer het script uit.
ruby api_client.rb
Het verwijderen van een bericht gebeurt op dezelfde manier. Voeg de toe vernietigen
actie:
def destroy @post = @ user.posts.find_by (params [: id]) als @post @ post.destroy else render json: post: "not found", status:: not_found end end
We staan alleen gebruikers toe om de berichten die ze daadwerkelijk bezitten te vernietigen. Als het bericht met succes is verwijderd, wordt de 204-statuscode (geen inhoud) geretourneerd. Je kunt ook reageren met de id van de post die is verwijderd omdat deze nog steeds beschikbaar is in het geheugen.
Hier is het stuk code om deze nieuwe functie te testen:
response = client.delete do | req | req.url '/ api / v1 / posts / 6' req.headers ['Content-type'] = 'application / json' end
Vervang de id van de post door een nummer dat voor u werkt.
Als u andere webservices toegang wilt geven tot uw API (vanaf de client-kant), moet CORS (Cross-Origin Resource Sharing) correct zijn ingesteld. Kort gezegd staat CORS toe dat webtoepassingen AJAX-verzoeken verzenden naar de services van derden. Gelukkig is er een juweel genaamd rack-cors dat ons in staat stelt om alles gemakkelijk in te stellen. Voeg het toe aan de Gemfile:
gem 'rack-cors'
Installeer het:
bundel installeren
En geef vervolgens de configuratie op in de config / initialiseerders / cors.rb het dossier. Eigenlijk is dit bestand al voor u gemaakt en bevat het een gebruiksmodel. Je kunt ook een vrij gedetailleerde documentatie vinden op de edelstenenpagina.
Met de volgende configuratie kan iedereen bijvoorbeeld uw API gebruiken op elke manier:
Rails.application.config.middleware.insert_before 0, Rack :: Cors do allow do origins '*' resource '/ api / *', headers:: any, methods: [: get,: post,: put,: patch, : delete,: options,: head] end end
Het laatste dat ik in deze handleiding ga vermelden, is hoe u uw API kunt beschermen tegen misbruik en denial-of-service-aanvallen. Er is een mooi juweel genaamd rack-attack (gemaakt door mensen van Kickstarter) waarmee je klanten op een zwarte lijst kunt zetten of op de witte lijst kunt zetten, het flooding van een server met verzoeken en meer kunt voorkomen.
Laat het juweel vallen Gemfile:
gem 'rack-attack'
Installeer het:
bundel installeren
En zorg dan voor configuratie binnen de rack_attack.rb initializer-bestand. De documentatie van het juweel somt alle beschikbare opties op en suggereert enkele use-cases. Hier is de voorbeeldconfiguratie die iedereen behalve u beperkt tot toegang tot de service en het maximale aantal verzoeken beperkt tot 5 per seconde:
class Rack :: Aanval safelist ('allow from localhost') do | req | # Verzoeken zijn toegestaan als de retourwaarde waarheidsgetrouw is '127.0.0.1' == req.ip || ':: 1' == req.ip end throttle ('req / ip',: limit => 5,: period => 1.second) do | req | req.ip end end
Een ander ding dat gedaan moet worden, is RackAttack als een middleware:
config.middleware.use Rack :: Aanval
We zijn aan het einde van dit artikel gekomen. Hopelijk voel je je nu meer zelfverzekerd over het maken van API's met Rails! Houd er rekening mee dat dit niet de enige beschikbare optie is. Een andere populaire oplossing die al geruime tijd bestaat, is het Grape-raamwerk, dus u kunt ook geïnteresseerd zijn om het te bekijken..
Aarzel niet om uw vragen te stellen als u iets onduidelijk lijkt. Ik dank je dat je bij me bent gebleven en dat je blij bent met het coderen!