我在API模式下使用rails,包括Devise and Devise JWT(用于API)和ActiveAdmin。我一切正常,但我一直在构建 API 控制器,现在 ActiveAdmin 身份验证已损坏,我无法弄清楚发生了什么。
所以我试着直接去/admin/login
,它有效。我输入了用户名和密码,单击登录时,出现以下错误:
NoMethodError in ActiveAdmin::Devise::SessionsController#create
private method `redirect_to' called for #<ActiveAdmin::Devise::SessionsController:0x0000000001d420>
我不太确定为什么这会被破坏,因为它主要使用默认设置。
我的路由文件:
Rails.application.routes.draw do
devise_for :admin_users, ActiveAdmin::Devise.config
ActiveAdmin.routes(self)
...
我没有在ActiveAdmin::Devise
中更改任何内容,甚至我的代码库中都没有显示文件。
在我的设计配置中:
config.authentication_method = :authenticate_admin_user!
config.current_user_method = :current_admin_user
我的非活动管理员会话控制器如下所示:
# frozen_string_literal: true
module Users
class SessionsController < Devise::SessionsController
respond_to :json
private
def respond_with(resource, _opts = {})
render json: {
status: { code: 200, message: 'Logged in sucessfully.' },
data: UserSerializer.new(resource).serializable_hash
}, status: :ok
end
def respond_to_on_destroy
if current_user
render json: {
status: 200,
message: 'logged out successfully'
}, status: :ok
else
render json: {
status: 401,
message: 'Couldn't find an active session.'
}, status: :unauthorized
end
end
end
end
这是我的管理员用户模型:
# frozen_string_literal: true
class AdminUser < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable,
:recoverable, :rememberable, :validatable
end
我不相信当我忽略重定向错误时登录实际上有效。我尝试转到任何页面,并收到相同的消息You need to sign in or sign up before continuing.
这是我的应用程序配置:
config.load_defaults 7.0
config.api_only = true
config.session_store :cookie_store, key: '_interslice_session'
# Required for all session management (regardless of session_store)
config.middleware.use ActionDispatch::Cookies
config.middleware.use Rack::MethodOverride
config.middleware.use ActionDispatch::Flash
config.middleware.use ActionDispatch::Session::CookieStore
config.middleware.use config.session_store, config.session_options
我做错了什么?
更新的代码:
class ApplicationController < ActionController::API
# https://github.com/rails/rails/blob/v7.0.2.3/actionpack/lib/action_controller/api.rb#L104
# skip modules that we need to load last
ActionController::API.without_modules(:Instrumentation, :ParamsWrapper).each do |m|
include m
end
# include what's missing
include ActionController::ImplicitRender
include ActionController::Helpers
include ActionView::Layouts
include ActionController::Flash
include ActionController::MimeResponds
# include modules that have to be last
include ActionController::Instrumentation
include ActionController::ParamsWrapper
ActiveSupport.run_load_hooks(:action_controller_api, self)
ActiveSupport.run_load_hooks(:action_controller, self)
respond_to :json, :html
def redirect_to(options = {}, response_options = {})
super
end
module Users
class SessionsController < Devise::SessionsController
respond_to :html
Rails.application.routes.draw do
devise_for :admin_users, ActiveAdmin::Devise.config
ActiveAdmin.routes(self)
devise_for :users, defaults: { format: :json }, path: '', path_names: {
sign_in: 'login',
sign_out: 'logout',
registration: 'signup'
},
controllers: {
sessions: 'users/sessions',
registrations: 'users/registrations'
应用程序配置:
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0
config.api_only = true
config.session_store :cookie_store, key: '_interslice_session'
# Required for all session management (regardless of session_store)
config.middleware.use ActionDispatch::Cookies
config.middleware.use Rack::MethodOverride
config.middleware.use ActionDispatch::Flash
config.middleware.use ActionDispatch::Session::CookieStore
config.middleware.use config.session_store, config.session_options
这是我正在使用的设置,希望它是不言自明的,以便我们可以找到实际错误。
# Gemfile
# ...
gem "sprockets-rails"
gem "sassc-rails"
gem 'activeadmin'
gem 'devise'
gem 'devise-jwt'
# config/application.rb
require_relative "boot"
require "rails/all"
require "action_controller/railtie"
require "action_view/railtie"
require "sprockets/railtie"
Bundler.require(*Rails.groups)
module Rails7api
class Application < Rails::Application
config.load_defaults 7.0
config.api_only = true
config.session_store :cookie_store, key: '_interslice_session'
config.middleware.use ActionDispatch::Cookies
config.middleware.use Rack::MethodOverride
config.middleware.use ActionDispatch::Flash
config.middleware.use config.session_store, config.session_options
end
end
# config/routes.rb
Rails.application.routes.draw do
# Admin
devise_for :admin_users, ActiveAdmin::Devise.config
ActiveAdmin.routes(self)
# Api (api_users, name is just for clarity)
devise_for :api_users, defaults: { format: :json }
namespace :api, defaults: { format: :json } do
resources :users
end
end
# config/initializers/devise.rb
Devise.setup do |config|
# ...
config.jwt do |jwt|
# jwt.secret = ENV['DEVISE_JWT_SECRET_KEY']
jwt.secret = Rails.application.credentials.devise_jwt_secret_key!
end
end
# db/migrate/20220424045738_create_authentication.rb
class CreateAuthentication < ActiveRecord::Migration[7.0]
def change
create_table :admin_users do |t|
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
t.timestamps null: false
end
add_index :admin_users, :email, unique: true
create_table :api_users do |t|
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
t.timestamps null: false
end
add_index :api_users, :email, unique: true
create_table :jwt_denylist do |t|
t.string :jti, null: false
t.datetime :exp, null: false
end
add_index :jwt_denylist, :jti
end
end
# app/models/admin_user.rb
class AdminUser < ApplicationRecord
devise :database_authenticatable
end
# app/models/api_user.rb
class ApiUser < ApplicationRecord
devise :database_authenticatable, :jwt_authenticatable, jwt_revocation_strategy: JwtDenylist
self.skip_session_storage = [:http_auth, :params_auth] # https://github.com/waiting-for-dev/devise-jwt#session-storage-caveat
end
# app/models/jwt_denylist.rb
class JwtDenylist < ApplicationRecord
include Devise::JWT::RevocationStrategies::Denylist
self.table_name = 'jwt_denylist'
end
# app/application_controller.rb
class ApplicationController < ActionController::Base # for devise and active admin
respond_to :json, :html
end
# app/api/application_controller.rb
module Api
class ApplicationController < ActionController::API # for api
before_action :authenticate_api_user!
end
end
# app/api/users_controller.rb
module Api
class UsersController < ApplicationController
def index
render json: User.all
end
end
end
人们有几种不同的方式得到这个错误,但它们似乎是同一问题的变体。我只能找到一种私人redirect_to
方法,甚至在文档中
https://api.rubyonrails.org/classes/ActionController/Flash.html#method-i-redirect_to
active_admin
和devise
都继承自ApplicationController
# ActiveAdmin::Devise::SessionsController < Devise::SessionsController < DeviseController < Devise.parent_controller.constantize # <= @@parent_controller = "ApplicationController"
# ActiveAdmin::BaseController < ::InheritedResources::Base < ::ApplicationController
当ApplicationController
继承自ActionController::API
时,活动管理员由于缺少依赖项而中断。所以我们必须一个接一个地包含它们,直到导轨引导和控制器看起来像这样
class ApplicationController < ActionController::API
include ActionController::Helpers # FIXES undefined method `helper' for ActiveAdmin::Devise::SessionsController:Class (NoMethodError)
include ActionView::Layouts # FIXES undefined method `layout' for ActiveAdmin::Devise::SessionsController:Class (NoMethodError)
include ActionController::Flash # FIXES undefined method `flash' for #<ActiveAdmin::Devise::SessionsController:0x0000000000d840>):
respond_to :json, :html # FIXES ActionController::UnknownFormat (ActionController::UnknownFormat):
end
这在您尝试登录并收到错误private method 'redirect_to'
有效。一点点调试和回溯指向responders
gem,它使用 html 响应,这没关系,即使我们的控制器是 api 并调用redirect_to但命中Flash#redirect_to
而不是Redirecting#redirect_to
#0 ActionController::Flash#redirect_to(options="/admin", response_options_and_flash={}) at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/actionpack-7.0.2.3/lib/action_controller/metal/flash.rb:52
#1 ActionController::Responder#redirect_to at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/responders-3.0.1/lib/action_controller/responder.rb:147
#2 ActionController::Responder#navigation_behavior(error=#<ActionView::MissingTemplate: Missing t...) at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/responders-3.0.1/lib/action_controller/responder.rb:207
#3 ActionController::Responder#to_html at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/responders-3.0.1/lib/action_controller/responder.rb:174
#4 ActionController::Responder#to_html at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/responders-3.0.1/lib/action_controller/responder.rb:171
#5 ActionController::Responder#respond at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/responders-3.0.1/lib/action_controller/responder.rb:165
#6 ActionController::Responder.call(args=[#<ActiveAdmin::Devise::SessionsControll...) at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/responders-3.0.1/lib/action_controller/responder.rb:158
#7 ActionController::RespondWith#respond_with(resources=[#<AdminUser id: 1, email: "admin@user.c..., block=nil) at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/responders-3.0.1/lib/action_controller/respond_with.rb:213
#8 Devise::SessionsController#create at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/devise-4.8.1/app/controllers/devise/sessions_controller.rb:23
由于API
控制器更薄
https://github.com/rails/rails/blob/v7.0.2.3/actionpack/lib/action_controller/api.rb#L112
比Base
控制器
https://github.com/rails/rails/blob/v7.0.2.3/actionpack/lib/action_controller/base.rb#L205
看起来缺少了什么。因此,使用Base
控制器进行一些调试和回溯确实揭示了它之间的细微差异。
#0 ActionController::Flash#redirect_to(options="/admin", response_options_and_flash={}) at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/actionpack-7.0.2.3/lib/action_controller/metal/flash.rb:52
#1 block {|payload={:request=>#<ActionDispatch::Request POS...|} in redirect_to at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/actionpack-7.0.2.3/lib/action_controller/metal/instrumentation.rb:42
#2 block in instrument at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/activesupport-7.0.2.3/lib/active_support/notifications.rb:206
#3 ActiveSupport::Notifications::Instrumenter#instrument(name="redirect_to.action_controller", payload={:request=>#<ActionDispatch::Request POS...) at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/activesupport-7.0.2.3/lib/active_support/notifications/instrumenter.rb:24
#4 ActiveSupport::Notifications.instrument(name="redirect_to.action_controller", payload={:request=>#<ActionDispatch::Request POS...) at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/activesupport-7.0.2.3/lib/active_support/notifications.rb:206
#5 ActionController::Instrumentation#redirect_to at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/actionpack-7.0.2.3/lib/action_controller/metal/instrumentation.rb:41
#6 ActionController::Responder#redirect_to at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/responders-3.0.1/lib/action_controller/responder.rb:147
#7 ActionController::Responder#navigation_behavior(error=#<ActionView::MissingTemplate: Missing t...) at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/responders-3.0.1/lib/action_controller/responder.rb:207
#8 ActionController::Responder#to_html at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/responders-3.0.1/lib/action_controller/responder.rb:174
#9 ActionController::Responder#to_html at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/responders-3.0.1/lib/action_controller/responder.rb:171
#10 ActionController::Responder#respond at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/responders-3.0.1/lib/action_controller/responder.rb:165
#11 ActionController::Responder.call(args=[#<ActiveAdmin::Devise::SessionsControll...) at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/responders-3.0.1/lib/action_controller/responder.rb:158
#12 ActionController::RespondWith#respond_with(resources=[#<AdminUser id: 1, email: "admin@user.c..., block=nil) at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/responders-3.0.1/lib/action_controller/respond_with.rb:213
#13 Devise::SessionsController#create at ~/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/devise-4.8.1/app/controllers/devise/sessions_controller.rb:23
我想我们本来是要先打Instrumentation#redirect_to
的。需要注意的是,Instrumentation
需要比其他模块晚加载。在控制器Base
中Flash
模块位于Instrumentation
之前。但我们最后包括Flash
把事情搞砸了。我不知道是否有更好的方法来更改这些模块的顺序:
class ApplicationController < ActionController::Metal
# https://github.com/rails/rails/blob/v7.0.2.3/actionpack/lib/action_controller/api.rb#L104
# skip modules that we need to load last
ActionController::API.without_modules(:Instrumentation, :ParamsWrapper).each do |m|
include m
end
# include what's missing
include ActionController::ImplicitRender
include ActionController::Helpers
include ActionView::Layouts
include ActionController::Flash
include ActionController::MimeResponds
# include modules that have to be last
include ActionController::Instrumentation
include ActionController::ParamsWrapper
ActiveSupport.run_load_hooks(:action_controller_api, self)
ActiveSupport.run_load_hooks(:action_controller, self)
respond_to :json, :html
end
它修复了错误。但是我觉得ApplicationController
应该继承Base
,它使事情变得更加简单,因为它是由设计和活动管理员使用的,使用API
并为活动管理员添加模块似乎在兜圈子。
@brcebn解决方法确实有效。就像所有很酷的孩子一样,进入私人方法。 https://github.com/heartcombo/responders/issues/222#issue-661963658
def redirect_to(options = {}, response_options = {})
super
end
这也有点毛茸茸的,所以我不得不写一些测试。这些仅在ApplicationController
从Base
继承时才有效。
# spec/requests/authentication_spec.rb
require 'rails_helper'
RSpec.describe 'Authentication', type: :request do
describe 'Edge case for Devise + JWT + RailsAPI + ActiveAdmin configuration' do
# This set up will raise private method error
#
# class ApplicationController < ActionController::API
# include ActionController::Helpers
# include ActionView::Layouts
# include ActionController::Flash # <= has private.respond_to
#
# respond_to :json, :html # when responding with html in an api controller
# end
#
before { AdminUser.create!(params) }
let(:params) { { email: 'admin@user.com', password: '123456' } }
it do
RSpec::Expectations.configuration.on_potential_false_positives = :nothing
expect{
post(admin_user_session_path, params: { admin_user: params })
}.to_not raise_error(NoMethodError)
end
it do
expect{
post(admin_user_session_path, params: { admin_user: params })
}.to_not raise_error
end
end
describe 'POST /api/users/sign_in' do
before { ApiUser.create!(params) }
before { post api_user_session_path, params: { api_user: params } }
let(:params) { { email: 'api@user.com', password: '123456' } }
it { expect(response).to have_http_status(:created) }
it { expect(headers['Authorization']).to include 'Bearer' }
it 'should not have admin access' do
get admin_dashboard_path
expect(response).to have_http_status(:redirect)
follow_redirect!
expect(request.path).to eq '/admin/login'
end
end
describe 'GET /api/users' do
context 'when signed out' do
before { get api_users_path }
it { expect(response.body).to include 'You need to sign in or sign up before continuing.' }
end
context 'when signed in' do
before { ApiUser.create!(params) }
before { post api_user_session_path, params: { api_user: params } }
let(:params) { { email: 'api@user.com', password: '123456' } }
it 'should not authorize without Authorization header' do
get api_users_path
expect(response.body).to include 'You need to sign in or sign up before continuing.'
end
it 'should authorize with Authorization header' do
get api_users_path, headers: { 'Authorization': headers['Authorization'] }
expect(response.body).to_not include 'You need to sign in or sign up before continuing.'
end
end
end
describe 'GET /admin' do
it do
get admin_root_path
expect(response).to have_http_status(:redirect)
end
context 'when api_user is authorized' do
before { ApiUser.create!(params) }
before { post api_user_session_path, params: { api_user: params } }
let(:params) { { email: 'api@user.com', password: '123456' } }
it 'should redirect without raising' do
get admin_root_path
expect(response).to have_http_status(:redirect)
end
end
end
describe 'POST /admin/login' do
before { AdminUser.create!(params) }
before { post admin_user_session_path, params: { admin_user: params } }
let(:params) { { email: 'admin@user.com', password: '123456' } }
it do
expect(response).to have_http_status(:redirect)
follow_redirect!
expect(response.body).to include 'Signed in successfully.'
end
end
describe 'DELETE /admin/logout' do
before { AdminUser.create!(params) }
before { post admin_user_session_path, params: { admin_user: params } }
let(:params) { { email: 'admin@user.com', password: '123456' } }
it 'should sign out' do
delete destroy_admin_user_session_path
expect(response).to have_http_status(:redirect)
follow_redirect!
expect(request.path).to eq '/unauthenticated' # <= what?
follow_redirect!
expect(response.body).to include 'Signed out successfully.'
expect(request.path).to eq '/admin/login'
end
end
end
$ rspec spec/requests/authentication_spec.rb
...........
Finished in 0.48745 seconds (files took 0.83 seconds to load)
11 examples, 0 failures
更新
上述带有ActionController::API.without_modules
的解决方案似乎是超级错误或不是正确的方法,或者ActiveSupport
钩子不应该在ApplicationController
内运行。
我发现的唯一另一种方法是定义完整的自定义控制器并从中继承。继承部分似乎很重要(如果您知道原因,请发表评论)。
# app/controllers/base_controller.rb
class BaseController < ActionController::Metal
abstract!
# Order of modules is important
# See: https://github.com/rails/rails/blob/v7.0.2.3/actionpack/lib/action_controller/base.rb#L205
MODULES = [
AbstractController::Rendering,
# Extra modules #################
ActionController::Helpers,
ActionView::Layouts,
ActionController::MimeResponds,
ActionController::Flash,
#################################
ActionController::UrlFor,
ActionController::Redirecting,
ActionController::ApiRendering,
ActionController::Renderers::All,
ActionController::ConditionalGet,
ActionController::BasicImplicitRender,
ActionController::StrongParameters,
ActionController::DataStreaming,
ActionController::DefaultHeaders,
ActionController::Logging,
# Before callbacks should also be executed as early as possible, so
# also include them at the bottom.
AbstractController::Callbacks,
# Append rescue at the bottom to wrap as much as possible.
ActionController::Rescue,
# Add instrumentations hooks at the bottom, to ensure they instrument
# all the methods properly.
ActionController::Instrumentation,
# Params wrapper should come before instrumentation so they are
# properly showed in logs
ActionController::ParamsWrapper
]
MODULES.each do |mod|
include mod
end
ActiveSupport.run_load_hooks(:action_controller_api, self)
ActiveSupport.run_load_hooks(:action_controller, self)
end
# app/application_controller.rb
class ApplicationController < BaseController # use for everything
respond_to :json, :html
end
# app/api/users_controller.rb
module Api
class UsersController < ApplicationController
before_action :authenticate_api_user!
def index
render json: User.all
end
end
end
测试!
12 examples, 0 failures
基于您的错误消息。似乎根没有为active_admin设置。
登录用户、确认帐户或更新密码后,Devise 将查找要重定向到的范围根路径。例如,使用:user
资源时,将使用user_root_path
(如果存在);否则,将使用默认root_path
。这意味着您需要在路由中设置根:
root to: 'home#index'
您还可以覆盖after_sign_in_path_for
和after_sign_out_path_for
以自定义重定向挂钩。
从这里获取参考。
我也有类似的问题。就我而言,它与宝石responders
有关。这是我关于它的问题,仍未解决。