OmniAuth库

Jun 4, 2017


在介绍Ruby的OmniAuth库之前,有必要提OAuth(开放授权)这个开放标准,OAuth允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容。

这篇文章不会对OAuth标准做详细介绍,如果感兴趣,可以查看OAuth2.0简介

那OmniAuth库是什么呢,来个官方的英文定义:

OmniAuth is a library that standardizes multi-provider authentication for web applications.

这里的provider可以是facebook, github, qq, weibo, wechat,也可以是自定义的一个provider。一般来说,当你的应用中引入了用户OmniAuth并配置了相应的provider后,只需要访问/auth/:provider, OmniAuth就可以根据指定的策略去做验证了,譬如打开一个QQ登陆的页面。

但是用户登陆后,下一步是做什么呢?OmniAuth提供了一个给定的url/auth/:provider/callback来作为登陆后的callback。但OmniAuth并没有实现这个接口,需要用户自己定义这个接口,并在routes.rb中定义相应route,如:

get '/auth/:provider/callback', to: 'sessions#create'

问题: 回调URL/auth/:provider/callback是否需要在provider侧手动配置?

于是在自定义的SessionsController中我们就从request中获取’omniauth.auth’: request.env['omniauth.auth']

omniauth.auth中存储的是uid(unique id), 个人信息等,关于详细的可以存储的信息了参考Auth Hash Schema wiki page

OmniAuth库只是修改了request, 没有其他动作,如并不考虑如何自动的把验证后的omniauth.auth的信息与你项目中的User关联起来,不过你只管拿到信息后去用就可以了。

如果在provider端验证失败,OmniAuth将获取response并将请求重定向到/auth/failure。通过params[:message]可以获取到错误信息。


与Rails做集成

配置provider

在Rails的initializers创建omniauth的配置,如config/initializers/omniauth.rb,配置内容:

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :developer unless Rails.env.production?
  provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET']
end

这里的TWITTER_KEYTWITTER_SECRET是需要手动去twitter申请的。

另这里的配置相对简单,但是实际上不用的provider可以配置不同的options,可以参考provider提供的文档,目前omniauth库支持的这些provider的列表

如[twitter]https://github.com/arunagw/omniauth-twitter 可以配置为:

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter, "API_KEY", "API_SECRET",
    {
      :secure_image_url => 'true',
      :image_size => 'original',
      :authorize_params => {
        :force_login => 'true',
        :lang => 'pt'
      }
    }
end

配置session

如果在Rails API中使用OmniAuth, 必须选用如下任一中间件:

  • ActionDispatch::Session::CacheStore

  • ActionDispatch::Session::CookieStore

  • ActionDispatch::Session::MemCacheStore

这些中间件已经默认被Rails加载了,但是这里可能会遇到的一个问题是,session options不其作用,复现此问题的配置是:

  • 只是在initializer中增加session_store.rb,设置session相关参数

  • 在application.rb中增加use ActionDispatch::Session::CookieStore

问题的原因是Rails在use ActionDispatch::Session::CookieStore时,session_options将被传递进应用。所以上述配置下,Session功能虽然正常,但是session_options(如session key将默认使用_session_id)不会生效,解决办法是:

  • 在application.rb中中间件引用之前,设置相关options,如
config.session_store :cookie_store, key: '_interslice_session'
config.middleware.use ActionDispatch::Cookies # Required for all session management
config.middleware.use ActionDispatch::Session::CookieStore, config.session_options

设置logger

默认OmniAuth打印log到STDOUT,可以配置其为

OmniAuth.config.logger = Rails.logger

Rails API + devise + devise_token_auth

关于使用后devise_token_auth后需要做的配置,可以参考devise_token_auth#configuration, 创建相应user表、model、routes配置等。

另初始化devise_token_auth的配置config/initializers/omniauth.rb:

DeviseTokenAuth.setup do |config|
  config.change_headers_on_each_request = false
  config.omniauth_prefix = "/api/omniauth"
end

这里的config.omniauth_prefix配置的是callback地址的前缀,所以callback地址不在是前面提到的/auth/:provider/callback,而变成了/api/omniauth/:provider/callback。所以你在provider端也需要手动配置这个callback地址,以QQ为例,在QQ开发平台上,需要修改应用开发地址(应用实际调用地址)为http://[YOU_APP_ADDRESS]/omniauth/qq_connect/callback

如果验证成功,调用的逻辑会回到DeviseTokenAuth::ApplicationController的omniauth_success方法,此方法的源码如:

def omniauth_success
  get_resource_from_auth_hash
  create_token_info
  set_token_on_resource
  create_auth_params

  if resource_class.devise_modules.include?(:confirmable)
    # don't send confirmation email!!!
    @resource.skip_confirmation!
  end

  sign_in(:user, @resource, store: false, bypass: false)

  @resource.save!

  yield @resource if block_given?

  render_data_or_redirect('deliverCredentials', @auth_params.as_json, @resource.as_json)
end

get_resource_from_auth_hash中从获取的验证回复中uidprovider参数并在数据库中查找数据或创建一条记录:

def get_resource_from_auth_hash
  # find or create user by provider and provider uid
  @resource = resource_class.where({
    uid:      auth_hash['uid'],
    provider: auth_hash['provider']
  }).first_or_initialize

  if @resource.new_record?
    @oauth_registration = true
    set_random_password
  end

  .....

  @resource
end

可以看到,其调用了@resource.save!,即验证成功后,就在数据库的user表(概念上的user表,可以是你程序中定义的各种model) 中存储get_resource_from_auth_hash中创造的数据。

yield @resource if block_given?中可以在自定义OmniauthCallbacksController覆盖omniauth_success方法时调用

super do
....
end


如何集成测试

使用OmniAuth自带的方式

设置OmniAuth测试模式:

OmniAuth.config.test_mode = true

测试模式下OmniAuth将使用Mock的验证过程,发往/auth/:provider的请求将被直接重定向到/auth/:provider/callback。如果使用了devise_token_auth,则是先重定向/omniauth/:provider/callback,而后重定向auth/:provider/callback

代码逻辑将走到OmniauthCallbacksController中。

如何Mock验证过程成功的response呢,对OmniAuth.config.mock_auth进行赋值即可。

OmniAuth.config.mock_auth[:twitter] = OmniAuth::AuthHash.new({
  :provider => 'twitter',
  :uid => '123545'
  # etc.
})

而Mock验证过程失败的response:

OmniAuth.config.mock_auth[:twitter] = :invalid_credentials

发往/auth/:provider的请求将重定向到/auth/failure?message=invalid_credentials

参考资料