Rails禁用Cookie时无法验证CSRF Token,如何启用protect_from_forgery?
你遇到的这个问题很典型——Rails默认的protect_from_forgery机制是依赖Cookie来存储参照CSRF Token的。当浏览器禁用Cookie时,Rails没法从Cookie里拿到_csrf_token这个参照值,哪怕你表单里已经提交了authenticity_token参数,系统也找不到可比对的基准,自然就抛出422错误了。
下面给你几个可行的解决方案,既能保留CSRF保护,又能适配无Cookie的场景:
方案1:自定义CSRF验证逻辑,脱离Cookie依赖
我们可以重写控制器的verified_request?方法,让Rails从请求参数或会话(前提是会话不依赖Cookie)里获取参照Token,替代默认的Cookie读取逻辑。
比如,如果你已经配置了无Cookie的会话存储(比如Redis缓存存储),可以这么改:
class ApplicationController < ActionController::Base protect_from_forgery with: :exception protected def verified_request? # 优先用默认逻辑,不行就比对请求参数里的Token和会话中存储的Token super || valid_authenticity_token?(session, params[:authenticity_token]) end end
配套的无Cookie会话配置
要让会话不依赖Cookie,你需要修改config/initializers/session_store.rb:
# 用Redis存储会话,不依赖Cookie Rails.application.config.session_store :redis_store, servers: 'redis://localhost:6379/0/session', expire_after: 24.hours, secure: true # 一定要配合HTTPS使用
这种方式需要在生成链接时把session_id作为参数带上(比如link_to "登录", login_path(session_id: session.id)),注意必须用HTTPS防止session id泄露。
方案2:用HTTP头传递CSRF Token,调整验证逻辑
如果是AJAX请求为主的场景,可以把CSRF Token放在页面的meta标签里,请求时通过X-CSRF-Token头传递,同时修改验证逻辑支持从请求头读取Token:
class ApplicationController < ActionController::Base protect_from_forgery with: :exception protected def verified_request? # 支持从Cookie、请求头、参数三个位置比对Token super || valid_authenticity_token?(session, request.headers['X-CSRF-Token']) || valid_authenticity_token?(session, params[:authenticity_token]) end end
前端页面里需要渲染meta标签:
<meta name="csrf-token" content="<%= form_authenticity_token %>">
AJAX请求时带上头:
const csrfToken = document.querySelector('meta[name="csrf-token"]').content; fetch('/login', { method: 'POST', headers: { 'X-CSRF-Token': csrfToken, 'Content-Type': 'application/json' }, body: JSON.stringify({...}) });
方案3:基于JWT的无Cookie认证+CSRF保护
如果你的应用可以用JWT(JSON Web Token)做身份认证,完全可以把CSRF Token嵌入到JWT中,彻底脱离Cookie:
- 登录成功时,生成JWT并把CSRF Token作为payload的一部分返回给前端;
- 前端后续请求把JWT放在
Authorization头,CSRF Token放在X-CSRF-Token头; - 控制器验证JWT的有效性,同时比对JWT中的CSRF Token和请求头的Token。
示例代码:
class ApplicationController < ActionController::Base # 对JSON请求跳过默认的Cookie依赖验证,改用自定义逻辑 protect_from_forgery with: :exception, unless: -> { request.format.json? } before_action :validate_csrf_token, if: -> { request.format.json? } private def validate_csrf_token auth_header = request.headers['Authorization'] return render_error unless auth_header.present? jwt_token = auth_header.split(' ').last decoded = JWT.decode(jwt_token, Rails.application.credentials.secret_key_base)[0] unless decoded['csrf_token'] == request.headers['X-CSRF-Token'] render json: { error: 'Invalid CSRF Token' }, status: :unprocessable_entity end end end
重要提醒
- 无论用哪种方案,都绝对不能跳过CSRF验证,这会让你的应用面临严重的跨站请求伪造攻击;
- 无Cookie场景下的Token传递一定要配合HTTPS使用,防止Token被中间人窃取;
- 尽量让CSRF Token和用户会话绑定,避免攻击者复用Token发起攻击。
内容的提问来源于stack exchange,提问作者Ramon Marques

