gpt4 book ai didi

ruby-on-rails - Active Admin 登录不起作用(Devise + ActiveAdmin + Devise JWT)

转载 作者:行者123 更新时间:2023-12-05 03:25:52 29 4
gpt4 key购买 nike

我在 API 模式下使用 Rails,使用 Devise 和 Devise JWT(用于 API)和 ActiveAdmin。我一切正常,但我一直在构建 API Controller ,现在 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 中更改任何内容,我什至没有在我的代码库中显示这些文件。

在我的 Devise 配置中:

config.authentication_method = :authenticate_admin_user!
config.current_user_method = :current_admin_user

我的非 activeadmin session Controller 看起来像:

# 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 continue.

这是我的应用程序配置:

    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

人们有几种不同的方式得到这个错误,但它们似乎是同一个问题的变体。我只能找到一种私有(private) 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 时,事件管理因缺少依赖项而中断。所以我们必须一一包含它们,直到 rails boots 和 controller 看起来像这样

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 响应,这没问题,即使我们的 Controller 是 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 Controller 相当纤薄

https://github.com/rails/rails/blob/v7.0.2.3/actionpack/lib/action_controller/api.rb#L112

Base Controller

https://github.com/rails/rails/blob/v7.0.2.3/actionpack/lib/action_controller/base.rb#L205

好像少了什么。因此,使用 Base Controller 进行一些调试和回溯确实揭示了一个微小的差异。

  #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 Controller 中,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 解决方法确实有效。就像所有酷 child 一样,使用那种私有(private)方法。 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 Hook 不应该在内部运行应用程序 Controller

我发现的唯一其他方法是定义完整的自定义 Controller 并从中继承。继承部分似乎很重要(如果您知道为什么,请发表评论)。

# 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

关于ruby-on-rails - Active Admin 登录不起作用(Devise + ActiveAdmin + Devise JWT),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/71904776/

29 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com