环境: 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, 是不是没想到啊?
情况是这样的, 公司内部测试服务器经常需要更新代码供测试人员使用网站, 每次都是我们后端开发人员部署的, 这样就加大了工作量,效率低下,话说我们部署也是使用capistrano 的, 只需一条命令就可以顺利部署, 但是还是不如非开发人员部署来的方便,于是就有了 rake_ui
rake_ui gem 是我发布的,但是是在修改别人代码的基础上发布的,下面介绍使用方法:
首先看下效果图:
1, 环境
Node.js Socket.io Rails 3.x
2,Gemfile
gem 'rake_ui', '0.6.0'
3, 在你的 routes.rb 中添加路由
Rails.application.routes.draw do mount RakeUi::Engine => "/rake_ui" end
4, 配置 config/rake_ui.yml
host: '192.168.10.107' log: '/data/projects/entos/log/rake.log'
host是你的ip地址,Nodejs 要用, log 是你项目下log目录下rake.log 会被自动创建
5, 配置 config/tasks.yml
- 'rake about' - 'rake routes'
把你需要执行的rake任务写在这个 yaml 中
6, 启动 nodejs server
rake start_node_server
ok, 你可以访问 /rake_ui 看到你的 web gui 界面了, 把你的部署方案写在rake任务中, 然后在这个界面可以点击部署.
该gem有可能被更新,看到最新的说明请移到步这里:
最近需要为一个站集成 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!!!
核心使用 task 指令 实现多机部署
# encoding:utf-8 # >cap local deploy # >cap remote deploy set :application, "entos" set :deploy_to, "/data/projects/entos" set :scm, "git" set :repository, "git@114.255.155.167:entos.git" set :branch, "master" set :use_sudo, false set :rails_env,"production" task :remote do set :user, "entsea" set :deploy_via, :remote_cache set :copy_exclude, %w(external) server "114.255.155.166", :web, :app, :db, :primary => true end task :local do set :user, 'zzq' set :deploy_via, :remote_cache set :copy_exclude, %w(external) server '192.168.10.105', :web, :app, :db, :primary => true end namespace :deploy do task :start do; end task :stop do; end desc "Creating ln -s , example: database.yml" task :create_sync do run "ln -s #{shared_path}/config/database.yml #{current_path}/config/database.yml" end desc "Restarting unicorn" task :restart, :roles => :app, :except => { :no_release => true } do # run "/bin/sh restart_server.sh" end end after "deploy:symlink", "deploy:create_sync"
环境: 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
环境:ruby 1.9.2 + rails 3.0.3 + ubuntu 10.10
params在rails中很常用,特别在表单提交的时候,params 产生的是一个Hash ,里面构造通过 form域的name构造 ,产生不同的 params 内容,今天 在看rails params 实现的时候 发现通过 attr_internal 的方法实现,params方法 的源码:
def params @_params ||= request.parameters end
发现其实是从 request 这个方法得到的,那么request方法又是怎么定义的:
attr_internal :headers, :response, :request
就是 用了 attr_internal 方法
看下 整个 metal.rb文件: here
发现了 response,headers,session(借助delegate委派) ,status,params == 都是通过 attr_internal 实现的,来看看 attr_internal 到底是何须人也 :
源码: here
class Module # Declares an attribute reader backed by an internally-named instance variable. def attr_internal_reader(*attrs) attrs.each do |attr| module_eval "def #{attr}() #{attr_internal_ivar_name(attr)} end", __FILE__, __LINE__ end end # Declares an attribute writer backed by an internally-named instance variable. def attr_internal_writer(*attrs) attrs.each do |attr| module_eval "def #{attr}=(v) #{attr_internal_ivar_name(attr)} = v end", __FILE__, __LINE__ end end # Declares an attribute reader and writer backed by an internally-named instance # variable. def attr_internal_accessor(*attrs) attr_internal_reader(*attrs) attr_internal_writer(*attrs) end alias_method :attr_internal, :attr_internal_accessor class << self; attr_accessor :attr_internal_naming_format end self.attr_internal_naming_format = '@_%s' private def attr_internal_ivar_name(attr) Module.attr_internal_naming_format % attr end end
发现其实就是通过 module_eval 给 对象 添加了 settet , getter 方法而已,但是命名格式是这样的:
self.attr_internal_naming_format = '@_%s'
DEMO:
require "active_support/core_ext/module/attr_internal" class Foo attr_accessor :sex,:birthday # attr_accessor ruby里封装的method attr_internal :name,:city # attr_internal rails 封装的 def bar name # call getter method # => @_name end end f = Foo.new f.name = 'wxianfeng' p f.instance_variables # => [:@_name] p f.name # => "wxianfeng" p f # => #<Foo:0x8630e18 @_name="wxianfeng"> p f.bar # => "wxianfeng"
所以 attr_internal 和 attr_accessor 其实是 等价的,只不过 从字面意思上看是内部变量(闭包变量的写法) ,attr_internal 希望你 通过方法名来调用,不用 @_%s 这个写法 来调用
所以 其实 一般我们在 controller 用的 request 方法 其实 可以直接这样写 @_request ,
request #=> @_request params # => @_request.parameters params # => @_params headers #=> @_headers status #=> @_status . . .
但是一般 不建议这样写
还发现 这些和 http相关的东西都定义在 metal 模块, metal 是 rails 链接 rack 的中间件,源码中的解释:
ActionController::Metal provides a way to get a valid Rack application from a controller.
Rack 是一个 ruby实现的web server,封装了 http的请求和响应等,例如 rails,sinatra == 都是在 rack 基础上实现的……
有机会很有必要 深入学习下…
SEE:
http://rubyonrailswin.wordpress.com/2007/03/07/actioncontroller-and-what-the-heck-is-attr_internal/
http://www.oschina.net/p/rack
最近部署一个项目,采用的是centos + nginx+ passenger
发现rails project不打log, 是文件权限问题,passenger 规定文件权限不能是root ,如果你部署在ubuntu就不会有这个问题
passenger典型部署结构
lecai- |-- current |-- releases |-- shared
把根目录lecai的权限改了即可
>useradd deploy >chown -R deploy:deploy lecai
时间: 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 是表名