下一个Middleware是Cookie相关的,ActionDispatch::Cookies
,定义在actionpack-3.2.13/lib/action_dispatch/middleware/cookies.rb
中:
def initialize(app)
@app = app
end
def call(env)
cookie_jar = nil
status, headers, body = @app.call(env)
if cookie_jar = env['action_dispatch.cookies']
cookie_jar.write(headers)
if headers[HTTP_HEADER].respond_to?(:join)
headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
end
end
[status, headers, body]
end
这个Middleware负责裆CookieJar对象存在时的write
操作,以及当Set-Cookie
实现了join
方法时,调用该方法的功能。
其中env['action_dispatch.cookies']
在调用request.cookie_jar
的时候就已经被调用了:
module ActionDispatch
class Request
def cookie_jar
env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self)
end
end
end
这里的Cookies::CookieJar
封装了所有对于Cookie的基本操作。
然后是一个Middleware是与Session相关的,具体实现取决于config.session_store
的实现。这里以默认实现ActionDispatch::Session::CookieStore
为例,实现在actionpack-3.2.13/lib/action_dispatch/middleware/session/cookie_store.rb
中,需要注意的是,CookieStore Session的实现较长,而且还跨三个类的层次(还有两个Rails实现的模块),因此主要讲解这三层实现的不同功能:
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/object/blank'
require 'action_dispatch/middleware/session/abstract_store'
require 'rack/session/cookie'
module ActionDispatch
module Session
# This cookie-based session store is the Rails default. Sessions typically
# contain at most a user_id and flash message; both fit within the 4K cookie
# size limit. Cookie-based sessions are dramatically faster than the
# alternatives.
#
# If you have more than 4K of session data or don't want your data to be
# visible to the user, pick another session store.
#
# CookieOverflow is raised if you attempt to store more than 4K of data.
#
# A message digest is included with the cookie to ensure data integrity:
# a user cannot alter his +user_id+ without knowing the secret key
# included in the hash. New apps are generated with a pregenerated secret
# in config/environment.rb. Set your own for old apps you're upgrading.
#
# Session options:
#
# * <tt>:secret</tt>: An application-wide key string. It's important that
# the secret is not vulnerable to a dictionary attack. Therefore, you
# should choose a secret consisting of random numbers and letters and
# more than 30 characters.
#
# :secret => '449fe2e7daee471bffae2fd8dc02313d'
#
# * <tt>:digest</tt>: The message digest algorithm used to verify session
# integrity defaults to 'SHA1' but may be any digest provided by OpenSSL,
# such as 'MD5', 'RIPEMD160', 'SHA256', etc.
#
# To generate a secret key for an existing application, run
# "rake secret" and set the key in config/initializers/secret_token.rb.
#
# Note that changing digest or secret invalidates all existing sessions!
class CookieStore < Rack::Session::Cookie
include Compatibility
include StaleSessionCheck
private
def unpacked_cookie_data(env)
env["action_dispatch.request.unsigned_session_cookie"] ||= begin
stale_session_check! do
request = ActionDispatch::Request.new(env)
if data = request.cookie_jar.signed[@key]
data.stringify_keys!
end
data || {}
end
end
end
def set_session(env, sid, session_data, options)
session_data.merge!("session_id" => sid)
end
def set_cookie(env, session_id, cookie)
request = ActionDispatch::Request.new(env)
request.cookie_jar.signed[@key] = cookie
end
end
end
end
这层类是Rails实现的,负责在设置cookie时使用SignedCookieJar
而不是普通的CookieJar
以保障安全性。SignedCookieJar
会在Cookie set时增加验证功能,而在get时增加一个验证的token,实现在actionpack-3.2.13/lib/action_dispatch/middleware/cookies.rb
中:
class SignedCookieJar < CookieJar
MAX_COOKIE_SIZE = 4096 # Cookies can typically store 4096 bytes.
SECRET_MIN_LENGTH = 30 # Characters
def initialize(parent_jar, secret)
ensure_secret_secure(secret)
@parent_jar = parent_jar
@verifier = ActiveSupport::MessageVerifier.new(secret)
end
def [](name)
if signed_message = @parent_jar[name]
@verifier.verify(signed_message)
end
rescue ActiveSupport::MessageVerifier::InvalidSignature
nil
end
def []=(key, options)
if options.is_a?(Hash)
options.symbolize_keys!
options[:value] = @verifier.generate(options[:value])
else
options = { :value => @verifier.generate(options) }
end
raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
@parent_jar[key] = options
end
def method_missing(method, *arguments, &block)
@parent_jar.send(method, *arguments, &block)
end
protected
# To prevent users from using something insecure like "Password" we make sure that the
# secret they've provided is at least 30 characters in length.
def ensure_secret_secure(secret)
if secret.blank?
raise ArgumentError, "A secret is required to generate an " +
"integrity hash for cookie session data. Use " +
"config.secret_token = \"some secret phrase of at " +
"least #{SECRET_MIN_LENGTH} characters\"" +
"in config/initializers/secret_token.rb"
end
if secret.length < SECRET_MIN_LENGTH
raise ArgumentError, "Secret should be something secure, " +
"like \"#{SecureRandom.hex(16)}\". The value you " +
"provided, \"#{secret}\", is shorter than the minimum length " +
"of #{SECRET_MIN_LENGTH} characters"
end
end
end
其中验证和验证码生成均用到了ActiveSupport::MessageVerifier
实用类。这个类定义在activesupport-3.2.13/lib/active_support/message_verifier.rb
中:
require 'active_support/base64'
require 'active_support/deprecation'
require 'active_support/core_ext/object/blank'
module ActiveSupport
# +MessageVerifier+ makes it easy to generate and verify messages which are signed
# to prevent tampering.
#
# This is useful for cases like remember-me tokens and auto-unsubscribe links where the
# session store isn't suitable or available.
#
# Remember Me:
# cookies[:remember_me] = @verifier.generate([@user.id, 2.weeks.from_now])
#
# In the authentication filter:
#
# id, time = @verifier.verify(cookies[:remember_me])
# if time < Time.now
# self.current_user = User.find(id)
# end
#
# By default it uses Marshal to serialize the message. If you want to use another
# serialization method, you can set the serializer attribute to something that responds
# to dump and load, e.g.:
#
# @verifier.serializer = YAML
class MessageVerifier
class InvalidSignature < StandardError; end
def initialize(secret, options = {})
unless options.is_a?(Hash)
ActiveSupport::Deprecation.warn "The second parameter should be an options hash. Use :digest => 'algorithm' to specify the digest algorithm."
options = { :digest => options }
end
@secret = secret
@digest = options[:digest] || 'SHA1'
@serializer = options[:serializer] || Marshal
end
def verify(signed_message)
raise InvalidSignature if signed_message.blank?
data, digest = signed_message.split("--")
if data.present? && digest.present? && secure_compare(digest, generate_digest(data))
@serializer.load(::Base64.decode64(data))
else
raise InvalidSignature
end
end
def generate(value)
data = ::Base64.strict_encode64(@serializer.dump(value))
"#{data}--#{generate_digest(data)}"
end
private
# constant-time comparison algorithm to prevent timing attacks
def secure_compare(a, b)
return false unless a.bytesize == b.bytesize
l = a.unpack "C#{a.bytesize}"
res = 0
b.each_byte { |byte| res |= byte ^ l.shift }
res == 0
end
def generate_digest(data)
require 'openssl' unless defined?(OpenSSL)
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data)
end
end
end
可以看到用以验证的数据跟在数据的后面一起被写入到session中。
具体的Cookie的装载和设定功能由基类Rack::Session::Cookie
负责,实现在rack-1.4.5/lib/rack/session/cookie.rb
中:
require 'openssl'
require 'rack/request'
require 'rack/response'
require 'rack/session/abstract/id'
module Rack
module Session
# Rack::Session::Cookie provides simple cookie based session management.
# By default, the session is a Ruby Hash stored as base64 encoded marshalled
# data set to :key (default: rack.session). The object that encodes the
# session data is configurable and must respond to +encode+ and +decode+.
# Both methods must take a string and return a string.
#
# When the secret key is set, cookie data is checked for data integrity.
# The old secret key is also accepted and allows graceful secret rotation.
#
# Example:
#
# use Rack::Session::Cookie, :key => 'rack.session',
# :domain => 'foo.com',
# :path => '/',
# :expire_after => 2592000,
# :secret => 'change_me',
# :old_secret => 'also_change_me'
#
# All parameters are optional.
#
# Example of a cookie with no encoding:
#
# Rack::Session::Cookie.new(application, {
# :coder => Rack::Session::Cookie::Identity.new
# })
#
# Example of a cookie with custom encoding:
#
# Rack::Session::Cookie.new(application, {
# :coder => Class.new {
# def encode(str); str.reverse; end
# def decode(str); str.reverse; end
# }.new
# })
#
class Cookie < Abstract::ID
# Encode session cookies as Base64
class Base64
def encode(str)
[str].pack('m')
end
def decode(str)
str.unpack('m').first
end
# Encode session cookies as Marshaled Base64 data
class Marshal < Base64
def encode(str)
super(::Marshal.dump(str))
end
def decode(str)
::Marshal.load(super(str)) rescue nil
end
end
end
# Use no encoding for session cookies
class Identity
def encode(str); str; end
def decode(str); str; end
end
# Reverse string encoding. (trollface)
class Reverse
def encode(str); str.reverse; end
def decode(str); str.reverse; end
end
attr_reader :coder
def initialize(app, options={})
@secrets = options.values_at(:secret, :old_secret).compact
warn <<-MSG unless @secrets.size >= 1
SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
This poses a security threat. It is strongly recommended that you
provide a secret to prevent exploits that may be possible from crafted
cookies. This will not be supported in future versions of Rack, and
future versions will even invalidate your existing user cookies.
Called from: #{caller[0]}.
MSG
@coder = options[:coder] ||= Base64::Marshal.new
super(app, options.merge!(:cookie_only => true))
end
private
def load_session(env)
data = unpacked_cookie_data(env)
data = persistent_session_id!(data)
[data["session_id"], data]
end
def extract_session_id(env)
unpacked_cookie_data(env)["session_id"]
end
def unpacked_cookie_data(env)
env["rack.session.unpacked_cookie_data"] ||= begin
request = Rack::Request.new(env)
session_data = request.cookies[@key]
if @secrets.size > 0 && session_data
session_data, digest = session_data.split("--")
if session_data && digest
ok = @secrets.any? do |secret|
secret && Rack::Utils.secure_compare(digest, generate_hmac(session_data, secret))
end
end
session_data = nil unless ok
end
coder.decode(session_data) || {}
end
end
def persistent_session_id!(data, sid=nil)
data ||= {}
data["session_id"] ||= sid || generate_sid
data
end
# Overwrite set cookie to bypass content equality and always stream the cookie.
def set_cookie(env, headers, cookie)
Utils.set_cookie_header!(headers, @key, cookie)
end
def set_session(env, session_id, session, options)
session = session.merge("session_id" => session_id)
session_data = coder.encode(session)
if @secrets.first
session_data = "#{session_data}--#{generate_hmac(session_data, @secrets.first)}"
end
if session_data.size > (4096 - @key.size)
env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
nil
else
session_data
end
end
def destroy_session(env, session_id, options)
# Nothing to do here, data is in the client
generate_sid unless options[:drop]
end
def generate_hmac(data, secret)
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, data)
end
end
end
end
这层由Rack实现,Rack::Session::Cookie
基本实现了Cookie存储Session所需要的全部逻辑,包括各种编码方法,以及简单的SHA1验证。其中Rails在继承后并没有使用它的SHA1验证功能,这可能是因为Rails要求CookieStore
使用Rails自己实现的CookieJar
类,并且需要对付一些安全攻击。
至于Session底层机制的实现由它的基类Rack::Session::Abstract::ID
负责,其实现在rack-1.4.5/lib/rack/session/abstract/id.rb
中:
# ID sets up a basic framework for implementing an id based sessioning
# service. Cookies sent to the client for maintaining sessions will only
# contain an id reference. Only #get_session and #set_session are
# required to be overwritten.
#
# All parameters are optional.
# * :key determines the name of the cookie, by default it is
# 'rack.session'
# * :path, :domain, :expire_after, :secure, and :httponly set the related
# cookie options as by Rack::Response#add_cookie
# * :skip will not a set a cookie in the response nor update the session state
# * :defer will not set a cookie in the response but still update the session
# state if it is used with a backend
# * :renew (implementation dependent) will prompt the generation of a new
# session id, and migration of data to be referenced at the new id. If
# :defer is set, it will be overridden and the cookie will be set.
# * :sidbits sets the number of bits in length that a generated session
# id will be.
#
# These options can be set on a per request basis, at the location of
# env['rack.session.options']. Additionally the id of the session can be
# found within the options hash at the key :id. It is highly not
# recommended to change its value.
#
# Is Rack::Utils::Context compatible.
#
# Not included by default; you must require 'rack/session/abstract/id'
# to use.
class ID
DEFAULT_OPTIONS = {
:key => 'rack.session',
:path => '/',
:domain => nil,
:expire_after => nil,
:secure => false,
:httponly => true,
:defer => false,
:renew => false,
:sidbits => 128,
:cookie_only => true,
:secure_random => (::SecureRandom rescue false)
}
attr_reader :key, :default_options
def initialize(app, options={})
@app = app
@default_options = self.class::DEFAULT_OPTIONS.merge(options)
@key = @default_options.delete(:key)
@cookie_only = @default_options.delete(:cookie_only)
initialize_sid
end
def call(env)
context(env)
end
def context(env, app=@app)
prepare_session(env)
status, headers, body = app.call(env)
commit_session(env, status, headers, body)
end
private
def initialize_sid
@sidbits = @default_options[:sidbits]
@sid_secure = @default_options[:secure_random]
@sid_length = @sidbits / 4
end
# Generate a new session id using Ruby #rand. The size of the
# session id is controlled by the :sidbits option.
# Monkey patch this to use custom methods for session id generation.
def generate_sid(secure = @sid_secure)
if secure
SecureRandom.hex(@sid_length)
else
"%0#{@sid_length}x" % Kernel.rand(2**@sidbits - 1)
end
rescue NotImplementedError
generate_sid(false)
end
# Sets the lazy session at 'rack.session' and places options and session
# metadata into 'rack.session.options'.
def prepare_session(env)
session_was = env[ENV_SESSION_KEY]
env[ENV_SESSION_KEY] = SessionHash.new(self, env)
env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options)
env[ENV_SESSION_KEY].merge! session_was if session_was
end
# Extracts the session id from provided cookies and passes it and the
# environment to #get_session.
def load_session(env)
sid = current_session_id(env)
sid, session = get_session(env, sid)
[sid, session || {}]
end
# Extract session id from request object.
def extract_session_id(env)
request = Rack::Request.new(env)
sid = request.cookies[@key]
sid ||= request.params[@key] unless @cookie_only
sid
end
# Returns the current session id from the OptionsHash.
def current_session_id(env)
env[ENV_SESSION_OPTIONS_KEY][:id]
end
# Check if the session exists or not.
def session_exists?(env)
value = current_session_id(env)
value && !value.empty?
end
# Session should be commited if it was loaded, any of specific options like :renew, :drop
# or :expire_after was given and the security permissions match. Skips if skip is given.
def commit_session?(env, session, options)
if options[:skip]
false
else
has_session = loaded_session?(session) || forced_session_update?(session, options)
has_session && security_matches?(env, options)
end
end
def loaded_session?(session)
!session.is_a?(SessionHash) || session.loaded?
end
def forced_session_update?(session, options)
force_options?(options) && session && !session.empty?
end
def force_options?(options)
options.values_at(:renew, :drop, :defer, :expire_after).any?
end
def security_matches?(env, options)
return true unless options[:secure]
request = Rack::Request.new(env)
request.ssl?
end
# Acquires the session from the environment and the session id from
# the session options and passes them to #set_session. If successful
# and the :defer option is not true, a cookie will be added to the
# response with the session's id.
def commit_session(env, status, headers, body)
session = env['rack.session']
options = env['rack.session.options']
if options[:drop] || options[:renew]
session_id = destroy_session(env, options[:id] || generate_sid, options)
return [status, headers, body] unless session_id
end
return [status, headers, body] unless commit_session?(env, session, options)
session.send(:load!) unless loaded_session?(session)
session = session.to_hash
session_id ||= options[:id] || generate_sid
if not data = set_session(env, session_id, session, options)
env["rack.errors"].puts("Warning! #{self.class.name} failed to save session. Content dropped.")
elsif options[:defer] and not options[:renew]
env["rack.errors"].puts("Defering cookie for #{session_id}") if $VERBOSE
else
cookie = Hash.new
cookie[:value] = data
cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
set_cookie(env, headers, cookie.merge!(options))
end
[status, headers, body]
end
# Sets the cookie back to the client with session id. We skip the cookie
# setting if the value didn't change (sid is the same) or expires was given.
def set_cookie(env, headers, cookie)
request = Rack::Request.new(env)
if request.cookies[@key] != cookie[:value] || cookie[:expires]
Utils.set_cookie_header!(headers, @key, cookie)
end
end
# All thread safety and session retrival proceedures should occur here.
# Should return [session_id, session].
# If nil is provided as the session id, generation of a new valid id
# should occur within.
def get_session(env, sid)
raise '#get_session not implemented.'
end
# All thread safety and session storage proceedures should occur here.
# Should return true or false dependant on whether or not the session
# was saved or not.
def set_session(env, sid, session, options)
raise '#set_session not implemented.'
end
# All thread safety and session destroy proceedures should occur here.
# Should return a new session id or nil if options[:drop]
def destroy_session(env, sid, options)
raise '#destroy_session not implemented'
end
end
Rack::Session::Abstract::ID
实现了一套抽象的以ID作为标示方法的Session取出和存储的机制,主要负责提供Session在内存中的存储结构SessionHash
,Session相关选项的存储结构OptionHash
,Middleware接口,初始化Session和请求结束后对Session的写入功能。注意其Middleware主要实现请求处理前上一次请求的Session的装载以及SessionHash
和OptionHash
的初始化,请求处理后将Session写入同时Cookie记录Session ID的功能或者丢弃Session的功能。
下一个Middleware和Session相关,那就是ActionDispatch::Flash
,定义在actionpack-3.2.13/lib/action_dispatch/middleware/flash.rb
:
module ActionDispatch
class Request
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
# read a notice you put there or <tt>flash["notice"] = "hello"</tt>
# to put a new one.
def flash
@env[Flash::KEY] ||= (session["flash"] || Flash::FlashHash.new)
end
end
# The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
# action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can
# then expose the flash to its template. Actually, that exposure is automatically done. Example:
#
# class PostsController < ActionController::Base
# def create
# # save post
# flash[:notice] = "Post successfully created"
# redirect_to posts_path(@post)
# end
#
# def show
# # doesn't need to assign the flash notice to the template, that's done automatically
# end
# end
#
# show.html.erb
# <% if flash[:notice] %>
# <div class="notice"><%= flash[:notice] %></div>
# <% end %>
#
# Since the +notice+ and +alert+ keys are a common idiom, convenience accessors are available:
#
# flash.alert = "You must be logged in"
# flash.notice = "Post successfully created"
#
# This example just places a string in the flash, but you can put any object in there. And of course, you can put as
# many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
#
# See docs on the FlashHash class for more details about the flash.
class Flash
KEY = 'action_dispatch.request.flash_hash'.freeze
class FlashNow
attr_accessor :flash
def initialize(flash)
@flash = flash
end
def []=(k, v)
@flash[k] = v
@flash.discard(k)
v
end
def [](k)
@flash[k]
end
# Convenience accessor for flash.now[:alert]=
def alert=(message)
self[:alert] = message
end
# Convenience accessor for flash.now[:notice]=
def notice=(message)
self[:notice] = message
end
end
# Implementation detail: please do not change the signature of the
# FlashHash class. Doing that will likely affect all Rails apps in
# production as the FlashHash currently stored in their sessions will
# become invalid.
class FlashHash
include Enumerable
def initialize
@used = Set.new
@closed = false
@flashes = {}
@now = nil
end
def initialize_copy(other)
if other.now_is_loaded?
@now = other.now.dup
@now.flash = self
end
super
end
def []=(k, v)
keep(k)
@flashes[k] = v
end
def [](k)
@flashes[k]
end
def update(h)
h.keys.each { |k| keep(k) }
@flashes.update h
self
end
def keys
@flashes.keys
end
def key?(name)
@flashes.key? name
end
def delete(key)
@flashes.delete key
self
end
def to_hash
@flashes.dup
end
def empty?
@flashes.empty?
end
def clear
@flashes.clear
end
def each(&block)
@flashes.each(&block)
end
alias :merge! :update
def replace(h)
@used = Set.new
@flashes.replace h
self
end
# Sets a flash that will not be available to the next action, only to the current.
#
# flash.now[:message] = "Hello current action"
#
# This method enables you to use the flash as a central messaging system in your app.
# When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>).
# When you need to pass an object to the current action, you use <tt>now</tt>, and your object will
# vanish when the current action is done.
#
# Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
def now
@now ||= FlashNow.new(self)
end
# Keeps either the entire current flash or a specific flash entry available for the next action:
#
# flash.keep # keeps the entire flash
# flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded
def keep(k = nil)
use(k, false)
end
# Marks the entire flash or a single flash entry to be discarded by the end of the current action:
#
# flash.discard # discard the entire flash at the end of the current action
# flash.discard(:warning) # discard only the "warning" entry at the end of the current action
def discard(k = nil)
use(k)
end
# Mark for removal entries that were kept, and delete unkept ones.
#
# This method is called automatically by filters, so you generally don't need to care about it.
def sweep
keys.each do |k|
unless @used.include?(k)
@used << k
else
delete(k)
@used.delete(k)
end
end
# clean up after keys that could have been left over by calling reject! or shift on the flash
(@used - keys).each{ |k| @used.delete(k) }
end
# Convenience accessor for flash[:alert]
def alert
self[:alert]
end
# Convenience accessor for flash[:alert]=
def alert=(message)
self[:alert] = message
end
# Convenience accessor for flash[:notice]
def notice
self[:notice]
end
# Convenience accessor for flash[:notice]=
def notice=(message)
self[:notice] = message
end
protected
def now_is_loaded?
!!@now
end
# Used internally by the <tt>keep</tt> and <tt>discard</tt> methods
# use() # marks the entire flash as used
# use('msg') # marks the "msg" entry as used
# use(nil, false) # marks the entire flash as unused (keeps it around for one more action)
# use('msg', false) # marks the "msg" entry as unused (keeps it around for one more action)
# Returns the single value for the key you asked to be marked (un)used or the FlashHash itself
# if no key is passed.
def use(key = nil, used = true)
Array(key || keys).each { |k| used ? @used << k : @used.delete(k) }
return key ? self[key] : self
end
end
def initialize(app)
@app = app
end
def call(env)
if (session = env['rack.session']) && (flash = session['flash'])
flash.sweep
end
@app.call(env)
ensure
session = env['rack.session'] || {}
flash_hash = env[KEY]
if flash_hash
if !flash_hash.empty? || session.key?('flash')
session["flash"] = flash_hash
new_hash = flash_hash.dup
else
new_hash = flash_hash
end
env[KEY] = new_hash
end
if session.key?('flash') && session['flash'].empty?
session.delete('flash')
end
end
end
end
Rails的Flash是完全基于Session存储的特殊对象。在处理请求前,从Rails env中取出session,然后从session中取出flash,调用sweep
方法,将还没有使用过的flash标记为已经使用,已经使用过的flash则删除(对于flash.now
而言,任何flash在设置时就已经被标记为使用过,这是它和flash
的最大区别)。在action和view执行的过程中,对于flash的操作会被写入到Rails env中。在处理请求后,将flash从Rails env中拷贝一份并且放置到session中持久化。
下一个Middleware是ActionDispatch::ParamsParser
,定义在actionpack-3.2.13/lib/action_dispatch/middleware/params_parser.rb
中:
require 'active_support/core_ext/hash/conversions'
require 'action_dispatch/http/request'
require 'active_support/core_ext/hash/indifferent_access'
module ActionDispatch
class ParamsParser
DEFAULT_PARSERS = {
Mime::XML => :xml_simple,
Mime::JSON => :json
}
def initialize(app, parsers = {})
@app, @parsers = app, DEFAULT_PARSERS.merge(parsers)
end
def call(env)
if params = parse_formatted_parameters(env)
env["action_dispatch.request.request_parameters"] = params
end
@app.call(env)
end
private
def parse_formatted_parameters(env)
request = Request.new(env)
return false if request.content_length.zero?
mime_type = content_type_from_legacy_post_data_format_header(env) ||
request.content_mime_type
strategy = @parsers[mime_type]
return false unless strategy
case strategy
when Proc
strategy.call(request.raw_post)
when :xml_simple, :xml_node
data = request.deep_munge(Hash.from_xml(request.body.read) || {})
request.body.rewind if request.body.respond_to?(:rewind)
data.with_indifferent_access
when :yaml
YAML.load(request.raw_post)
when :json
data = ActiveSupport::JSON.decode(request.body)
request.body.rewind if request.body.respond_to?(:rewind)
data = {:_json => data} unless data.is_a?(Hash)
request.deep_munge(data).with_indifferent_access
else
false
end
rescue Exception => e # YAML, XML or Ruby code block errors
logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}"
raise e
end
def content_type_from_legacy_post_data_format_header(env)
if x_post_format = env['HTTP_X_POST_DATA_FORMAT']
case x_post_format.to_s.downcase
when 'yaml' then return Mime::YAML
when 'xml' then return Mime::XML
end
end
nil
end
def logger(env)
env['action_dispatch.logger'] || Logger.new($stderr)
end
end
end
由于Rails同样支持使用XML,JSON或者YAML作为参数上传,因此这个Middlware负责将这些参数全部转换为标准的Hash类型。这里识别MIME类型使用到了ActionDispatch::Http::MimeNegotiation
模块的content_mime_type
方法,定义在actionpack-3.2.13/lib/action_dispatch/http/mime_negotiation.rb
:
# The MIME type of the HTTP request, such as Mime::XML.
#
# For backward compatibility, the post \format is extracted from the
# X-Post-Data-Format HTTP header if present.
def content_mime_type
@env["action_dispatch.request.content_type"] ||= begin
if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/
Mime::Type.lookup($1.strip.downcase)
else
nil
end
end
end
实际搜索用得是Mime::Type.lookup
方法,在所有注册过的Mime类型中搜索出匹配的Mime::Type
类,不过由于parse_formatted_parameters
本身就实现了三种类型的支持,因此实际上也只能支持三种类型。如果能够找到,解析结果将放在Rails env的action_dispatch.request.request_parameters
项内。
下一个Middleware是ActionDispatch::Head
,定义在actionpack-3.2.13/lib/action_dispatch/middleware/head.rb
中:
module ActionDispatch
class Head
def initialize(app)
@app = app
end
def call(env)
if env["REQUEST_METHOD"] == "HEAD"
env["REQUEST_METHOD"] = "GET"
env["rack.methodoverride.original_method"] = "HEAD"
status, headers, _ = @app.call(env)
[status, headers, []]
else
@app.call(env)
end
end
end
end
Rails本身不直接支持HEAD请求,但是通过这个Middleware可以提供简单支持。这里先把Request Method改成GET请求,然后把原来的方法写入Rails env的rack.methodoverride.original_method
项,接着按照GET请求的方法继续响应请求。请求结束后,将body去除,只留下header和status code。
下一个Middleware是Rack::ConditionalGet
,定义在rack-1.4.5/lib/rack/conditionalget.rb
:
require 'rack/utils'
module Rack
# Middleware that enables conditional GET using If-None-Match and
# If-Modified-Since. The application should set either or both of the
# Last-Modified or Etag response headers according to RFC 2616. When
# either of the conditions is met, the response body is set to be zero
# length and the response status is set to 304 Not Modified.
#
# Applications that defer response body generation until the body's each
# message is received will avoid response body generation completely when
# a conditional GET matches.
#
# Adapted from Michael Klishin's Merb implementation:
# http://github.com/wycats/merb-core/tree/master/lib/merb-core/rack/middleware/conditional_get.rb
class ConditionalGet
def initialize(app)
@app = app
end
def call(env)
case env['REQUEST_METHOD']
when "GET", "HEAD"
status, headers, body = @app.call(env)
headers = Utils::HeaderHash.new(headers)
if status == 200 && fresh?(env, headers)
status = 304
headers.delete('Content-Type')
headers.delete('Content-Length')
body = []
end
[status, headers, body]
else
@app.call(env)
end
end
private
def fresh?(env, headers)
modified_since = env['HTTP_IF_MODIFIED_SINCE']
none_match = env['HTTP_IF_NONE_MATCH']
return false unless modified_since || none_match
success = true
success &&= modified_since?(to_rfc2822(modified_since), headers) if modified_since
success &&= etag_matches?(none_match, headers) if none_match
success
end
def etag_matches?(none_match, headers)
etag = headers['ETag'] and etag == none_match
end
def modified_since?(modified_since, headers)
last_modified = to_rfc2822(headers['Last-Modified']) and
modified_since and
modified_since >= last_modified
end
def to_rfc2822(since)
Time.rfc2822(since) rescue nil
end
end
end
这个Middleware能简单处理请求中包含HTTP_IF_MODIFIED_SINCE
和HTTP_IF_NONE_MATCH
的情况。当返回的Response的Header中也包含Last-Modified
或ETag
项时,并且Last-Modified
小于等于HTTP_IF_MODIFIED_SINCE
,且ETag
等于HTTP_IF_NONE_MATCH
时,将返回304请求而不是200请求,同时消除返回的body内容,这个方法虽然不能节省服务器的性能损耗,但是可以提高响应速度。
下一个请求是Rack::ETag
,定义在rack-1.4.5/lib/rack/etag.rb
中:
require 'digest/md5'
module Rack
# Automatically sets the ETag header on all String bodies.
#
# The ETag header is skipped if ETag or Last-Modified headers are sent or if
# a sendfile body (body.responds_to :to_path) is given (since such cases
# should be handled by apache/nginx).
#
# On initialization, you can pass two parameters: a Cache-Control directive
# used when Etag is absent and a directive when it is present. The first
# defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
class ETag
DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
@app = app
@cache_control = cache_control
@no_cache_control = no_cache_control
end
def call(env)
status, headers, body = @app.call(env)
if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
digest, body = digest_body(body)
headers['ETag'] = %("#{digest}") if digest
end
unless headers['Cache-Control']
if digest
headers['Cache-Control'] = @cache_control if @cache_control
else
headers['Cache-Control'] = @no_cache_control if @no_cache_control
end
end
[status, headers, body]
end
private
def etag_status?(status)
status == 200 || status == 201
end
def etag_body?(body)
!body.respond_to?(:to_path)
end
def skip_caching?(headers)
(headers['Cache-Control'] && headers['Cache-Control'].include?('no-cache')) ||
headers.key?('ETag') || headers.key?('Last-Modified')
end
def digest_body(body)
parts = []
body.each { |part| parts << part }
string_body = parts.join
digest = Digest::MD5.hexdigest(string_body) unless string_body.empty?
[digest, parts]
end
end
end
这个Middleware试图为每个有需求的Response添加ETag
项和Cache-Control
项。添加ETag
的条件是,Status码在200或201,body没有实现to_path
方法(在Rails中代表这是sendfile的Header),并且Cache-Control
项中没有包含no-cache
,Header中也没有ETag
或者是Last-Modified
。生成ETag的方法是对body的内容做MD5运算,结果前后各增加一个双引号。如果没有Cache-Control
,那么如果之前生成了ETag,这里设定的Cache-Control
是默认的"max-age=0, private, must-revalidate",如果没有生成过则使用Rails在声明这个Middleware时定义的'no-cache'。
最后一个Middleware是ActionDispatch::BestStandardsSupport
,定义在actionpack-3.2.13/lib/action_dispatch/middleware/best_standards_support.rb
:
module ActionDispatch
class BestStandardsSupport
def initialize(app, type = true)
@app = app
@header = case type
when true
"IE=Edge,chrome=1"
when :builtin
"IE=Edge"
when false
nil
end
end
def call(env)
status, headers, body = @app.call(env)
if headers["X-UA-Compatible"] && @header
unless headers["X-UA-Compatible"][@header]
headers["X-UA-Compatible"] << "," << @header.to_s
end
else
headers["X-UA-Compatible"] = @header
end
[status, headers, body]
end
end
end
这个Middleware完全为了Internet Explorer而存在,为了尽可能使Internet Explorer使用最高版本的内核解析HTML,如果安装了Google Chrome Frame,则启用它来让Internet Explorer使用Webkit核心来解析HTML。可以使用config.action_dispatch.best_standards_support
选项来对这一功能进行配置。
至此,这一阶段的Middleware已经结束,下一章将开始解析Rails Router的源代码。