My Rails app uses Devise for authentication. It has a sister iOS app, and users can log in to the iOS app using the same credentials that they use for the web app. So I need some kind of API for authentication.

这里有很多类似的问题指向this tutorial,但是它似乎已经过时了,因为token_authenticatable模块已经从Devise中删除了,并且一些行抛出了错误.(我使用的是Devise 3.2.2.)我试着根据该教程(和this one)推出我自己的教程,但我对此并不是百分之百有信心-我觉得可能有什么地方我误解了或错过了.

首先,按照this gist的建议,我在users表中添加了authentication_token文本属性,并在user.rb表中添加了以下内容:

before_save :ensure_authentication_token

def ensure_authentication_token
  if authentication_token.blank?
    self.authentication_token = generate_authentication_token
  end
end

private

  def generate_authentication_token
    loop do
      token = Devise.friendly_token
      break token unless User.find_by(authentication_token: token)
    end
  end

Then I have the following controllers:

api_controller.rb

class ApiController < ApplicationController
  respond_to :json
  skip_before_filter :authenticate_user!

  protected

  def user_params
    params[:user].permit(:email, :password, :password_confirmation)
  end
end

(请注意,我的application_controllerbefore_filter :authenticate_user!行.)

api/sessions_controller.rb

class Api::SessionsController < Devise::RegistrationsController
  prepend_before_filter :require_no_authentication, :only => [:create ]

  before_filter :ensure_params_exist

  respond_to :json

  skip_before_filter :verify_authenticity_token

  def create
    build_resource
    resource = User.find_for_database_authentication(
      email: params[:user][:email]
    )
    return invalid_login_attempt unless resource

    if resource.valid_password?(params[:user][:password])
      sign_in("user", resource)
      render json: {
        success: true,
        auth_token: resource.authentication_token,
        email: resource.email
      }
      return
    end
    invalid_login_attempt
  end

  def destroy
    sign_out(resource_name)
  end

  protected

    def ensure_params_exist
      return unless params[:user].blank?
      render json: {
        success: false,
        message: "missing user parameter"
      }, status: 422
    end

    def invalid_login_attempt
      warden.custom_failure!
      render json: {
        success: false,
        message: "Error with your login or password"
      }, status: 401
    end
end

api/registrations_controller.rb

class Api::RegistrationsController < ApiController
  skip_before_filter :verify_authenticity_token

  def create
    user = User.new(user_params)
    if user.save
      render(
        json: Jbuilder.encode do |j|
          j.success true
          j.email user.email
          j.auth_token user.authentication_token
        end,
        status: 201
      )
      return
    else
      warden.custom_failure!
      render json: user.errors, status: 422
    end
  end
end

And in config/routes.rb:

  namespace :api, defaults: { format: "json" } do
    devise_for :users
  end

我有点不知所措,我相信这里有一些东西我future 的self 会回顾和畏缩(通常是有的).一些不确定的部分:

Firstly,你会注意到Api::SessionsControllerDevise::RegistrationsController继承,而Api::RegistrationsControllerApiController继承(我还有一些其他控制器,比如Api::EventsController < ApiController,它为我的其他模型处理更多标准的REST内容,与Deave没有太多联系.)这是一个相当丑陋的安排,但我想不出另一种方法来获得我在Api::RegistrationsController中需要的方法.我在上面链接的教程有第include Devise::Controllers::InternalHelpers行,但这个模块似乎在Desive的最新版本中被删除了.

Secondly, I've disabled CSRF protection with the line skip_before_filter :verify_authentication_token. I have my doubts about whether this is a good idea - I see a lot of conflicting or hard to understand advice about whether JSON APIs are vulnerable to CSRF attacks - but adding that line was the only way I could get the damn thing to work.

Thirdly, I want to make sure I understand how authentication works once a user has signed in. Say I have an API call GET /api/friends which returns a list of the current user's friends. As I understand it, the iOS app would have to get the user's authentication_token from the database (which is a fixed value for each user that never changes??), then submit it as a param along with every request, e.g. GET /api/friends?authentication_token=abcdefgh1234, then my Api::FriendsController could do something like User.find_by(authentication_token: params[:authentication_token]) to get the current_user. Is it really this simple, or am I missing something?

所以,对于那些一直阅读到这个庞大问题结尾的人来说,谢谢你们的时间!总结如下:

  1. Is this login system secure?或是否有我忽略或误解的东西,例如,当涉及到CSRF攻击时?
  2. Is my understanding of how to authenticate requests once users are signed in correct?(见"第三……"上图.)
  3. Is there any way this code can be cleaned up or made nicer? Particularly the ugly design of having one controller inherit from Devise::RegistrationsController and the others from ApiController.

谢谢!

推荐答案

You don't want to disable CSRF, I have read that people think it doesn't apply to JSON APIs for some reason, but this is a misunderstanding. To keep it enabled, you want to make a few changes:

  • 在服务器端,向会话控制器添加一个after_过滤器:

    after_filter :set_csrf_header, only: [:new, :create]
    
    protected
    
    def set_csrf_header
       response.headers['X-CSRF-Token'] = form_authenticity_token
    end
    

    This will generate a token, put it in your session and copy it in the response header for selected actions.

  • client side (iOS) you need to make sure two things are in place.

    • 您的客户端需要扫描此标头的所有服务器响应,并在传递时保留它.

      ... get ahold of response object
      // response may be a NSURLResponse object, so convert:
      NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
      // grab token if present, make sure you have a config object to store it in
      NSString *token = [[httpResponse allHeaderFields] objectForKey:@"X-CSRF-Token"];
      if (token)
         [yourConfig setCsrfToken:token];
      
    • 最后,您的客户端需要将此内标识添加到它发出的所有"非GET"请求中:

      ... get ahold of your request object
      if (yourConfig.csrfToken && ![request.httpMethod isEqualToString:@"GET"])
        [request setValue:yourConfig.csrfToken forHTTPHeaderField:@"X-CSRF-Token"];
      

Final piece of the puzzle is to understand that when logging in to devise, two subsequent sessions/csrf tokens are being used. A login flow would look like this:

GET /users/sign_in ->
  // new action is called, initial token is set
  // now send login form on callback:
  POST /users/sign_in <username, password> ->
    // create action called, token is reset
    // when login is successful, session and token are replaced 
    // and you can send authenticated requests

Json相关问答推荐

Google Page to JSON应用程序脚本出现CORS错误

您可以使用Jolt对JSON执行OR条件吗

使用Kotlin限制序列化类属性的允许整数值

Jolt-Json转换:通过引用标识符(而不是索引)设置值

小写嵌套特定键的震动转换

如何将该 JSON 解析为 Kotlin 类?

为什么根据其他工具,来自 aws rds 的 JSON 在 Docker 中格式错误运行?

如何在 Dart 中与多个 map (字典)相交

在 Flutter 中将对象转换为可编码对象失败

Retrofit 2.0 如何解析嵌套的 JSON 对象?

Rails:format.js 或 format.json,或两者兼而有之?

如何使用 Jackson 定义可选的 json 字段

Jackson Scala 模块的小例子?

使用绝对或相对路径在 curl 请求中发送 json 文件

如何向从文件中检索的 JSON 数据添加键值?

JSON Schema:验证数字或空值

有什么方法可以判断您安装的 gulp 版本吗?

如何在 Python 中合并两个 json 字符串?

从调试器获取 IntelliJ Idea 中的 JSON 对象

在 iPhone 上解析 JSON 日期