使用 devise_token_auth 和 active_model_serializer 时覆盖序列化程序
Overriding serializer when using devise_token_auth and active_model_serializer
在使用 devise_token_auth 和 active_model_serializer 设计 sign_up 方法时,我无法覆盖 Rails 序列化程序。
我想在查询我的 API 时自定义从 Devise sign_up 控制器返回的字段。
devise_token_auth gem 文档表明:
要自定义 json 渲染,请实现以下受保护的控制器方法
注册控制器
...
render_create_success
...
注意:控制器覆盖必须实现它们替换的控制器的预期操作。
一切都很好,但是我该怎么做呢?
我已经尝试生成如下所示的 UserController 序列化程序:
class UsersController < ApplicationController
def default_serializer_options
{ serializer: UserSerializer }
end
# GET /users
def index
@users = User.all
render json: @users
end
end
但它仅用于自定义方法,例如上面的索引方法:它不会被 sign_up
等设计方法采用
如果我能得到详细的答复,我将不胜感激,因为我到处都看过,但我一次只得到了一个谜题。
Devise sign_up 对应于 devise_token_auth 注册控制器,Devise sign_in 对应于 devise_token_auth 会话控制器。因此,在使用此 gem 时,自定义 Devise sign_in 和 sign_up 方法需要自定义这两个 devise_token_auth 控制器。
根据您需要完成的任务,有两种方法可以解决此问题。
方法一
如果您想完全自定义控制器中的方法,请在此处按照文档覆盖 devise_token_auth 控制器方法:https://github.com/lynndylanhurley/devise_token_auth#custom-controller-overrides
这就是我所做的并且工作正常:
#config/routes.rb
...
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
sessions: 'overrides/sessions',
registrations: 'overrides/registrations'
}
...
如果您的本地控制器覆盖中存在方法,这会将所有 devise_token_auth 会话和注册路由到控制器的本地版本。如果该方法在您的本地覆盖中不存在,那么它将 运行 来自 gem 的方法。您基本上必须将控制器从 gem 复制到 'app/controllers/overrides' 并对您需要自定义的任何方法进行任何更改。从您未自定义的本地副本中删除方法。您也可以通过这种方式添加回调。如果要修改响应,请在方法末尾自定义渲染,通过 active_model_serializer.
将响应 return 作为 json
这是我的会话控制器的示例,它添加了几个自定义 before_actions 来添加自定义功能:
#app/controllers/overrides/sessions_controller.rb
module Overrides
class SessionsController < DeviseTokenAuth::SessionsController
skip_before_action :authenticate_user_with_filter
before_action :set_country_by_ip, :only => [:create]
before_action :create_facebook_user, :only => [:create]
def create
# Check
field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first
@resource = nil
if field
q_value = resource_params[field]
if resource_class.case_insensitive_keys.include?(field)
q_value.downcase!
end
#q = "#{field.to_s} = ? AND provider='email'"
q = "#{field.to_s} = ? AND provider='#{params[:provider]}'"
#if ActiveRecord::Base.connection.adapter_name.downcase.starts_with? 'mysql'
# q = "BINARY " + q
#end
@resource = resource_class.where(q, q_value).first
end
#sign in will be successful if @resource exists (matching user was found) and is a facebook login OR (email login and password matches)
if @resource and (params[:provider] == 'facebook' || (valid_params?(field, q_value) and @resource.valid_password?(resource_params[:password]) and (!@resource.respond_to?(:active_for_authentication?) or @resource.active_for_authentication?)))
# create client id
@client_id = SecureRandom.urlsafe_base64(nil, false)
@token = SecureRandom.urlsafe_base64(nil, false)
@resource.tokens[@client_id] = { token: BCrypt::Password.create(@token), expiry: (Time.now + DeviseTokenAuth.token_lifespan).to_i }
@resource.save
sign_in(:user, @resource, store: false, bypass: false)
yield @resource if block_given?
#render_create_success
render json: { data: resource_data(resource_json: @resource.token_validation_response) }
elsif @resource and not (!@resource.respond_to?(:active_for_authentication?) or @resource.active_for_authentication?)
render_create_error_not_confirmed
else
render_create_error_bad_credentials
end
end
def set_country_by_ip
if !params['fb_code'].blank?
if !params['user_ip'].blank?
#checks if IP sent is valid, otherwise raise an error
raise 'Invalid IP' unless (params['user_ip'] =~ Resolv::IPv4::Regex ? true : false)
country_code = Custom::FacesLibrary.get_country_by_ip(params['user_ip'])
country_id = Country.find_by(country_code: country_code)
if country_id
params.merge!(country_id: country_id.id, country_name: country_id.name, test: 'Test')
I18n.locale = country_id.language_code
else
params.merge!(country_id: 1, country_name: 'International')
end
else
params.merge!(country_id: 1, country_name: 'International')
end
end
end
def create_facebook_user
if !params['fb_code'].blank?
# TODO capture errors for invalid, expired or already used codes to return beter errors in API
user_info, access_token = Omniauth::Facebook.authenticate(params['fb_code'])
if user_info['email'].blank?
Omniauth::Facebook.deauthorize(access_token)
end
#if Facebook user does not exist create it
@user = User.find_by('uid = ? and provider = ?', user_info['id'], 'facebook')
if !@user
@graph = Koala::Facebook::API.new(access_token, ENV['FACEBOOK_APP_SECRET'])
Koala.config.api_version = "v2.6"
new_user_picture = @graph.get_picture_data(user_info['id'], type: :normal)
new_user_info = {
uid: user_info['id'],
provider: 'facebook',
email: user_info['email'],
name: user_info['name'],
first_name: user_info['first_name'],
last_name: user_info['last_name'],
image: new_user_picture['data']['url'],
gender: user_info['gender'],
fb_auth_token: access_token,
friend_count: user_info['friends']['summary']['total_count'],
friends: user_info['friends']['data']
}
@user = User.new(new_user_info)
@user.password = Devise.friendly_token.first(8)
@user.country_id = params['country_id']
@user.country_name = params['country_name']
if !@user.save
render json: @user.errors, status: :unprocessable_entity
end
end
#regardless of user creation, merge facebook parameters for proper sign_in in standard action
params.merge!(provider: 'facebook', email: @user.email)
else
params.merge!(provider: 'email')
end
end
end
end
注意在回调中使用 params.merge!
将自定义参数添加到主控制器方法。这是一个漂亮的技巧,不幸的是将在 Rails 5.1 中被弃用,因为参数将不再继承自散列。
方法#2
如果您只想向自定义控制器中的方法添加功能,您可以通过子类化控制器、从原始控制器继承并将块传递给 super,如下所述:
https://github.com/lynndylanhurley/devise_token_auth#passing-blocks-to-controllers
我已经对自定义注册控制器中的创建方法执行了此操作。
按照方法#1 修改路线
#config/routes.rb
...
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
sessions: 'overrides/sessions',
registrations: 'overrides/registrations'
}
...
并在自定义控制器中自定义create
方法:
#app/controllers/overrides/registrations_controller.rb
module Overrides
class RegistrationsController < DeviseTokenAuth::RegistrationsController
skip_before_action :authenticate_user_with_filter
#will run upon creating a new registration and will set the country_id and locale parameters
#based on whether or not a user_ip param is sent with the request
#will default to country_id=1 and locale='en' (International) if it's not sent.
before_action :set_country_and_locale_by_ip, :only => [:create]
def set_country_and_locale_by_ip
if !params['user_ip'].blank?
#checks if IP sent is valid, otherwise raise an error
raise 'Invalid IP' unless (params['user_ip'] =~ Resolv::IPv4::Regex ? true : false)
country_code = Custom::FacesLibrary.get_country_by_ip(params['user_ip'])
#TODO check if there's an internet connection here or inside the library function
#params.merge!(country_id: 1, country_name: 'International', locale: 'en')
country_id = Country.find_by(country_code: country_code)
if country_id
params.merge!(country_id: country_id.id, locale: country_id.language_code, country_name: country_id.name)
else
params.merge!(country_id: 1, country_name: 'International', locale: 'en')
end
else
params.merge!(country_id: 1, country_name: 'International', locale: 'en')
end
end
#this will add behaviour to the registrations controller create method
def create
super do |resource|
create_assets(@resource)
end
end
def create_assets(user)
begin
Asset.create(user_id: user.id, name: "stars", qty: 50)
Asset.create(user_id: user.id, name: "lives", qty: 5)
Asset.create(user_id: user.id, name: "trophies", qty: 0)
end
end
end
end
对于特定的序列化程序问题,我是这样做的:
overrides/sessions_controller.rb
module Api
module V1
module Overrides
class SessionsController < ::DeviseTokenAuth::SessionsController
# override this method to customise how the resource is rendered. in this case an ActiveModelSerializers 0.10 serializer.
def render_create_success
render json: { data: ActiveModelSerializers::SerializableResource.new(@resource).as_json }
end
end
end
end
end
config/routes.rb
namespace :api, defaults: {format: 'json'} do
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
sessions: 'api/v1/overrides/sessions'
}
# snip the rest
在使用 devise_token_auth 和 active_model_serializer 设计 sign_up 方法时,我无法覆盖 Rails 序列化程序。
我想在查询我的 API 时自定义从 Devise sign_up 控制器返回的字段。
devise_token_auth gem 文档表明:
要自定义 json 渲染,请实现以下受保护的控制器方法
注册控制器
...
render_create_success
...
注意:控制器覆盖必须实现它们替换的控制器的预期操作。
一切都很好,但是我该怎么做呢?
我已经尝试生成如下所示的 UserController 序列化程序:
class UsersController < ApplicationController
def default_serializer_options
{ serializer: UserSerializer }
end
# GET /users
def index
@users = User.all
render json: @users
end
end
但它仅用于自定义方法,例如上面的索引方法:它不会被 sign_up
等设计方法采用如果我能得到详细的答复,我将不胜感激,因为我到处都看过,但我一次只得到了一个谜题。
Devise sign_up 对应于 devise_token_auth 注册控制器,Devise sign_in 对应于 devise_token_auth 会话控制器。因此,在使用此 gem 时,自定义 Devise sign_in 和 sign_up 方法需要自定义这两个 devise_token_auth 控制器。
根据您需要完成的任务,有两种方法可以解决此问题。
方法一
如果您想完全自定义控制器中的方法,请在此处按照文档覆盖 devise_token_auth 控制器方法:https://github.com/lynndylanhurley/devise_token_auth#custom-controller-overrides
这就是我所做的并且工作正常:
#config/routes.rb
...
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
sessions: 'overrides/sessions',
registrations: 'overrides/registrations'
}
...
如果您的本地控制器覆盖中存在方法,这会将所有 devise_token_auth 会话和注册路由到控制器的本地版本。如果该方法在您的本地覆盖中不存在,那么它将 运行 来自 gem 的方法。您基本上必须将控制器从 gem 复制到 'app/controllers/overrides' 并对您需要自定义的任何方法进行任何更改。从您未自定义的本地副本中删除方法。您也可以通过这种方式添加回调。如果要修改响应,请在方法末尾自定义渲染,通过 active_model_serializer.
将响应 return 作为 json这是我的会话控制器的示例,它添加了几个自定义 before_actions 来添加自定义功能:
#app/controllers/overrides/sessions_controller.rb
module Overrides
class SessionsController < DeviseTokenAuth::SessionsController
skip_before_action :authenticate_user_with_filter
before_action :set_country_by_ip, :only => [:create]
before_action :create_facebook_user, :only => [:create]
def create
# Check
field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first
@resource = nil
if field
q_value = resource_params[field]
if resource_class.case_insensitive_keys.include?(field)
q_value.downcase!
end
#q = "#{field.to_s} = ? AND provider='email'"
q = "#{field.to_s} = ? AND provider='#{params[:provider]}'"
#if ActiveRecord::Base.connection.adapter_name.downcase.starts_with? 'mysql'
# q = "BINARY " + q
#end
@resource = resource_class.where(q, q_value).first
end
#sign in will be successful if @resource exists (matching user was found) and is a facebook login OR (email login and password matches)
if @resource and (params[:provider] == 'facebook' || (valid_params?(field, q_value) and @resource.valid_password?(resource_params[:password]) and (!@resource.respond_to?(:active_for_authentication?) or @resource.active_for_authentication?)))
# create client id
@client_id = SecureRandom.urlsafe_base64(nil, false)
@token = SecureRandom.urlsafe_base64(nil, false)
@resource.tokens[@client_id] = { token: BCrypt::Password.create(@token), expiry: (Time.now + DeviseTokenAuth.token_lifespan).to_i }
@resource.save
sign_in(:user, @resource, store: false, bypass: false)
yield @resource if block_given?
#render_create_success
render json: { data: resource_data(resource_json: @resource.token_validation_response) }
elsif @resource and not (!@resource.respond_to?(:active_for_authentication?) or @resource.active_for_authentication?)
render_create_error_not_confirmed
else
render_create_error_bad_credentials
end
end
def set_country_by_ip
if !params['fb_code'].blank?
if !params['user_ip'].blank?
#checks if IP sent is valid, otherwise raise an error
raise 'Invalid IP' unless (params['user_ip'] =~ Resolv::IPv4::Regex ? true : false)
country_code = Custom::FacesLibrary.get_country_by_ip(params['user_ip'])
country_id = Country.find_by(country_code: country_code)
if country_id
params.merge!(country_id: country_id.id, country_name: country_id.name, test: 'Test')
I18n.locale = country_id.language_code
else
params.merge!(country_id: 1, country_name: 'International')
end
else
params.merge!(country_id: 1, country_name: 'International')
end
end
end
def create_facebook_user
if !params['fb_code'].blank?
# TODO capture errors for invalid, expired or already used codes to return beter errors in API
user_info, access_token = Omniauth::Facebook.authenticate(params['fb_code'])
if user_info['email'].blank?
Omniauth::Facebook.deauthorize(access_token)
end
#if Facebook user does not exist create it
@user = User.find_by('uid = ? and provider = ?', user_info['id'], 'facebook')
if !@user
@graph = Koala::Facebook::API.new(access_token, ENV['FACEBOOK_APP_SECRET'])
Koala.config.api_version = "v2.6"
new_user_picture = @graph.get_picture_data(user_info['id'], type: :normal)
new_user_info = {
uid: user_info['id'],
provider: 'facebook',
email: user_info['email'],
name: user_info['name'],
first_name: user_info['first_name'],
last_name: user_info['last_name'],
image: new_user_picture['data']['url'],
gender: user_info['gender'],
fb_auth_token: access_token,
friend_count: user_info['friends']['summary']['total_count'],
friends: user_info['friends']['data']
}
@user = User.new(new_user_info)
@user.password = Devise.friendly_token.first(8)
@user.country_id = params['country_id']
@user.country_name = params['country_name']
if !@user.save
render json: @user.errors, status: :unprocessable_entity
end
end
#regardless of user creation, merge facebook parameters for proper sign_in in standard action
params.merge!(provider: 'facebook', email: @user.email)
else
params.merge!(provider: 'email')
end
end
end
end
注意在回调中使用 params.merge!
将自定义参数添加到主控制器方法。这是一个漂亮的技巧,不幸的是将在 Rails 5.1 中被弃用,因为参数将不再继承自散列。
方法#2
如果您只想向自定义控制器中的方法添加功能,您可以通过子类化控制器、从原始控制器继承并将块传递给 super,如下所述:
https://github.com/lynndylanhurley/devise_token_auth#passing-blocks-to-controllers
我已经对自定义注册控制器中的创建方法执行了此操作。
按照方法#1 修改路线
#config/routes.rb
...
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
sessions: 'overrides/sessions',
registrations: 'overrides/registrations'
}
...
并在自定义控制器中自定义create
方法:
#app/controllers/overrides/registrations_controller.rb
module Overrides
class RegistrationsController < DeviseTokenAuth::RegistrationsController
skip_before_action :authenticate_user_with_filter
#will run upon creating a new registration and will set the country_id and locale parameters
#based on whether or not a user_ip param is sent with the request
#will default to country_id=1 and locale='en' (International) if it's not sent.
before_action :set_country_and_locale_by_ip, :only => [:create]
def set_country_and_locale_by_ip
if !params['user_ip'].blank?
#checks if IP sent is valid, otherwise raise an error
raise 'Invalid IP' unless (params['user_ip'] =~ Resolv::IPv4::Regex ? true : false)
country_code = Custom::FacesLibrary.get_country_by_ip(params['user_ip'])
#TODO check if there's an internet connection here or inside the library function
#params.merge!(country_id: 1, country_name: 'International', locale: 'en')
country_id = Country.find_by(country_code: country_code)
if country_id
params.merge!(country_id: country_id.id, locale: country_id.language_code, country_name: country_id.name)
else
params.merge!(country_id: 1, country_name: 'International', locale: 'en')
end
else
params.merge!(country_id: 1, country_name: 'International', locale: 'en')
end
end
#this will add behaviour to the registrations controller create method
def create
super do |resource|
create_assets(@resource)
end
end
def create_assets(user)
begin
Asset.create(user_id: user.id, name: "stars", qty: 50)
Asset.create(user_id: user.id, name: "lives", qty: 5)
Asset.create(user_id: user.id, name: "trophies", qty: 0)
end
end
end
end
对于特定的序列化程序问题,我是这样做的:
overrides/sessions_controller.rb
module Api
module V1
module Overrides
class SessionsController < ::DeviseTokenAuth::SessionsController
# override this method to customise how the resource is rendered. in this case an ActiveModelSerializers 0.10 serializer.
def render_create_success
render json: { data: ActiveModelSerializers::SerializableResource.new(@resource).as_json }
end
end
end
end
end
config/routes.rb
namespace :api, defaults: {format: 'json'} do
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
sessions: 'api/v1/overrides/sessions'
}
# snip the rest