Active Admin 登录不起作用(Devise + ActiveAdmin + Devise JWT)



我在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_admindevise都继承自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'有效。一点点调试和回溯指向respondersgem,它使用 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需要比其他模块晚加载。在控制器BaseFlash模块位于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

这也有点毛茸茸的,所以我不得不写一些测试。这些仅在ApplicationControllerBase继承时才有效。

# 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_forafter_sign_out_path_for以自定义重定向挂钩。

从这里获取参考。

我也有类似的问题。就我而言,它与宝石responders有关。这是我关于它的问题,仍未解决。

最新更新