环境: ruby 1.8.7 + rails 2.3.x, ruby 1.9.2 + rails 3.x
前段时间处理手机客户端post请求问题, 经常遇到csrf token 问题, 另外web上也经常遇到和session相关的问题, 不深究下去, 很多东西云里雾里, 于是把rails源码csrf_token, 和 session cookiestore 相关的代码研究了下.
csrf_token 原理
这个相信做rails的, 没有不知道的,是rails framework中为了防止 XSS攻击的, 可是你知道它的原理吗?
好, 顺着代码跟进去, 这是 rails 3.x 的源码.
入口就是 ApplicationController 中的 protect_from_forgery
# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 85 def protect_from_forgery(options = {}) self.request_forgery_protection_token ||= :authenticity_token prepend_before_filter :verify_authenticity_token, options end
再找到 verify_authenticity_token
def verify_authenticity_token verified_request? || handle_unverified_request end
再找到 verified_request?
发现判断合法的请求方法:
1, 跳过不验证的
2, GET 请求
3, csrf_token 和参数中 authenticity_token 值相同的
4, http header 中 X-CSRF-Token 和 csrf_token 的值相同的
不合法请求会 reset_session
def handle_unverified_request reset_session end
来看看 rails 2.x 的, 为什么说 rails 2.x 的,和 rails 3.x 不一样, 影响也很大.
合法的请求:
1, 跳过检查的
2, GET 请求
3, ajax 请求
4, 不是 html 格式请求, 例如 json, xml 格式请求都是合法的
5, csrf_token 值和 参数中 authenticity_token 值相同的
不合法的请求会 raise error
def verify_authenticity_token verified_request? || raise(ActionController::InvalidAuthenticityToken) end
上面的处理都有漏洞, 来看看
rails 3.x 的, 假如用户登录是 post, 登录前还没有 session ,此时会 reset_ssession,因为本来就没有登录后的session,reset_session后,后面的代码继续执行, 假如用户知道用户用户名,密码,利用http client 工具就可以成功获得登录后的session, 虽然 csrf 会验证失败, 所以可以自己打个patch使用 rails 2.x 的方式, 直接 raise
rails2.x 的当请求格式不是 html,是 json 就可以成功跳过 csrf 验证, 例如我这个更新redmine的脚本就是利用这个漏洞实现的.
https://gist.github.com/wxianfeng/5070599
那么 csrf_token 的值又是存在什么地方的呢, 在 session[:_csrf_token],rails 默认session是 cookie store, 这就涉及到cookiestore原理了.
关于 csrf_token 还有一个需要注意的地方, 在 test env 下是不需要 csrf_token 的, 顺着 csrf_meta_tag 跟进去可以看到.
Rails Session CookieStore 原理
在rails后端调试下 session, 打印出来的结果是一个hash, 以github 为例, 先反向得到 session 数据, 用firebug可以看到github的cookie中有一个 _gh_session, 如下:
_gh_sess=BAh7CjoPc2Vzc2lvbl9pZCIlMjM1OGMwZjFhYmU2MTQ0MGRlYWUzYWVhODVhM2U2MTk6EF9jc3JmX3Rva2VuSSIxcHNLWEFoYittaXVVVnZXU3BxMDBJaE52Z0QvQ0kyYjg1cU5pNTJMU2R6TT0GOgZFRjoJdXNlcmkD2IsBOhBmaW5nZXJwcmludCIlZGFmNjBhOGFlYTJlZWE3YThjNWY1OGRmMzg2YzhhNWQ6DGNvbnRleHRJIgYvBjsHRg%3D%3D--0320c02623b8a27a66bbbcd38d095511c459e1f3;
取出 — 前面的部分, 假设为 data
::Marshal.load ::Base64.decode64(data) 后会得到一个hash, 这个就是后端的 session数据
ruby-1.9.2-p290 :016 > data = "BAh7CjoPc2Vzc2lvbl9pZCIlMjM1OGMwZjFhYmU2MTQ0MGRlYWUzYWVhODVhM2U2MTk6EF9jc3JmX3Rva2VuSSIxcHNLWEFoYittaXVVVnZXU3BxMDBJaE52Z0QvQ0kyYjg1cU5pNTJMU2R6TT0GOgZFRjoJdXNlcmkD2IsBOhBmaW5nZXJwcmludCIlZGFmNjBhOGFlYTJlZWE3YThjNWY1OGRmMzg2YzhhNWQ6DGNvbnRleHRJIgYvBjsHRg%3D%3D" => "BAh7CjoPc2Vzc2lvbl9pZCIlMjM1OGMwZjFhYmU2MTQ0MGRlYWUzYWVhODVhM2U2MTk6EF9jc3JmX3Rva2VuSSIxcHNLWEFoYittaXVVVnZXU3BxMDBJaE52Z0QvQ0kyYjg1cU5pNTJMU2R6TT0GOgZFRjoJdXNlcmkD2IsBOhBmaW5nZXJwcmludCIlZGFmNjBhOGFlYTJlZWE3YThjNWY1OGRmMzg2YzhhNWQ6DGNvbnRleHRJIgYvBjsHRg%3D%3D" ruby-1.9.2-p290 :017 > ::Marshal.load ::Base64.decode64(data) => {:session_id=>"2358c0f1abe61440deae3aea85a3e619", :_csrf_token=>"psKXAhb+miuUVvWSpq00IhNvgD/CI2b85qNi52LSdzM=", :user=>101336, :fingerprint=>"daf60a8aea2eea7a8c5f58df386c8a5d", :context=>"/"}
再来正向生成
data = ::Base64.encode64 Marshal.dump(h)
那—后面的 digest 是怎么生成的, 和rails中 secrect 合起来加密生成的, 这样别人就不能伪造cookie了,术语叫 cookie 签名.
大概生成算法是这样:
session = {"_csrf_token"="xxxxx","user_id"=>4} session_data = ::Base64.encode64 Marshal.dump(session) session_data = "#{session_data}--#{generate_hmac(session_data, @secrets.first)}" def generate_hmac(data, secret) OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, data) end
源码位置:
/Users/wangxianfeng/.rvm/gems/ruby-1.9.2-p290/gems/rack-1.4.3/lib/rack/session/cookie.rb /home/wxianfeng/.rvm/gems/ruby-1.9.2-p320/gems/activesupport-3.2.2/lib/active_support/message_verifier.rb
总结起来一句话, rails 的session cookiestore 是存在浏览器的cookie中的, 由 http 协议的 headers 带到后端, 后端解包出来的.
OVER, 是不是没想到啊?
最近需要为一个站集成 OAuth 协议 第三方站点账号登录, 目前实现了两个 sina weibo 和 qq, 说下大致步骤 和注意事项,关键用了rails里面经典的gem omniauth.
weibo:
1, 到 http://open.weibo.com 申请应用, 会给你 key和secret
2, 有一步会验证你的 webmaster 权限,按照说明来即可
3, 我使用的gem
gem "omniauth", '1.1.0' gem 'omniauth-weibo-oauth2', '0.2.0'
4, 为 omniauth 配置 weibo provider
config/initliazers/omniauth.rb:
Rails.application.config.middleware.use OmniAuth::Builder do provider :weibo, '1978113365', '82fa3e1654c905b5b545a16945ahjiyb' end
5, 为了便于调试,修改 hosts 把域名指向本地
/etc/hosts
127.0.0.1 www.abc.com
6, 配置 callback 路由
config/routes.rb
match "/auth/:provider/callback" => "sessions#auth"
7, 访问weibo
www.abc.com:3000/auth/weibo
注意这里不能用 localhost访问,不然会得到:
redirect_uri_mismatch
错误
还有测试的时候,需要到 open.weibo.com 中指定测试账号,这个也需要注意下.
8, 成功后你的callback action中会收到返回给的数据,接下来就是你的事情了,基本的 你可以这样看到
Rails.logger.debug request.env["omniauth.auth"]
QQ:
大致步骤和 weibo 一样,但是不同的是 qq 需要配置 回调地址
我在open qq中配置的回调地址是 abc.com
但是本地调试时, 访问abc.com:3000/auth/qq 总是得到
redirect uri is illegal(100010)!
错误
最后找到的原因是 不能用 3000 端口, 需要使用 80 端口, 最后用 nginx + unicorn 配置80端口访问 解决了.
qq 用的gem是
gem 'omniauth-qq', :git => 'git://github.com/blankyao/omniauth-qq.git'
Ok, That’s ALL!!!
环境: Rails 3.0.3
Rails中判断表是否存在,表的某个字段是否存在都提供了API,但是如何判断数据库存在,没有提供api
可以使用下面的方法判断出来:
> ActiveRecord::Base.connection.execute("USE INFORMATION_SCHEMA") # 连接mysql INFORMATION_SCHEMA 数据库 >ActiveRecord::Base.connection.execute("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = 'db'").to_a =>[["db"]] # 存在db >ActiveRecord::Base.connection.execute("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = 'db1'").to_a =>[] # 不存在db1
时间: 2011-07-24
收获:
发现北京ROR的公司不是一般的多,签到单上看到N多公司,技术上没有太大收获,都是介绍性的,没有实战性的,内容主要涉及: mirah , Mongodb,Erlang,Grape
进程:http://www.surveymonkey.com/s/MSY2L7T
PS : 798 很好玩,很有艺术特色
现场:
798 入口
Ruby活动地方
Rails rumble 创始人
现场job board
现场
清一色老外,清一色Mac
介绍Mirah
798
798
798
环境:ruby1.9.2 + rails 3.0.3
一直以为
add_column :users , :age , :integer , :limit=> 4
在数据库里对应的类型是 int(4)
其实是错误的!!!
看下 limit的说明文档:
:limit - Requests a maximum column length. This is number of characters for :string and :text columns and number of bytes for :binary and :integer columns.
对于 string 和 text 比较简单,例如
add_column :users,:name , :string , :limit=> 60
那么数据库中的 类型就是 varchar(60)
对于 binary 和 integer 的就不一样了 , 表示的是字节数 , 但是 :limit =>11 不是表示 11个字节的整数 , 是4 个字节整数
对应关系:
:limit Numeric Type Column Size 1 tinyint 1 byte 2 smallint 2 bytes 3 mediumint 3 bytes nil, 4, 11 int(11) 4 bytes 5 to 8 bigint 8 bytes
而mysql的integer类型(也是int型) 表示大小如下:
详细 here
rails 里的实现代码 here
核心代码:
# Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) return super unless type.to_s == 'integer' case limit when 1; 'tinyint' when 2; 'smallint' when 3; 'mediumint' when nil, 4, 11; 'int(11)' # compatibility with MySQL default when 5..8; 'bigint' else raise(ActiveRecordError, "No integer type has byte size #{limit}") end end
从上面代码可以看出, 当limit 为 nil,4,11 的时候 , mysql的类型就是 int(11), 也就是常在 migration看到的 integer , :limit=>11
另外可以从已经有的表中得到字段的字节数
ruby-1.9.2-p0 > ActiveRecord::Migration.add_column :forms , :int9, :integer , :limit=>11 -- add_column(:forms, :int9, :integer, {:limit=>11}) SQL (300.9ms) ALTER TABLE `forms` ADD `int9` int(11) -> 0.3012s => nil ruby-1.9.2-p0 > Form.reset_column_information => nil ruby-1.9.2-p0 > Form.columns_hash["int9"].limit => 4 ruby-1.9.2-p0 > Form.columns_hash["int9"] => #<ActiveRecord::ConnectionAdapters::Mysql2Column:0xb8407c8 @null=true, @sql_type="int(11)", @name="int9", @scale=nil, @precision=nil, @limit=4, @type=:integer, @default=nil, @primary=false>
看到没,在迁移的时候 指定 :limit => 11 , 但是 通过 columns_hash 得到的 limit 确是4 , 也就是说 mysql 的 Column Size 是4 bytes , 如果指定 :limit => nil 或者 :limit=>4 得到的 都是 4 bytes
因为 rails 里面 主键id 默认是有符号的 int(11) ,所以 mysql的主键最大id 是 2147483647 , 如果改成无符号的 最大可以到 4294967295
ruby 如何得到 整数的字节数?
size 方法
ruby-1.9.2-p0 > 10_000_000_000.size => 8 ruby-1.9.2-p0 > 1.size => 4
如何让我的integer类型的字段变成无符号的?
ruby-1.9.2-p0 > ActiveRecord::Migration.add_column :forms , :int8, "integer unsigned" -- add_column(:forms, :int8, "integer unsigned") SQL (297.4ms) ALTER TABLE `forms` ADD `int8` integer unsigned -> 0.2976s => nil
如何正确 添加mysql中int(4)类型的字段?
ruby-1.9.2-p0 > ActiveRecord::Migration.add_column :users , :number , "int(4)" -- add_column(:users, :number, "int(4)") SQL (280.4ms) ALTER TABLE `users` ADD `number` int(4) -> 0.2808s
最后看下 migration 类型 对应 数据库类型的关系:
SEE:
http://www.snowgiraffe.com/tech/366/rails-migrations-mysql-unsigned-integers-primary-keys-and-a-lot-of-fun-times/
http://thewebfellas.com/blog/2008/6/2/unsigned-integers-for-mysql-on-rails
http://www.kuqin.com/rubycndocument/man/built-in-class/class_object_numeric_integer.html
测试环境:rails 2.X + rails 3.0.3
今天发现 rails api上索引name的命名规则是错误的,here
测试:
ruby-1.9.2-p0 > ActiveRecord::Migration.add_index :users , :email -- add_index(:users, :email) SQL (0.4ms) SHOW KEYS FROM `users` SQL (367.1ms) CREATE INDEX `index_users_on_email` ON `users` (`email`) -> 0.3680s
按照 文档上写的应该是 users_email ,但是实际上是 index_users_on_email
ruby-1.9.2-p0 > ActiveRecord::Migration.add_index :users , :name , :unique=>true -- add_index(:users, :name, {:unique=>true}) SQL (0.3ms) SHOW KEYS FROM `users` SQL (340.4ms) CREATE UNIQUE INDEX `index_users_on_name` ON `users` (`name`) -> 0.3413s
按照文档写的应该是 users_name ,但是实际上是 index_users_on_name
ruby-1.9.2-p0 > ActiveRecord::Migration.add_index :users , [:login,:name] -- add_index(:users, [:login, :name]) SQL (0.3ms) SHOW KEYS FROM `users` SQL (314.3ms) CREATE INDEX `index_users_on_login_and_name` ON `users` (`login`, `name`) -> 0.3152s
按照文档下写的应该是 users_login_name 而实际是 index_users_on_login_and_name
为什么 name 如此重要,因为 remove_index 也是根据name 来的,所以 规则必须记住!
例如文档上一个demo:
# Remove the suppliers_name_index in the suppliers table. # remove_index :suppliers, :name
其实 是删除 name 为 index_suppliers_on_name 的索引 ,而不是文档上说的 suppliers_name_index
文档上是错误的,从 源码中 也可以看出来
idnex_name method 源码:
def index_name(table_name, options) #:nodoc: if Hash === options # legacy support if options[:column] "index_#{table_name}_on_#{Array.wrap(options[:column]) * '_and_'}" # HERE elsif options[:name] options[:name] else raise ArgumentError, "You must specify the index name" end else index_name(table_name, :column => options) end end
rails查看 表的索引:
ruby-1.9.2-p0 > ActiveRecord::Migration.indexes("users") # users 是表名
环境:ruby 1.9.2 + rails 3.0.3
rails console 用起来还是很爽的,路由也可以在console下使用 , 甚至可以 get , post , 下面介绍惯用手法:
1,rake 查看routes
>rake routes
2,console 下查看 routes
Rails.application.routes.routes # rails 2.x 使用 ActionController::Routing::Routes.routes
3, 查看 root(routes)
ruby-1.9.2-p0 > app.root_path => "/" ruby-1.9.2-p0 > app.root_url => "http://www.example.com/" ruby-1.9.2-p0 > app.host = "www.blog.wxianfeng.com" => "www.blog.wxianfeng.com" ruby-1.9.2-p0 > app.root_url => "http://www.blog.wxianfeng.com/"
4,查看资源 路由
ruby-1.9.2-p0 > user = User.first User Load (0.3ms) SELECT `users`.* FROM `users` LIMIT 1 => #<User id: 1, login: "entos", name: "", email: "entos@entos.com", crypted_password: "3dea29b4e40bc9a70bb63678678c5ff37fe49753", salt: "2ec7e5db7f3ce5de61f1add8275b674dbd2770dc", remember_token: nil, remember_token_expires_at: nil, activation_code: nil, activated_at: nil, status: 2, suspend_at: nil, avatar_id: nil, orgunit_id: nil, mobile_phone: nil, last_login_at: nil, language: nil, options: nil, created_at: "2011-03-01 07:42:37", updated_at: "2011-03-01 07:42:37"> ruby-1.9.2-p0 > app.user_path(user) => "/users/1" ruby-1.9.2-p0 > app.users_path => "/users" ruby-1.9.2-p0 > app.new_user_path => "/users/new" ruby-1.9.2-p0 > app.edit_user_path(:id=>user.id) => "/users/1/edit" ruby-1.9.2-p0 > app.users_url => "http://www.blog.wxianfeng.com/users"
5,不使用app调用
ruby-1.9.2-p0 > include ActionController::UrlWriter => Object ruby-1.9.2-p0 > default_url_options[:host] = "blog.wxianfeng.com" => "blog.wxianfeng.com" ruby-1.9.2-p0 > users_url => "http://blog.wxianfeng.com/users"
6,path 和 route Hash 互转
ruby-1.9.2-p0 > r = Rails.application.routes ruby-1.9.2-p0 > r.generate :controller => "users" , :action=>"new" => "/signup" ruby-1.9.2-p0 > r.generate :controller => "users" , :action=>"edit" , :id=>1 => "/users/1/edit" ruby-1.9.2-p0 > r.recognize_path "/users/index" => {:action=>"show", :controller=>"users", :id=>"index"} ruby-1.9.2-p0 > r.recognize_path "/users",:method=>"post" => {:action=>"create", :controller=>"users"}
7,get ,post
模拟get访问首页,没登录 然后跳转到了/login , 然后 post 提交登录 成功
ruby-1.9.2-p0 > app.class => ActionDispatch::Integration::Session ruby-1.9.2-p0 > app.get "/" => 302 ruby-1.9.2-p0 > app.controller.params => {"controller"=>"welcome", "action"=>"index"} ruby-1.9.2-p0 > app.response.redirect_url => "http://www.example.com/login" ruby-1.9.2-p0 > app.post "/session" , {:login=>"entos",:password=>"netposa"} SQL (0.3ms) SHOW TABLES User Load (0.2ms) SELECT `users`.* FROM `users` WHERE (status = 2) AND (`users`.`login` = 'entos') LIMIT 1 SQL (0.1ms) BEGIN User Load (0.3ms) SELECT `users`.* FROM `users` WHERE (`users`.`id` = 1) LIMIT 1 SQL (0.0ms) COMMIT => 302 ruby-1.9.2-p0 > app.controller.params => {"login"=>"entos", "password"=>"netposa", "action"=>"create", "controller"=>"sessions"} ruby-1.9.2-p0 > app.session[:user_id] => 1 ruby-1.9.2-p0 > app.cookies => #<Rack::Test::CookieJar:0xb010120 @default_host="www.example.com", @cookies=[#<Rack::Test::Cookie:0x9b726f0 @default_host="www.example.com", @name_value_raw="_ent_os_session=BAh7CEkiD3Nlc3Npb25faWQGOgZFRiIlMzM4ZTdhYzU4OTY3NDhmMmZmMGFhNDkyYTExZWVmOThJIgx1c2VyX2lkBjsARmkGSSIKZmxhc2gGOwBGSUM6JUFjdGlvbkRpc3BhdGNoOjpGbGFzaDo6Rmxhc2hIYXNoewY6C25vdGljZUkiG0xvZ2dlZCBpbiBzdWNjZXNzZnVsbHkGOwBUBjoKQHVzZWRvOghTZXQGOgpAaGFzaHsA--d8652cbfebcae436e64a824d7ac2f64a81aa6619", @name="_ent_os_session", @value="BAh7CEkiD3Nlc3Npb25faWQGOgZFRiIlMzM4ZTdhYzU4OTY3NDhmMmZmMGFhNDkyYTExZWVmOThJIgx1c2VyX2lkBjsARmkGSSIKZmxhc2gGOwBGSUM6JUFjdGlvbkRpc3BhdGNoOjpGbGFzaDo6Rmxhc2hIYXNoewY6C25vdGljZUkiG0xvZ2dlZCBpbiBzdWNjZXNzZnVsbHkGOwBUBjoKQHVzZWRvOghTZXQGOgpAaGFzaHsA--d8652cbfebcae436e64a824d7ac2f64a81aa6619", @options={"path"=>"/", "HttpOnly"=>nil, "domain"=>"www.example.com"}>, #<Rack::Test::Cookie:0x9b826f4 @default_host="www.example.com", @name_value_raw="auth_token=", @name="auth_token", @value="", @options={"path"=>"/", "domain"=>"www.example.com"}>]> ruby-1.9.2-p0 > app.response.redirect_url => "http://www.example.com/" ruby-1.9.2-p0 > app.flash => {:notice=>"Logged in successfully"} ruby-1.9.2-p0 >
甚至 你还可以 ajax 异步提交
>> app.xml_http_request "/store/add_to_cart", :id => 1 => 200
8,分配一个 实例变量
>>app.assigns[:foo] = “bar”
SEE
http://clarkware.com/blog/2006/04/04/running-your-rails-app-headless
http://blog.zobie.com/2008/11/testing-routes-in-rails/
http://railstech.com/2010/06/routes-testing-in-rails/
http://stuartsierra.com/2008/01/08/testing-named-routes-in-the-rails-console
环境: ruby1.9.2 + rails 3.0.3
我们知道 params 返回的是一个 hash , 例如 {"id"=>1} ,那为什么 params[:id] = 1 ,而不是 nil 呢 ?
irb下测试一下:
ruby-1.9.2-p0 > h={"id"=>1} => {"id"=>1} ruby-1.9.2-p0 > h[:id] => nil
带着这个疑问,设置断点 ,debug 进rails 源码 , 发现了原因,
1,跟到了 params 方法源码:
def params
@_params ||= request.parameters
end
给 @_params 设置Watch , 发现如下 :
发现 @_params 的class 是 ActiveSupport::HashWithIndifferentAccess
也就是说
params.class # => ActiveSupport::HashWithIndifferentAccess
2,继续 F7 跟进去
跟到了这个文件 的 default方法
require 'active_support/core_ext/hash/keys' # This class has dubious semantics and we only have it so that # people can write params[:key] instead of params['key'] # and they get the same value for both keys. module ActiveSupport class HashWithIndifferentAccess < Hash def extractable_options? true end def initialize(constructor = {}) if constructor.is_a?(Hash) super() update(constructor) else super(constructor) end end def default(key = nil) if key.is_a?(Symbol) && include?(key = key.to_s) self[key] else super end end def self.new_from_hash_copying_default(hash) ActiveSupport::HashWithIndifferentAccess.new(hash).tap do |new_hash| new_hash.default = hash.default end end alias_method :regular_writer, :[]= unless method_defined?(:regular_writer) alias_method :regular_update, :update unless method_defined?(:regular_update) # Assigns a new value to the hash: # # hash = HashWithIndifferentAccess.new # hash[:key] = "value" # def []=(key, value) regular_writer(convert_key(key), convert_value(value)) end alias_method :store, :[]= # Updates the instantized hash with values from the second: # # hash_1 = HashWithIndifferentAccess.new # hash_1[:key] = "value" # # hash_2 = HashWithIndifferentAccess.new # hash_2[:key] = "New Value!" # # hash_1.update(hash_2) # => {"key"=>"New Value!"} # def update(other_hash) other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) } self end alias_method :merge!, :update # Checks the hash for a key matching the argument passed in: # # hash = HashWithIndifferentAccess.new # hash["key"] = "value" # hash.key? :key # => true # hash.key? "key" # => true # def key?(key) super(convert_key(key)) end alias_method :include?, :key? alias_method :has_key?, :key? alias_method :member?, :key? # Fetches the value for the specified key, same as doing hash[key] def fetch(key, *extras) super(convert_key(key), *extras) end # Returns an array of the values at the specified indices: # # hash = HashWithIndifferentAccess.new # hash[:a] = "x" # hash[:b] = "y" # hash.values_at("a", "b") # => ["x", "y"] # def values_at(*indices) indices.collect {|key| self[convert_key(key)]} end # Returns an exact copy of the hash. def dup HashWithIndifferentAccess.new(self) end # Merges the instantized and the specified hashes together, giving precedence to the values from the second hash # Does not overwrite the existing hash. def merge(hash) self.dup.update(hash) end # Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second. # This overloaded definition prevents returning a regular hash, if reverse_merge is called on a HashWithDifferentAccess. def reverse_merge(other_hash) super self.class.new_from_hash_copying_default(other_hash) end def reverse_merge!(other_hash) replace(reverse_merge( other_hash )) end # Removes a specified key from the hash. def delete(key) super(convert_key(key)) end def stringify_keys!; self end def stringify_keys; dup end undef :symbolize_keys! def symbolize_keys; to_hash.symbolize_keys end def to_options!; self end # Convert to a Hash with String keys. def to_hash Hash.new(default).merge!(self) end protected def convert_key(key) key.kind_of?(Symbol) ? key.to_s : key end def convert_value(value) case value when Hash self.class.new_from_hash_copying_default(value) when Array value.collect { |e| e.is_a?(Hash) ? self.class.new_from_hash_copying_default(e) : e } else value end end end end HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess
好了 所有 实现的原理都在 这个文件里了,HashWithIndifferentAccess是 Hash的子类,其中覆盖了default 方法,Hash当找不到 hash 的 key 时 会寻找default值,即执行 default 方法 , so ….
Hash#default
用法demo:
ruby-1.9.2-p0 > h={} => {} ruby-1.9.2-p0 > h.default=1 => 1 ruby-1.9.2-p0 > h[:a] => 1
核心代码:
def default(key = nil) if key.is_a?(Symbol) && include?(key = key.to_s) self[key] else super end end
当是symbol 时 转化为 string , 然后 self[string_key]
举一反三:
DEMO:
class MyHash < Hash def initialize(constructor = {}) if constructor.is_a?(Hash) super() update(constructor) # Hash#update == Hash#merge! else super(constructor) end end def default(key = nil) p key #=> :id if key.is_a?(Symbol) && include?(key = key.to_s) # Hash#include? = Hash#has_key? = Hash#member? = Hash#key? p key #=> "id" self[key] else super end end end h = MyHash.new({"id"=>1}) p h #=> {"id"=>1} p h.class #=> MyHash p h[:id] #=> 1 p h["id"] #=> 1
SEE:
环境:ruby 1.9.2 + rails 3.0.3
我们经常会有这样的操作:
user = User.find_by_login("wxianfeng") # => nil user.name # => NoMethodError: undefined method `name' for nil:NilClass
假如 login 为 wxianfeng 不存在 ,会报错:
NoMethodError: undefined method `name' for nil:NilClass
那么建议使用 try 方法避免报错,try 返回的是 nil
user.try(:name) # =>nil
也就相当于
nil.try(:name) # => nil
看下源码: here
其实就是调用了 __send__
方法 , __send__
方法 和 send 方法等价 , 只不过 __send__
方法 为了防止 有已经存在的 send 方法 , nil 的话 调用 NilClass 的 try 方法
另外 发现 github上 try方法已经重新写了 ,如下: here
class Object # Invokes the method identified by the symbol +method+, passing it any arguments # and/or the block specified, just like the regular Ruby <tt>Object#send</tt> does. # # *Unlike* that method however, a +NoMethodError+ exception will *not* be raised # and +nil+ will be returned instead, if the receiving object is a +nil+ object or NilClass. # # If try is called without a method to call, it will yield any given block with the object. # # ==== Examples # # Without try # @person && @person.name # or # @person ? @person.name : nil # # With try # @person.try(:name) # # +try+ also accepts arguments and/or a block, for the method it is trying # Person.try(:find, 1) # @people.try(:collect) {|p| p.name} # # Without a method argument try will yield to the block unless the reciever is nil. # @person.try { |p| "#{p.first_name} #{p.last_name}" } #-- # +try+ behaves like +Object#send+, unless called on +NilClass+. def try(*a, &b) if a.empty? && block_given? yield self else __send__(*a, &b) end end end class NilClass #:nodoc: def try(*args) nil end end
其实只是判断了 if a.empty? && block_given? 这种情况 则直接执行block 内容然后返回,效果一样…..
DEMO:
require "active_support/core_ext/object/try" class Klass def send(*args) "helo " + args.join(' ') end def hello(*args) "Hello " + args.join(' ') end def self.foobar(s) "#{s} foobar" end end k = Klass.new # __send__ 为了防止有方法名叫send , 建议用 __send__ p k.__send__ :hello, "gentle", "readers" #=> "Hello gentle readers" p k.send "gentle", "readers" #=> "Helo gentle readers" # Ruby 里一切皆是对象,类也是对象 # Klass(类) 是 Class 的实例 , Class 是 Object 的实例 , 那么 Klass 也就是 Object 的实例 所以 Klass 可以调用try 方法 p Klass.try(:foobar,"hey") # => "hey foobar" # k 是Klass 的实例,Klass 的父类是 Object , 所以 k 可以调用 try 方法 p k.try(:send,"bla","bla") # => "helo bla bla" # class 得到的是 实例关系 # superclass 得到的是 继承关系 p Klass.superclass # Object p Klass.class # Class p k.class # Klass
另外 这是 对象nil 那如果 没有那个字段了 , 就会 报 找不到方法的错误
例如:
ruby-1.9.2-p0 > u=User.first User Load (175.8ms) SELECT `users`.* FROM `users` LIMIT 1 => #<User id: 1, login: "entos", name: "", email: "entos@entos.com", crypted_password: "557c88b0713f63397249f4198368e4a57d6d400f", salt: "4e04ef1cf506595ac3edf6a249791c55995b0f8f", remember_token: nil, remember_token_expires_at: nil, activation_code: nil, activated_at: nil, status: 2, suspend_at: nil, avatar_id: nil, orgunit_id: nil, mobile_phone: nil, last_login_at: nil, language: nil, options: nil, created_at: "2011-02-24 02:55:42", updated_at: "2011-02-24 02:55:42"> ruby-1.9.2-p0 > u.hi NoMethodError: undefined method `hi' for #<User:0x9fcfe00>
建议加上 respond_to? 判断
ruby-1.9.2-p0 > u.respond_to? "hi" => false
环境:ruby 1.9.2 + rails 3.0.3
我们经常需要在 rails console 中进行Model的操作,想看执行的sql ,必须到 rails log 中去查看 , 现在 有一个更好的办法,直接输出到 console 中…
在console 运行下面这句话即可:
ActiveRecord::Base.logger = Logger.new(STDOUT)
或者 直接写到 config/appliction.rb 中 ,下次启动console的时候 不需要在写上面语句:
if Rails.env == 'development' ActiveRecord::Base.logger = Logger.new(STDOUT) end
DEMO:
wxianfeng@ubuntu:/usr/local/system/projects/entos/ent_os$ rails c Loading development environment (Rails 3.0.3) ruby-1.9.2-p0 > ActiveRecord::Base.logger = Logger.new(STDOUT) => #<Logger:0xadc0730 @progname=nil, @level=0, @default_formatter=#<Logger::Formatter:0xadc071c @datetime_format=nil>, @formatter=nil, @logdev=#<Logger::LogDevice:0xadc06a4 @shift_size=nil, @shift_age=nil, @filename=nil, @dev=#<IO:<STDOUT>>, @mutex=#<Logger::LogDevice::LogDeviceMutex:0xadc0690 @mon_owner=nil, @mon_count=0, @mon_mutex=#<Mutex:0xadc0668>>>> ruby-1.9.2-p0 > User.last User Load (0.2ms) SELECT `users`.* FROM `users` ORDER BY users.id DESC LIMIT 1 => #<User id: 15, login: "xxxxxx", name: "", email: "xx@zz.com", crypted_password: "471f98733c6d2456df58a354feddcf7af22ea78e", salt: "f03c284f91365a3eeb30a2898b79524694efdac5", remember_token: nil, remember_token_expires_at: nil, activation_code: nil, activated_at: "2011-01-07 08:00:25", status: 2, suspend_at: nil, avatar_id: nil, orgunit_id: nil, mobile_phone: nil, last_login_at: nil, language: nil, options: nil, created_at: "2011-01-07 08:00:17", updated_at: "2011-01-07 08:00:25">
另外 还可以 使用 hirb gem 来让输出格式以表格排列,个人不是太喜欢,原有的方式可以看出数据的返回格式,是集合数组 , 还是单个对象 一清二楚 。。。而hirb 就没有了
SEE: