Crafting API's met Rails

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.

Een API-only applicatie maken

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:

modellen / user.rb

 has_many: berichten valideert: naam, aanwezigheid: waar

modellen / post.rb

 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.

Demo-gegevens laden

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.

Gemfile

groep: ontwikkeling doe edel 'faker' einde

Installeer de edelsteen:

bundel installeren

Nu tweak seeds.rb:

db / 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

Reageren met JSON

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:

config / routes.rb

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:

controllers / api / v1 / users_controller.rb

module Api module V1 klasse UsersController < ApplicationController end end end

controllers / api / v1 / posts_controller.rb

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:

Gemfile

gem 'jbuilder', '~> 2.5' gem 'active_model_serializers', '~> 0.10.0'

Voer dan uit:

bundel installeren

De jBuilder-edelsteen gebruiken

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:

controllers / api / v1 / posts_controller.rb

 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:

views / api / v1 / berichten / index.json.jbuilder

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.

Serializers gebruiken

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:

serializers / post_serializer.rb

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:

controllers / api / v1 / posts_controller.rb

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:

serializers / post_serializer.rb

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:

serializers / post_serializer.rb

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.

De API beveiligen

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:

db / migrate / xyz_add_token_to_users.rb

add_index: gebruikers,: token, unique: true

Pas de migratie toe:

rails db: migreren

Voeg nu de before_save Bel terug:

modellen / user.rb

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:

modellen / user.rb

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.

Gebruikersregistratie

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:

controllers / api / v1 / users_controller.rb

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!

views / api / v1 / users / create.json.jbuilder

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.

Gebruikersregistratie testen

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:

api_client.rb

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:

api_client.rb

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.

Authenticeren met het token

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:

controllers / api / v1 / posts_controller.rb 

class PostsController < ApplicationController include ActionController::HttpAuthentication::Token::ControllerMethods #… end

Voeg een nieuwe toe before_action en de bijbehorende methode:

controllers / api / v1 / posts_controller.rb 

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.

Een bericht maken

Om onze verificatie in actie te zien, voegt u de creëren actie voor de PostsController:

controllers / api / v1 / posts_controller.rb 

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:

api_client.rb

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

Een bericht verwijderen

Het verwijderen van een bericht gebeurt op dezelfde manier. Voeg de toe vernietigen actie:

controllers / api / v1 / posts_controller.rb 

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:

api_client.rb

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.

CORS instellen

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:

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:

config / initialiseerders / cors.rb

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

Misbruik voorkomen

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:

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:

config / initialiseerders / rack_attack.rb

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 / application.rb

config.middleware.use Rack :: Aanval

Conclusie

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!