Rails 3 disabling session cookies
I have RESTful API written on RoR 3. I have to make my application not to send "Set-Cookie header" (clients are authorizing using auth_token parameter).
I have tried to use session :off
and reset_session
but it does not m开发者_开发技巧ake any sense.
I am using devise
as authentication framework.
Here is my ApplicationController
class ApplicationController < ActionController::Base
before_filter :reset_session #, :unless => :session_required?
session :off #, :unless => :session_required?
skip_before_filter :verify_authenticity_token
before_filter :access_control_headers!
def options
render :text => ""
end
private
def access_control_headers!
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
response.headers["Access-Control-Allow-Credentials"] = "true"
response.headers["Access-Control-Allow-Headers"] = "Content-type"
end
def session_required?
!(params[:format] == 'xml' or params[:format] == 'json')
end
end
Use the built in option.
env['rack.session.options'][:skip] = true
or the equivalent
request.session_options[:skip] = true
You can find the documentation for it here https://github.com/rack/rack/blob/master/lib/rack/session/abstract/id.rb#L213
As is mentioned in a comment on John's answer, clearing the session will not prevent the session cookie from being sent. If you wish to totally remove the cookie from being sent, you have to use Rack middleware.
class CookieFilter
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
# use only one of the next two lines
# this will remove ALL cookies from the response
headers.delete 'Set-Cookie'
# this will remove just your session cookie
Rack::Utils.delete_cookie_header!(headers, '_app-name_session')
[status, headers, body]
end
end
Use it by creating an initializer with the following body:
Rails.application.config.middleware.insert_before ::ActionDispatch::Cookies, ::CookieFilter
To prevent the cookie filter to end up in application stack traces, which can be utterly confusing at times, you may want to silence it in the backtrace (Assuming you put it in lib/cookie_filter.rb
):
Rails.backtrace_cleaner.add_silencer { |line| line.start_with? "lib/cookie_filter.rb" }
I'm not sure when they added it to Devise, but there appears to be a configuration that will let you disable the sending of the session cookie when using a auth_token:
# By default Devise will store the user in session. You can skip storage for
# :http_auth and :token_auth by adding those symbols to the array below.
# Notice that if you are skipping storage for all authentication paths, you
# may want to disable generating routes to Devise's sessions controller by
# passing :skip => :sessions to `devise_for` in your config/routes.rb
config.skip_session_storage = [:http_auth, :token_auth]
It does work well. The only issue I had was that I still needed to be able to make an initial request to my token_controller in order to generate/retrieve the token. I.e. POST /api/v1/tokens.json
, which unfortunately would cause a session cookie to be returned for that request.
So I ended up implementing the CookieFilter
intializer that Ryan Ahearn wrote above anyway.
Also, since my app has both a web front-end as well as a JSON api, I only wanted to filter the cookies for the JSON api. So I modified the CookieFilter
class to first check the requests belonged to the api:
if env['PATH_INFO'].match(/^\/api/)
Rack::Utils.delete_cookie_header!(headers, '_myapp_session')
end
Not sure if there's a better way of doing that...
Another solution: In the controller you want to avoid cookies, add this:
after_filter :skip_set_cookies_header
def skip_set_cookies_header
request.session_options = {}
end
If you have a set of api controllers, set this in an api_controller class and let your other controllers inherit the api_controller.
This skips setting Set-Cookie header since the session opts is empty.
The default CookieSessionStore doesn't send a "Set-Cookie" header unless something is added to the session. Is something in your stack writing to the session? (it's probably Devise)
session :off
has been deprecated:
def session(*args)
ActiveSupport::Deprecation.warn(
"Disabling sessions for a single controller has been deprecated. " +
"Sessions are now lazy loaded. So if you don't access them, " +
"consider them off. You can still modify the session cookie " +
"options with request.session_options.", caller)
end
If something in your stack is setting session info, you can clear it using session.clear
like so:
after_filter :clear_session
def clear_session
session.clear
end
Which will prevent the Set-Cookie header from being sent
Further to John's answer, if you are using CSRF protection you would need to turn that off for web service requests. You can add the following as a protected method in your application controller:
def protect_against_forgery?
unless request.format.xml? or request.format.json?
super
end
end
This way HTML requests still use CSRF (or not - depends on config.action_controller.allow_forgery_protection = true/false
in the environment).
I myself truly missed being able to declaratively turn off sessions (using session :off
)
... thus I brought it "back" - use it just like in plain-old-rails (<= 2.2) :
than of course this might require some additional Devise specific hacking of your own, since session_off might cause session == nil
in a controller, and most rails extensions since 2.3 simply assume a lazy session that shall not be nil ever.
https://github.com/kares/session_off
Imo the best approach is to simply remove the cookie session store middleware.
To do so, add this to your application.rb (or to a specific environment if needed):
# No session store
config.middleware.delete ActionDispatch::Session::CookieStore
Try this instead
after_filter :skip_set_cookies_header
def skip_set_cookies_header
session.instance_variable_set('@loaded', false)
end
Or even better, always remove Set-Cookie header when session data did not change
before_filter :session_as_comparable_array # first before_filter
after_filter :skip_set_cookies_header # last after_filter
def session_as_comparable_array(obj = session)
@session_as_comparable_array = case obj
when Hash
obj.keys.sort_by(&:to_s).collect{ |k| [k, session_as_comparable_array(obj[k])] }
when Array
obj.sort_by(&:to_s).collect{ |k| session_as_comparable_array(k) }
else
obj
end
end
def skip_set_cookies_header
session.instance_variable_set('@loaded', false) if (@session_as_comparable_array == session_as_comparable_array)
end
# frozen_string_literal: true
module Api
module Web
module Base
class WebApiApplicationController < ApplicationController
include DeviseTokenAuth::Concerns::SetUserByToken
include Api::Concerns::ErrorsConcern
devise_token_auth_group :user, contains: %i[api_web_v1_user]
respond_to :json
serialization_scope :current_user
before_action :METHOD_NAME
private
def METHOD_NAME
request.session_options[:skip] = true
end
end
end
end
end
It's working for me.
精彩评论