0

我正在开发一个项目,该项目分为两个应用程序: - 一个处理数据库并将数据呈现为 JSON 的 rails JSON API - 一个“前端” rails 应用程序在需要时向 API 发送请求,并且以一种很好的方式显示 json 数据。

API 的身份验证是基于令牌的,gem'simple_token_authentication'这意味着对于发送到 API 的大多数请求,您必须在请求头中发送用户令牌和他的电子邮件才能获得授权。

在我之前从事该项目的人还在 API 端安装了 Devise 身份验证系统,以允许在使用电子邮件和密码成功登录后从导航器直接访问 API 方法。

我刚刚开始在应该请求 API 的“前端应用程序”上进行编码,但我遇到了麻烦,尤其是在身份验证系统方面。

由于 Devise 已经安装在 API 上,我认为让用户登录前端应用程序是一个好主意,然后请求 API 上的设计方法用于创建用户、身份验证、重置密码......

问题是 devise 的方法是渲染 html 而不是 JSON,所以我实际上不得不覆盖大多数 devise 的控制器。让您快速了解它的工作原理:您在前端应用程序上填写注册表单,然后将参数发送到前端应用程序控制器,然后在 API 上请求设计的注册用户方法:

1)前端应用控制器:

  def create
    # Post on API to create USER
    @response =   HTTParty.post(ENV['API_ADDRESS']+'users',
        :body => { :password => params[:user][:password],
                   :password_confirmation => params[:user][:password_confirmation],
                   :email => params[:user][:email]
                 }.to_json,
        :headers => { 'Content-Type' => 'application/json' })
    # si le User est bien crée je récupère son email et son token, je les store en session et je redirige vers Account#new
    if user_id = @response["id"]
      session[:user_email] = @response["email"]
      session[:user_token] = @response["authentication_token"]
      redirect_to new_account_path
    else
      puts @response
      @errors = @response["errors"]
      puts @errors
      render :new
    end
  end 

2)API覆盖的设计控制器:

class RegistrationsController < Devise::RegistrationsController
  def new
    super
  end

  def create
    @user = User.new(user_params)
    if @user.save
      render :json => @user
    else
      render_error
    end
  end

  def update
    super
  end

  private

  def user_params
    params.require(:registration).permit(:password, :email)
  end

  def render_error
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
  end
end

这工作正常。在这里,我将刚刚在 API 上创建的用户作为 JSON 发回,我将身份验证令牌和他的电子邮件存储在会话哈希中。

我的问题是我试图重用一些设计代码的 reset_password 方法。首先,我要求重置密码,这将为请求更改的用户生成重置密码令牌。这还会向用户生成一封电子邮件,其中包含指向特定用户的重置密码表单的链接(内部带有令牌)。这运作良好。我收到电子邮件中的链接,然后转到我的前端应用程序上的 edit_password 表单:

更改您的密码

<form action="/users/password" method='post'>
  <input name="authenticity_token" value="<%= form_authenticity_token %>" type="hidden">
  <%= hidden_field_tag "[user][reset_password_token]", params[:reset_password_token] %>
   <%=label_tag "Password" %>
  <input type="text" name="[user][password">
  <%=label_tag "Password Confirmation" %>
  <input type="text" name="[user][password_confirmation]">
  <input type="Submit" value="change my password">
</form>

提交表单后,它会通过我的前端应用程序控制器:

  def update_password
      @response =   HTTParty.patch(ENV['API_ADDRESS']+'users/password',
        :body => {
          :user => {
            :password => params[:user][:password],
            :password_confirmation => params[:user][:password_confirmation],
            :reset_password_token => params[:user][:reset_password_token]
          }
                 }.to_json,
        :headers => { 'Content-Type' => 'application/json' })
  end

然后调用我重写的 Devise::PasswordController (update method) :

# app/controllers/registrations_controller.rb
class PasswordsController < Devise::RegistrationsController
   # POST /resource/password
  def create
    if resource_params[:email].blank?
      render_error_empty_field and return
    end
    self.resource = resource_class.send_reset_password_instructions(resource_params)
    yield resource if block_given?

    if successfully_sent?(resource)
      render_success
    else
      render_error
    end
  end

  def update
    self.resource = resource_class.reset_password_by_token(resource_params)
    yield resource if block_given?

    if resource.errors.empty?
      resource.unlock_access! if unlockable?(resource)
      render_success
    else
      render_error
    end
  end


  private

  # TODO change just one big method render_error with different cases

  def render_success
    render json: { success: "You will receive an email with instructions on how to reset your password in a few minutes." }
  end

  def render_error
    render json: { error: "Ce compte n'existe pas." }
  end
  def render_error_empty_field
    render json: { error: "Merci d'entrer un email" }
  end
end

但是该请求始终是 Unauthorized :

Started PATCH "/users/password" for ::1 at 2016-02-05 11:28:30 +0100
Processing by PasswordsController#update as HTML
  Parameters: {"user"=>{"password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "reset_password_token"=>"[FILTERED]"}, "password"=>{"user"=>{"password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "reset_password_token"=>"[FILTERED]"}}}
Completed 401 Unauthorized in 1ms (ActiveRecord: 0.0ms)

我不明白为什么最后一个请求未经授权?

4

1 回答 1

0

您的前任可能只是为了方便而在 API 方面搞得一团糟。

我们知道将 cookie 用于 API 是一个非常糟糕的主意,因为它为 CSRF/XSRF 攻击敞开了大门。

我们不能对 API 使用 Rails CSRF 保护,因为它只能保证请求来自我们自己的服务器。并且只能在您自己的服务器上使用的 API 并不是很有用。

默认情况下,Devise 使用基于 cookie 的身份验证策略,因为这适用于基于 Web 的应用程序,而 Devise 就是让基于 Web 的应用程序中的身份验证变得容易。

所以你应该做的是要么从 API 应用程序中完全删除 Devise,要么将 Devise 转换为使用基于令牌的策略。您还应该考虑从API 应用程序中删除会话中间件。此外,Devise 控制器非常倾向于客户端交互,因此试图将它们击败为 API 控制器将会非常混乱。

在 API 中更新密码只是:

class API::V1::Users::PasswordsController
  before_action :authenticate_user!
  def create
    @user = User.find(params[:user_id])
    raise AccessDenied unless @user == current_user
    @user.update(password: params[:password])
    respond_with(@user)
  end
end

这是一个非常简化的示例 - 但关键是,如果您从控制器中去除与表单/闪烁和重定向相关的所有垃圾,那么您实际上不会重复使用那么多。

如果您的前端应用程序是“经典”客户端/服务器 Rails 应用程序,那么您可以使用基于常规 cookie 的身份验证(设计)并让它与 API 应用程序共享数据库。由于其无状态特性,基于令牌的身份验证不适用于经典客户端/服务器应用程序。

如果前端应用程序是 Angular 或 Ember.js 之类的 SPA,您可能需要考虑使用Doorkeeper设置您自己的 OAuth 提供程序。

服务图

于 2016-02-05T11:31:32.873 回答