用户登录Web应用服务器的API身份验证

12

我正在使用Ruby on Rails构建Web应用程序和单独的API(这样用户可以与其他人共享他们收集的数据),用户可以登录Web应用程序并填写数据,这些数据应该发布到API服务器。

根据我目前所了解的,我可以使用基于cookie的身份验证来检查用户是否已登录Web应用程序。现在假设用户想要向API服务器发布数据。由于用户已经通过Web应用程序服务器进行了身份验证,因此应该如何制作帖子请求,以便API知道它正在从已登录的特定用户那里获取数据。此外,如果用户想要获取对他/她私有的API数据,应该如何进行此目的的获取请求?


你的应用程序和API之间有什么关系?你是指针对已登录用户从JavaScript AJAX请求获取JSON数据还是完全分开的事情? - Mike Szyndel
1
我希望API完全独立于Web应用程序。Web应用程序将收集有关已登录用户的一些数据。现在,我想将此数据发送(POST)到Restful API中,其中它将与用户名一起存储。我该如何做才能避免安全问题,并且只有已登录的用户可以发布有关自身的数据? - Peeyush
@chan123你在web应用程序服务器和API中分享你的模型/业务逻辑,是指它们共享同一个数据库吗?还是独立于数据库? - Surya
在API中不应该使用cookies。OAuth旨在解决授权客户端访问资源的问题。我已经提供了一个带有代码示例的答案。 - Andrew Hacking
5个回答

13
您可以考虑使用doorkeeper gem来进行API授权。我曾考虑过它,但由于复杂性和缺乏文档而放弃了,因为在我的用例中无法使其正常工作。简而言之,我无法让其正常工作。
有一篇很好的文章介绍了如何使用Warden进行身份验证,而不需要Devise,这应该能让您了解身份验证系统的关键部分。Devise不适用于API身份验证,事实上,Devise最近删除了可能对API有用的唯一一个东西,即基于令牌的身份验证,显然API不是他们路线图的一部分!
我使用上述引用文章中的指南创建了自己的JSON-only Warden策略,使用OAUTH 2 Owner Password Credentials授权类型(请参见RFC 6749)生成并返回用于以后API请求的Bearertoken。 API客户端可以轻松创建JSON以执行此类身份验证以获取授权访问令牌。
我将提供一些Rails代码供您入门,但您必须将其集成到特定环境中。不提供保修 :)
Warden初始化程序:
# config/initializers/warden.rb
Dir["./app/strategies/warden/*.rb"].each { |file| require file }

Rails.application.config.middleware.insert_after ActionDispatch::ParamsParser, Warden::Manager do |manager|
  manager.default_strategies :null_auth, :oauth_access_token, :oauth_owner_password
  manager.failure_app = UnauthorizedController
end

OAUTH 2密码认证的狱卒策略:

# app/strategies/warden/oauth_owner_password_strategy.rb
module Warden
  class OauthOwnerPasswordStrategy < Strategies::Base
    def valid?
      return false if request.get?

      params['grant_type'] == 'password' && params['client_id'] == 'web' && ! params['username'].blank?
    end

    def authenticate!
      user = User.with_login(params['username']).first
      if user.nil? || user.confirmed_at.nil? || ! user.authenticate!(params['password'])
        # delay failures for up to 20ms to thwart timing based attacks
        sleep(SecureRandom.random_number(20) / 1000.0)
        fail! :message => 'strategies.password.failed'
      else
        success! user, store: false
      end

      # ADD HERE: log IP and timestamp of all authentication attempts
    end
  end

  Strategies.add(:oauth_owner_password, OauthOwnerPasswordStrategy)
end

OAUTH 2访问令牌认证的Warden策略:

# app/strategies/warden/oauth_access_token_strategy.rb
module Warden
  class OauthAccessTokenStrategy < Strategies::Base
    def valid?
      # must be a bearer token
      return false unless auth_header = request.headers['authorization']
      auth_header.split(' ')[0] == 'Bearer'
    end

    def authenticate!
      # Use a periodic cleaner instead
      # clean out all old tokens. DOES NOT RUN CALLBACKS!
      Token.expired.delete

      # lookup bearer token
      token = Token.active.first(purpose: 'access', token: request.headers['authorization'].split(' ')[1])
      if token && (user = token.user) && user.confirmed_at
        success! user, store: false
      else
        # delay failures for up to 20ms to thwart timing based attacks
        sleep(SecureRandom.random_number(20) / 1000.0)
        fail! message: 'strategies.oauth_access_token.failed'
      end
    end
  end

  Strategies.add(:oauth_access_token, OauthAccessTokenStrategy)
end

空认证策略(在开发中可能会有用,只需在config/environments/development.rb中设置config.null_auth_user):

# app/strategies/warden/null_auth_strategy.rb
module Warden
  class NullAuthStrategy < Strategies::Base
    def valid?
      ! Rails.configuration.null_auth_user.blank?
    end

    def authenticate!
      user = User.with_login(params["username"]||Rails.configuration.null_auth_user).first
      if user.nil?
        fail! :message => "strategies.password.failed"
      else
        success! user, store: false
      end
    end
  end

  Strategies.add(:null_auth, NullAuthStrategy)
end

JSON客户端的狱卒失败应用(使用裸金属Rails控制器):
# app/controllers/unauthorized_controller.rb
class UnauthorizedController < ActionController::Metal

  def self.call(env)
    @respond ||= action(:respond)
    @respond.call(env)
  end

  def respond(env)
    self.status = 401
    self.content_type = 'json'
    self.response_body = { 'errors' => ['Authentication failure']}.to_json
  end
end

在您的基本API控制器中添加以下内容:
before_filter :authenticate!

protected

    helper_method :warden, :signed_in?, :current_user

    def warden
      request.env['warden']
    end

    def signed_in?
      !current_user.nil?
    end

    def current_user
      @current_user ||= warden.user
    end

    def authenticate!(*args)
      warden.authenticate!(*args)
      # ADD ANY POST AUTHENTICATION SETUP CODE HERE
    end

一个会话控制器:
class SessionsController < ApiController
  skip_before_filter :authenticate!

  # TODO exceptions and errors should return unauthorized HTTP response.
  # see RFC for details

  def create
    # mandate the password strategy.
    # don't use session store (don't want session cookies on APIs)
    authenticate!(scope: :oauth_owner_password, store: false)

    if signed_in?
      # create access token
      token = Token.create! purpose: 'access',
                            user: current_user,
                            expires_in: Rails.configuration.session_lifetime

       # Ensure response is never cached
       response.headers["Cache-Control"] = "no-store"
       response.headers["Pragma"] = "no-cache"
       response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"

      # send the OAuth response
      render json: {
          access_token: token.token,
          token_type: 'Bearer',
          expires_in: token.expires_in,
          scope: 'user'
      }
    end
  end

  def destroy
    Token.current.delete
    warden.logout
    head :no_content
  end
end

你需要定义自己的用户模型和令牌模型来跟踪用户和承载令牌,令牌模型需要有一个名为 active 的范围来将结果集限制为未过期的令牌。令牌生成应使用 SecureRandom.urlsafe_base64

Andrew,太好了。对于那些寻找示例模型的人来说,Devise用户模型是一个很好的开始(https://github.com/plataformatec/devise/blob/master/lib/generators/active_record/devise_generator.rb),但是一个样本Token模型会真正帮助这个示例。你能包含一个吗,Andrew? - Simon Young
1
我试图保持ORM的中立性,因为并不是每个人(包括我自己)都使用ActiveRecord,但我会在有时间时更新,包括一些Sequel和AR的示例模型,但AR将未经测试。 - Andrew Hacking
@AndrewHacking - 很棒的解决方案!感谢您的发布!在此基础上,您是否介意分享一下您的AR Token模型的示例? - Anconia

10
当你提到Web应用程序服务器和独立的API服务器时,每当Web应用程序服务器上有用户更新时,它们需要相互通信。我建议您将它们分解为3个实体作为rails引擎。
1. 核心:它将保存所有模型和数据逻辑。 2. 应用程序:依赖于核心引擎,并具有面向客户端的代码,主要是控制器和视图。 3. API:再次依赖于核心引擎,并具有处理逻辑,可能是API控制器。
为什么是核心?因为当您需要更新业务逻辑时,只需在一个地方进行:核心引擎。
现在进一步回答您的问题,即如何从Web应用程序服务器对API调用进行身份验证。您需要:
  1. 构建API - Rails CastBuilding Awesome Rails APIS from Collective Idea Blog
  2. 保护API - Rails CastLooking for suggestions for building a secure REST API within Ruby on Rails
  3. 我更喜欢使用OAuth来保护API调用。要在rails中实现OAuth2,您可以使用doorkeeper

完成API保护后,您可以在Web应用程序中实现身份验证逻辑。您可以使用OAuth2从API验证您的应用程序。

也可以通过doorkeeper使你的API仅对使用OAuth调用的用户可用:https://doorkeeper-provider.herokuapp.com/#client-applications 附注:我更喜欢从API中获得json响应,这是一个我认为比较流行的趋势。 ;)
编辑 - postman是一款Chrome扩展程序,可以在实际编写应用程序之前制作实验/虚假API。 这样会更快,因为你最终会知道你必须在一天结束时设计什么。

5
通常它的工作原理如下。您的应用程序为每个用户发行一个秘密令牌(可以是例如md5哈希,它很长且相当随机)。令牌应由用户保管安全。您可以通过遵循两个规则来实现: - 永远不要公开披露令牌(所有请求都应该从后端发起,没有AJAX调用等) - 所有请求都应该通过https进行,以便加密
为什么要使用令牌而不是用户名和密码?如果令牌被破解,您可以撤销它,用户仍然控制着他们的帐户。此外,基于令牌的身份验证可以防止执行某些操作,例如更改与帐户关联的电子邮件或密码。
应该在每个请求中将令牌作为参数传递给您的API。

请问“所有请求都应该从后端发起”是什么意思(有没有相关的资源可以阅读)?另外,如果用户想要允许其他服务提供商访问他的数据,我应该使用OAuth2和API密钥来实现吗? - Peeyush
我的示例中的秘密密钥就像密码一样。而且你不会在公开可用的代码中发布密码,对吧?你可以使用OAuth,但这更加复杂(而且我从未尝试过设置OAuth 2服务器,所以不会假装我知道如何设置)。 - Mike Szyndel

0

如果你想构建基于 JSON、XML 的 API,使用 rabl gem https://github.com/nesquena/rabl

为了更简单的身份验证,可以选择 Rails 提供的基于 session 变量的身份验证。如果你想要一些干净的用户功能,那就选择 authologic gem https://github.com/binarylogic/authlogic

如果你想要完整的用户管理系统,那就选择 devise gem。


-4
Rails使用会话来跟踪用户的状态,这些状态存储在用户的cookie中。
会话的文档可以在这里找到。
如果您利用诸如Devise之类的身份验证系统,则在控制器中将有一个current_user方法以及许多不同的助手可供利用,具体取决于您的特定需求。

它如何回答 OP 的问题? - Raj

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接