¿Cómo funciona... ? Devise
Link: https://rubygems.org/gems/devise
¿Qué hace?
Seguimos con otra gema que conocemos casi todos, la gema más utilizada (por lejos) para autenticación de usuarios.
¿Cómo se usa?
No vamos a entrar en mucho detalle porque hay pasos que no son interesantes para explicar. Los pasos iniciales son: 1) agregar la gema: gem 'devise'; y 2) correr el comando instalador 'rails generate devise:install'. Una vez instalada nos va a mostrar en la consola algunas instrucciones sobre cómo continuar, configurar modelos, rutas, etc.
La gema ofrece mucha funcionalidad relacionadas al manejo de cuentas además de la autenticación (emails, confirmación, invitaciones, recuperar contraseña, etc) pero hoy vamos a hablar sólo de la autenticación por ahora y quizás en otro momento analicemos otros módulos, ya que todo el proceso hasta llegar a la autenticación es largo e interesante de explicar.
Además de permitir autenticar al usuario nos provee varios helper methods para usar en vistas y controladores para saber si el usuario está o no loggeado.
¿Cómo funciona la instalación?
Lo primero que vamos a analizar es el comando que instala Devise, las otras gemas que analizamos no tenían esta funcionalidad. El "instalador" en realidad es un generador, estos son clases con funcionalidades específicas para agregar código a nuestra aplicación con el fin de tener una base necesaria de configuración/templates/etc y la posibilidad de modificar los archivos generados para customizar el uso de la gema.
Crear un generador es fácil, sólo hay que agregar un archivo ruby en lib/generators con el nombre del generador y el sufijo "_generator". Si el archivo del generador se encuentra dentro de un directorio hijo de generators, el comando va a dividir el generador y el directorio con ":". Entonces, dentro del directorio lib/generators/devise de la gema podemos ver estos archivos:
Si ejecutamos `rails generate | grep 'devise'` podemos ver que tenemos 4 generadores que empiezan con "devise:" y corresponden a esos 4 archivos "_generator" (el que queda no es un generador):
Veamos el initializer que nos pide ejecutar la documentación:
El generador hereda de Rails::Generators::Base y al ejecutarlo corre los métodos públicos uno a uno secuencialmente: Primero copia el template del initializer, luego copia el archivo de internacionalización y al final muestra el archivo README. Los archivos a copiar los lee del path definido en la línea `source_root File.expand_path("../../templates", __FILE__)`, si vamos a ese directorio vamos a ver los archivos que se copian.
¿Cómo se inicializa la gema?
Como en la mayoría de las gemas, el archivo de entrada es el archivo en /lib con el nombre de la misma.
Lo vamos a dividir en tres partes:
Esta parte no es muy interesante, sólo carga módulos de ruby y rails.
Esta sección define el módulo Devise, hace muchos "autoload" (vamos a hablar más adelante de esto), carga todas las variables para configurar la gema, etc.
Acá nos interesan 2 de esos require: warden y devise/rails (models y modules intervienen en lo que vimos antes al requerir la extensión de ActiveRecord en el initializer, mapping está asociado a las rutas).
Bonus: ¿Qué es Warden?
Si usaron Devise algún tiempo seguro leyeron muchas veces esta palabra. Warden es una gema que se usa como middleware en el stack de la aplicación y se encarga de la autenticación a nivel stack. Devise usa Warden como base para guardar el usuario loggeado y esto permite tener al usuario autenticado incluso antes de que el request llegue al a aplicación de Rails (por eso podemos usar métodos asociados a la autenticación en el router incluso antes de siquiere llegar a un controller!). En un rato vamos a ver cómo se relacionan con un poco más de detalle.
Finalmente require el módulo Devise::Rails. Primero le dice a rails que agregue Warden::Manager como middleware y copia la configuración de Warden dentro de Devise:
# lib/devise/rails.rb
Luego agrega las rutas de devise:
Agrega helper methods en los controllers:
(Y algunas configuraciones más asociadas a oauth.)
¿Cómo se configura?
El generador "install" creó un archivo `devise.rb` en la carpeta config/initializers. Todos los archivos dentro de esa carpeta son ejecutados al iniciar nuestra aplicación luego de que se cargaron las gemas, por eso tenemos acceso a la método setup de la clase Devise.
Este archivo nos permite configurar la gema: el método recibe un bloque y pasa la clase Devise así se pueden configurar todas las variables de clase. (Link al código)
Pero además hace algo que, personalmente, me parece raro: requiere la extensión de ActiveRecord de Devise dentro de la configuración (me parece raro porque el resto de líneas son "config.clave = valor" y esta línea pareciera no encajar). Este archivo es el que termina agregando en nuestros modelos un helper para incluir los distintos módulos de devise (entre otras cosas) al extender ActiveRecord con el módulo Devise::Models definido en lib/devise/models.rb
Seguramente habrán visto que el modelo que autentica Devise incluye distintas features usando el método (valga la redundancia) 'devise'. Ej:
Cada uno de esos symbols representa un módulo para cada funcionalidad que provee Devise, cada módulo está implementado como un Concern y sólo se incluyen los módulos que queramos usar en nuestra app y sólo en la clase que lo configuremos (a diferencia de Authlogic que agrega toda la funcionalidad en ActiveRecord::Base ensuciando todas las clases incluso si no se van a usar para autenticación, Devise también ensucia un poco pero agrega muchos menos métodos).
¿Cómo interactuan Warden y Devise?
Devise define una "estrategia" que va a usar Warden para autenticar, básicamente la estrategia es el algoritmo para validar la entrada del usuario y verificar si corresponde o no a un usuario en nuestra base de datos. Si los datos son correctos, llama al método `success!` para que finalmente Warden guarde al usuario.
Para loggear a un usuario, Devise llama al método authenticate! en el objeto warden. Este al final valida los datos provistos por el usuario usando esa estrategia y, si son válidos, guarda el usuario en el objeto warden.
También podemos consultar si el usuario está loggeado, para esto Devise también usa el objeto warden del stack con varios métodos que agregó mediante un helper.
Con ese código vemos cómo agrega muchos helper methods usando el número de nuestro modelo y que cada método termina ejecutando un método en el objeto "warden".
Entonces, Devise no se encarga de todo el proceso de autenticación, sólo valida los datos ingresados por el usuarios pero el resultado de esa validación lo guardar Warden y finalmente se usa este para verificar si el usuario está o no loggeado.
Conclusión
Aprendimos sobre: generadores, initializers, un proceso de carga de la gema más complejo que antes, interacción con Warden (un middleware) y cómo funciona la configuración de Devise. Provee muchas más cosas que vamos a ir viendo en otros posts.
Recursos:
Generadores: https://guides.rubyonrails.org/generators.html
Warden Overview: The What, The Way and The How: https://github.com/wardencommunity/warden/wiki/Overview
Rack: https://github.com/rack/rack
¿Qué hace?
Seguimos con otra gema que conocemos casi todos, la gema más utilizada (por lejos) para autenticación de usuarios.
¿Cómo se usa?
No vamos a entrar en mucho detalle porque hay pasos que no son interesantes para explicar. Los pasos iniciales son: 1) agregar la gema: gem 'devise'; y 2) correr el comando instalador 'rails generate devise:install'. Una vez instalada nos va a mostrar en la consola algunas instrucciones sobre cómo continuar, configurar modelos, rutas, etc.
La gema ofrece mucha funcionalidad relacionadas al manejo de cuentas además de la autenticación (emails, confirmación, invitaciones, recuperar contraseña, etc) pero hoy vamos a hablar sólo de la autenticación por ahora y quizás en otro momento analicemos otros módulos, ya que todo el proceso hasta llegar a la autenticación es largo e interesante de explicar.
Además de permitir autenticar al usuario nos provee varios helper methods para usar en vistas y controladores para saber si el usuario está o no loggeado.
¿Cómo funciona la instalación?
Lo primero que vamos a analizar es el comando que instala Devise, las otras gemas que analizamos no tenían esta funcionalidad. El "instalador" en realidad es un generador, estos son clases con funcionalidades específicas para agregar código a nuestra aplicación con el fin de tener una base necesaria de configuración/templates/etc y la posibilidad de modificar los archivos generados para customizar el uso de la gema.
Crear un generador es fácil, sólo hay que agregar un archivo ruby en lib/generators con el nombre del generador y el sufijo "_generator". Si el archivo del generador se encuentra dentro de un directorio hijo de generators, el comando va a dividir el generador y el directorio con ":". Entonces, dentro del directorio lib/generators/devise de la gema podemos ver estos archivos:
# lib/generators/devise
controllers_generator.rb
devise_generator.rb
install_generator.rb
orm_helpers.rb
views_generator.rb
Si ejecutamos `rails generate | grep 'devise'` podemos ver que tenemos 4 generadores que empiezan con "devise:" y corresponden a esos 4 archivos "_generator" (el que queda no es un generador):
# rails generate | grep 'devise'
...
devise
devise:controllers
devise:install
devise:views
...
Veamos el initializer que nos pide ejecutar la documentación:
# lib/generators/devise/install_generator.rb
require 'rails/generators/base'
require 'securerandom'
module Devise
module Generators
MissingORMError = Class.new(Thor::Error)
class InstallGenerator < Rails::Generators::Base
source_root File.expand_path("../../templates", __FILE__)
desc "Creates a Devise initializer and copy locale files to your application."
class_option :orm
def copy_initializer
.....
template "devise.rb", "config/initializers/devise.rb"
end
def copy_locale
copy_file "../../../config/locales/en.yml", "config/locales/devise.en.yml"
end
def show_readme
readme "README" if behavior == :invoke
end
def rails_4?
Rails::VERSION::MAJOR == 4
end
end
end
end
El generador hereda de Rails::Generators::Base y al ejecutarlo corre los métodos públicos uno a uno secuencialmente: Primero copia el template del initializer, luego copia el archivo de internacionalización y al final muestra el archivo README. Los archivos a copiar los lee del path definido en la línea `source_root File.expand_path("../../templates", __FILE__)`, si vamos a ese directorio vamos a ver los archivos que se copian.
¿Cómo se inicializa la gema?
Como en la mayoría de las gemas, el archivo de entrada es el archivo en /lib con el nombre de la misma.
Lo vamos a dividir en tres partes:
# lib/devise.rb
require 'rails'
require 'active_support/core_ext/numeric/time'
require 'active_support/dependencies'
require 'orm_adapter'
require 'set'
require 'securerandom'
require 'responders'
Esta parte no es muy interesante, sólo carga módulos de ruby y rails.
# lib/devise.rb
module Devise
autoload :Delegator, 'devise/delegator'
...
end
Esta sección define el módulo Devise, hace muchos "autoload" (vamos a hablar más adelante de esto), carga todas las variables para configurar la gema, etc.
# lib/devise.rb
require 'warden'
require 'devise/mapping'
require 'devise/models'
require 'devise/modules'
require 'devise/rails'
Acá nos interesan 2 de esos require: warden y devise/rails (models y modules intervienen en lo que vimos antes al requerir la extensión de ActiveRecord en el initializer, mapping está asociado a las rutas).
Bonus: ¿Qué es Warden?
Si usaron Devise algún tiempo seguro leyeron muchas veces esta palabra. Warden es una gema que se usa como middleware en el stack de la aplicación y se encarga de la autenticación a nivel stack. Devise usa Warden como base para guardar el usuario loggeado y esto permite tener al usuario autenticado incluso antes de que el request llegue al a aplicación de Rails (por eso podemos usar métodos asociados a la autenticación en el router incluso antes de siquiere llegar a un controller!). En un rato vamos a ver cómo se relacionan con un poco más de detalle.
Finalmente require el módulo Devise::Rails. Primero le dice a rails que agregue Warden::Manager como middleware y copia la configuración de Warden dentro de Devise:
# lib/devise/rails.rb
module Devise
class Engine < ::Rails::Engine
config.devise = Devise
# Initialize Warden and copy its configurations.
config.app_middleware.use Warden::Manager do |config|
Devise.warden_config = config
end
Luego agrega las rutas de devise:
# Force routes to be loaded if we are doing any eager load.
config.before_eager_load do |app|
app.reload_routes! if Devise.reload_routes
end
Agrega helper methods en los controllers:
initializer "devise.url_helpers" do
Devise.include_helpers(Devise::Controllers)
end
(Y algunas configuraciones más asociadas a oauth.)
¿Cómo se configura?
El generador "install" creó un archivo `devise.rb` en la carpeta config/initializers. Todos los archivos dentro de esa carpeta son ejecutados al iniciar nuestra aplicación luego de que se cargaron las gemas, por eso tenemos acceso a la método setup de la clase Devise.
# config/initializers/devise.rb
Devise.setup do |config|
...
require 'devise/orm/active_record'
...
end
Este archivo nos permite configurar la gema: el método recibe un bloque y pasa la clase Devise así se pueden configurar todas las variables de clase. (Link al código)
Pero además hace algo que, personalmente, me parece raro: requiere la extensión de ActiveRecord de Devise dentro de la configuración (me parece raro porque el resto de líneas son "config.clave = valor" y esta línea pareciera no encajar). Este archivo es el que termina agregando en nuestros modelos un helper para incluir los distintos módulos de devise (entre otras cosas) al extender ActiveRecord con el módulo Devise::Models definido en lib/devise/models.rb
# lib/devise/models.rb
module Devise
module Models
...
def self.config(mod, *accessors) #:nodoc:
...
end
def self.check_fields!(klass)
...
end
def devise(*modules)
...
Seguramente habrán visto que el modelo que autentica Devise incluye distintas features usando el método (valga la redundancia) 'devise'. Ej:
class Admin < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
Cada uno de esos symbols representa un módulo para cada funcionalidad que provee Devise, cada módulo está implementado como un Concern y sólo se incluyen los módulos que queramos usar en nuestra app y sólo en la clase que lo configuremos (a diferencia de Authlogic que agrega toda la funcionalidad en ActiveRecord::Base ensuciando todas las clases incluso si no se van a usar para autenticación, Devise también ensucia un poco pero agrega muchos menos métodos).
¿Cómo interactuan Warden y Devise?
Devise define una "estrategia" que va a usar Warden para autenticar, básicamente la estrategia es el algoritmo para validar la entrada del usuario y verificar si corresponde o no a un usuario en nuestra base de datos. Si los datos son correctos, llama al método `success!` para que finalmente Warden guarde al usuario.
# lib/devise/strategies/database_authenticatable.rb
module Devise
module Strategies
# Default strategy for signing in a user, based on their email and password in the database.
class DatabaseAuthenticatable < Authenticatable
def authenticate!
resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash)
hashed = false
if validate(resource){ hashed = true; resource.valid_password?(password) }
...
success!(resource)
end
...
end
end
end
end
Warden::Strategies.add(:database_authenticatable, Devise::Strategies::DatabaseAuthenticatable)
Para loggear a un usuario, Devise llama al método authenticate! en el objeto warden. Este al final valida los datos provistos por el usuario usando esa estrategia y, si son válidos, guarda el usuario en el objeto warden.
# app/controllers/devise/sessions_controller.rb
def create
self.resource = warden.authenticate!(auth_options)
...
end
También podemos consultar si el usuario está loggeado, para esto Devise también usa el objeto warden del stack con varios métodos que agregó mediante un helper.
# lib/devise/controllers/helpers.rb
def self.define_helpers(mapping) #:nodoc:
mapping = mapping.name
class_eval <<-METHODS, __FILE__, __LINE__ + 1
def authenticate_#{mapping}!(opts={})
opts[:scope] = :#{mapping}
warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
end
def #{mapping}_signed_in?
!!current_#{mapping}
end
def current_#{mapping}
@current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
end
def #{mapping}_session
current_#{mapping} && warden.session(:#{mapping})
end
METHODS
ActiveSupport.on_load(:action_controller) do
if respond_to?(:helper_method)
helper_method "current_#{mapping}", "#{mapping}_signed_in?", "#{mapping}_session"
end
end
end
Con ese código vemos cómo agrega muchos helper methods usando el número de nuestro modelo y que cada método termina ejecutando un método en el objeto "warden".
Entonces, Devise no se encarga de todo el proceso de autenticación, sólo valida los datos ingresados por el usuarios pero el resultado de esa validación lo guardar Warden y finalmente se usa este para verificar si el usuario está o no loggeado.
Conclusión
Aprendimos sobre: generadores, initializers, un proceso de carga de la gema más complejo que antes, interacción con Warden (un middleware) y cómo funciona la configuración de Devise. Provee muchas más cosas que vamos a ir viendo en otros posts.
Recursos:
Generadores: https://guides.rubyonrails.org/generators.html
Warden Overview: The What, The Way and The How: https://github.com/wardencommunity/warden/wiki/Overview
Rack: https://github.com/rack/rack
Comentarios
Publicar un comentario