我的Rails应用程序使用Devise进行身份验证.它有一个姐妹iOS应用程序,用户可以使用他们用于Web应用程序的相同凭据登录iOS应用程序.所以我需要某种API进行身份验证.
这里有很多类似的问题指向本教程,但它似乎token_authenticatable
已经过时了,因为该模块已从Devise中删除,并且某些行会抛出错误.(我正在使用Devise 3.2.2.)我试图根据那个教程(和这个教程)推出自己的教程(但是这个),但我对它没有100%的信心 - 我觉得我可能会有一些东西被误解或错过了.
首先,按照这个要点的建议,我authentication_token
在我的users
表中添加了一个text属性,以及以下内容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
然后我有以下控制器:
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_controller
有这条线before_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
在config/routes.rb中:
namespace :api, defaults: { format: "json" } do devise_for :users end
我有点超出了我的深度,我确信这里有一些东西,我的未来自我会回顾并畏缩(通常有).一些不确定的部分:
首先,你会注意到Api::SessionsController
继承自Devise::RegistrationsController
而Api::RegistrationsController
继承自ApiController
(我还有一些其他的控制器,比如Api::EventsController < ApiController
为我的其他模型处理更多标准REST的东西而且与Devise没什么联系.)这是一个非常难看的安排,但我无法找到另一种方法来获取我需要的方法Api::RegistrationsController
.我上面链接的教程有一行include Devise::Controllers::InternalHelpers
,但这个模块似乎已在更新版本的Devise中删除了.
其次,我用线路禁用了CSRF保护skip_before_filter :verify_authentication_token
.我怀疑这是否是一个好主意 - 我看到很多关于JSON API是否容易受到CSRF攻击的冲突或难以理解的建议 - 但添加该行是我能够让该死的工作的唯一方法.
第三,我想确保我了解用户登录后身份验证的工作原理.假设我有一个API调用GET /api/friends
,它返回当前用户的朋友列表.根据我的理解,iOS应用程序必须authentication_token
从数据库中获取用户(这是每个用户永远不会改变的固定值),然后将其作为参数与每个请求一起提交,例如GET /api/friends?authentication_token=abcdefgh1234
,然后我Api::FriendsController
可以做User.find_by(authentication_token: params[:authentication_token])
得到current_user的东西.它真的很简单,还是我错过了什么?
因此,对于那些设法一直读到这个庞大问题结尾的人来说,谢谢你的时间!总结一下:
这个登录系统安全吗?或者是否有一些我忽略或误解的事情,例如在涉及CSRF攻击时?
一旦用户签名正确,我是否了解如何验证请求?(见上文"第三......")
有没有什么办法可以清理或更好的代码?特别是让一个控制器继承而来自Devise::RegistrationsController
其他控制器的丑陋设计ApiController
.
谢谢!
你不想禁用CSRF,我读过人们认为它不适用于JSON API由于某些原因,但这是一个误解.要使其保持启用状态,您需要进行一些更改:
在服务器端向会话控制器添加一个after_filter:
after_filter :set_csrf_header, only: [:new, :create] protected def set_csrf_header response.headers['X-CSRF-Token'] = form_authenticity_token end
这将生成一个令牌,将其放入您的会话中并将其复制到所选操作的响应头中.
客户端(iOS)您需要确保两件事情到位.
您的客户端需要扫描此标头的所有服务器响应,并在传递时保留它.
... 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"];
最后一个难题是要了解在登录设计时,正在使用两个后续会话/ csrf令牌.登录流程如下所示:
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