Rails应用本地登录未下发session ID 外部访问可正常登录故障求助
我未修改任何代码,此前正常运行的登录功能突然失效:输入正确的邮箱和密码后仍会跳转回登录页,我没有使用devise组件。
一开始我怀疑是代码问题,但日志显示邮箱密码验证通过,服务端正常发起首页跳转请求。
之后我用本地代理工具Burp Suite追踪浏览器和服务端间的session ID等数据,发现登录成功后服务端未下发session ID,触发authenticate_user校验拦截导致登录失败,希望定位原因并解决。
我还使用ngrok工具暴露本地服务后,外部环境的用户可以正常登录,Burp Suite也显示该场景下session ID下发正常。
由此确认仅本地环境登录时不会下发session ID,近期我未编辑代码,仅执行了以下操作:
- 安装Git
- 通过Git将项目"app1"推送到GitHub
- 安装SourceTree并绑定GitHub账号
- 通过SourceTree从GitHub克隆"app1",重命名为"app2"
执行上述操作前我在application.rb中添加了如下配置,即便注释该配置问题也未解决:
application.rb
config.session_store :cookie_store, expire_after: 8.hours
所有问题均在执行上述操作后出现。
相关代码
routes.rb
get "login" => "users#login_form" post "login" => "users#login" post "logout" => "users#logout"
application_controller.rb
class ApplicationController < ActionController::Base protect_from_forgery with: :null_session before_action :set_current_user skip_before_action :verify_authenticity_token caches_action :set_current_user, :authenticate_user, :forbid_login_user def set_current_user @current_user = User.find_by(id: session[:user_id]) end def authenticate_user if @current_user == nil flash[:notice] = "You need to log in" redirect_to("/login") end end def forbid_login_user if @current_user flash[:notice] = "You are already logged in" redirect_to("/") end end end
users_controller.rb
before_action :authenticate_user, {except: [:new, :create, :login_form, :login]} before_action :forbid_login_user, {only: [:new, :create, :login_form, :login]} before_action :ensure_correct_user, {only: [:edit, :update]} before_action :ensure_correct_user_account, {only: [:setting_password, :update_password, :setting_email, :update_email, :delete_account, :destroy]} caches_action :index, :show, :follow, :new, :create, :edit, :user_params, :update, :destroy, :login_form, :login, :logout, :likes, :ensure_correct_user, :followings, :followers, :top def login @user = User.find_by(email: params[:email]) if @user && @user.authenticate(params[:password]) session[:user_id] = @user.id flash[:notice] = "You have successfully logged in" redirect_to("/") else @error_message = "Email address or password is incorrect" @email = params[:email] @password = params[:password] render("users/login_form.html.erb") end end def login_form end def logout session[:user_id] = nil flash[:notice] = "You have successfully logged out" redirect_to("/login") end
登录成功后默认跳转至站点首页"/"。
运行日志
以下是我尝试登录时的日志,可见Started GET "/"说明账号验证通过发起首页跳转,但后续日志提示Filter chain halted as :authenticate_user rendered or redirected,最终跳转回登录页:
Started POST "/login" for ::1 at 2021-08-24 14:44:31 +0900 Processing by UsersController#login as HTML Parameters: {"utf8"=>"✓", "authenticity_token"=>"JBcKtTsdCT0JXgyIfzjKVqi/8KEz4pRmUXB2Kybn8eHcnz7UKXoMsbBRkBCnoUqwnIdi4hUkZ/6oQKFVqMyG/g==", "email"=>"hoge", "password"=>"[FILTERED]"} User Load (35.1ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IS NULL LIMIT 1 ↳ app/controllers/application_controller.rb:9 User Load (0.6ms) SELECT `users`.* FROM `users` WHERE `users`.`email` = 'hoge' LIMIT 1 ↳ app/controllers/users_controller.rb:255 Redirected to http://localhost:3000/ Completed 302 Found in 422ms (ActiveRecord: 41.9ms) Started GET "/" for ::1 at 2021-08-24 14:44:32 +0900 Processing by UsersController#top as HTML User Load (0.6ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IS NULL LIMIT 1 ↳ app/controllers/application_controller.rb:9 Redirected to http://localhost:3000/login Filter chain halted as :authenticate_user rendered or redirected Completed 302 Found in 6ms (ActiveRecord: 0.6ms) Started GET "/login" for ::1 at 2021-08-24 14:44:32 +0900 Processing by UsersController#login_form as HTML User Load (0.5ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IS NULL LIMIT 1 ↳ app/controllers/application_controller.rb:9 Rendering users/login_form.html.erb within layouts/application Rendered users/login_form.html.erb within layouts/application (0.9ms) CACHE User Load (0.1ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IS NULL LIMIT 1 ↳ app/views/layouts/application.html.erb:51 Completed 200 OK in 244ms (Views: 233.9ms | ActiveRecord: 0.5ms)
Burp Suite抓包结果
使用ngrok暴露服务后外部用户登录时的抓包结果可见Cookie和Set-Cookie字段均正常存在:
本地环境登录时的抓包结果可见Cookie和Set-Cookie字段均不存在:
已尝试的解决方法
重启服务和电脑未解决问题,在Chrome、Firefox、Brave多个浏览器测试均复现该问题。
版本信息
- ruby 2.6.4p104
- RubyGems 3.0.3
- Rails 5.2.3
- Burp Suite Community Edition 2021.8.2
补充说明
以下操作均未解决问题:
- 执行
bundle exec rake tmp:cache:clear命令 - 执行
rails tmp:clear命令 - 在users_controller.rb的login_form方法中添加清理session逻辑如下:
def login_form reset_session session[:user_id] = nil session.delete(:user_id) end
恳请各位帮忙定位问题原因,感谢!
根因定位
问题出在代码错误使用caches_action缓存了与会话状态强相关的方法和接口:
application_controller.rb中对set_current_user、authenticate_user、forbid_login_user三个权限校验方法加了动作缓存,缓存生效后方法会直接复用历史结果,不会实时读取当前请求的session值,日志中WHERE users.id IS NULL就是缓存的set_current_user方法返回空用户的直接表现。users_controller.rb中把login、logout等会话操作接口也加入了缓存列表,登录成功后生成的Set-Cookie响应头会被缓存逻辑过滤,导致本地环境请求时服务端不下发session ID。- ngrok访问正常是因为Rails动作缓存的键包含请求域名,ngrok的域名与localhost不同,对应的缓存未被污染,所以功能正常。你之前操作Git、克隆项目的行为触发了本地缓存的生成,刚好命中了这个代码隐患。
修复步骤
- 移除错误的缓存配置:
- 删除
application_controller.rb中的caches_action :set_current_user, :authenticate_user, :forbid_login_user行,权限校验方法绝对不能缓存 - 清理
users_controller.rb中caches_action列表的:login、:logout、:login_form、:ensure_correct_user等和用户状态、权限相关的条目,动作缓存仅可用于无状态的公开页面
- 删除
- 执行全量缓存清理:
rails tmp:clear rails cache:clear - 重启Rails服务,本地登录即可恢复正常。
优化建议
你当前配置的protect_from_forgery with: :null_session和skip_before_action :verify_authenticity_token存在CSRF安全风险,如果不是纯API服务建议恢复默认的CSRF校验逻辑。
内容的提问来源于stack exchange,提问作者punpun36

