前提
- Rails 4.2.x
- Ruby 2.3
本題
RailsアプリでSSLを強制させたい場合、つまり HTTP -> HTTPS のリダイレクトをさせたい場合は、
force_ssl
を使うことができます。
class HogeController < ApplicationController
force_ssl only: :new
end
ここで、やんごとなき理由で HTTP -> HTTPS のリダイレクトが不要なアクションは必ずHTTPでアクセスさせたい、という要求があったとしましょう。
このときHTTPのページのみを行き来していれば何も問題はないのですが、一度HTTPSのページを経由すると、以降相対パスによる遷移はすべてHTTPSとなってしまいます。
つまり、明示的に HTTPS -> HTTP のリダイレクトを行わないといけません。
force_ssl
を使っているという前提で、どうすればよいかちょっと考えてみました。
普通に考えるとrequest.ssl? #=> true
のときにHTTPへリダイレクトする、という処理を書けば良いのですが、force_ssl
の対象となるアクションはその処理を実行したくないです(リダイレクトループになってしまうので)。
すぐに思いついたのはforce_ssl
が追加するCallbackの中身をCallbackChainから探して判定するという方法ですが、これはCallbackChainに入るときにはすでにlambdaに変換されていてあとからそのメソッド名とオプションを判定しづらい(できない?)のと、思わぬバグを仕込んでしまいそうで怖いので見送りました。
そこで、ひとつの実装として下記のようにしてみました。
class ApplicationController < ActionController::Base
include SslRedirectable
end
class HogeController < ApplicationController
force_ssl only: :new
end
module SslRedirectable
extend ActiveSupport::Concern
included do
before_action :force_non_ssl, if: :force_non_ssl?
class_attribute :ssl_actions
SSL_ACTIONS_ALL = [:all]
def self.force_ssl(options = {})
self.ssl_actions = options[:only] || SSL_ACTIONS_ALL
super
end
end
def force_non_ssl?
request.ssl? && request.get? && !ssl_required?
end
def ssl_required?
return false unless ssl_actions
ssl_actions.include?(action_name.to_sym) || ssl_actions == SSL_ACTIONS_ALL
end
def force_non_ssl
redirect_to non_ssl_url
flash.keep
end
def non_ssl_url
URI(request.original_url).tap { |u| u.scheme = 'http' }.to_s
end
end
force_ssl
を呼び出したときの対象のアクションをclass_attributeとして保持しておき、あとで当該アクションが呼ばれた時に参照する、というアプローチにしてみました。
force_ssl
をオーバーライドして拡張していますが、このくらいなら変更に弱くないだろうし、既存コードに手を加えずに実現できるメリットが大きいのでありかな、という感じです。
ここおかしくね?とか、もっとスマートな方法があるとか、ご意見あればぜひお願いします!