Rails csrf_token 和 Session CookieStore 原理

Posted by wxianfeng Fri, 01 Mar 2013 22:59:00 GMT

环境: 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, 是不是没想到啊?


rails 集成 OAuth 第三方登录

Posted by wxianfeng Fri, 26 Oct 2012 18:04:00 GMT

最近需要为一个站集成 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 判断mysql数据库是否存在

Posted by wxianfeng Wed, 15 Feb 2012 20:56:00 GMT

环境: 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

北京 798 Ruby/Rails 活动

Posted by wxianfeng Fri, 29 Jul 2011 18:36:00 GMT

时间: 2011-07-24
收获:
发现北京ROR的公司不是一般的多,签到单上看到N多公司,技术上没有太大收获,都是介绍性的,没有实战性的,内容主要涉及: mirah , Mongodb,Erlang,Grape

进程:http://www.surveymonkey.com/s/MSY2L7T

PS : 798 很好玩,很有艺术特色

现场:

093

798 入口

059

Ruby活动地方

054

Rails rumble 创始人

049

现场job board

067

现场

061

清一色老外,清一色Mac

056

介绍Mirah

045

798

083

798

043

798

MORE


Rails 深入学习 Migration limit

Posted by wxianfeng Sat, 16 Apr 2011 21:09:00 GMT

环境: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 注意索引 name

Posted by wxianfeng Thu, 14 Apr 2011 17:56:00 GMT

测试环境: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 是表名

Rails console 测试路由

Posted by wxianfeng Tue, 01 Mar 2011 17:00:00 GMT

环境: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.wxianfeng.com"
 => "www.wxianfeng.com" 
ruby-1.9.2-p0 > app.root_url
 => "http://www.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.wxianfeng.com/users" 

5,不使用app调用

ruby-1.9.2-p0 > include ActionController::UrlWriter
 => Object 
ruby-1.9.2-p0 > default_url_options[:host] = "wxianfeng.com"
 => "wxianfeng.com" 
ruby-1.9.2-p0 > users_url
 => "http://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


rails params[:id] 实现原理

Posted by wxianfeng Thu, 10 Feb 2011 16:36:00 GMT

环境: 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:

http://lukaszwrobel.pl/blog/ruby-hash-default-value


Rails try method

Posted by wxianfeng Thu, 13 Jan 2011 19:20:00 GMT

环境: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

rails 控制台输出sql

Posted by wxianfeng Thu, 13 Jan 2011 14:14:00 GMT

环境: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:

http://tuohuang.thoughtworkers.org/?p=114