我很难理解并在API中正确实施 用户身份验证 。换句话说,我很难理解Grape API与Backbone.js,AngularJS或Ember.js等前端框架的集成。
我正在试图调整所有不同的方法,并阅读了很多相关内容,但谷歌给我提供了真正糟糕的资源,在我看来,就像这篇主题没有真正好的文章一样 - Rails和用户身份验证使用Devise和前端框架。
我将描述我目前的支点,我希望你能就我的实施提供一些反馈,并指出我正确的方向。
当前实施
我有 Gemfile 后端 Rails REST API (我会故意缩短所有文件代码)
gem 'rails', '4.1.6'
gem 'mongoid', '~> 4.0.0'
gem 'devise'
gem 'grape'
gem 'rack-cors', :require => 'rack/cors'
我目前的实施只包含以下路线的API( routes.rb ):
api_base /api API::Base
GET /:version/posts(.:format)
GET /:version/posts/:id(.:format)
POST /:version/posts(.:format)
DELETE /:version/posts/:id(.:format)
POST /:version/users/authenticate(.:format)
POST /:version/users/register(.:format)
DELETE /:version/users/logout(.:format)
我创建了以下型号 user.rb
class User
include Mongoid::Document
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
field :email, type: String, default: ""
field :encrypted_password, type: String, default: ""
field :authentication_token, type: String
before_save :ensure_authentication_token!
def ensure_authentication_token!
self.authentication_token ||= generate_authentication_token
end
private
def generate_authentication_token
loop do
token = Devise.friendly_token
break token unless User.where(authentication_token: token).first
end
end
end
在我的控制器中,我创建了以下文件夹结构: controllers-> api-> v1 ,我创建了以下共享模块身份验证( authentication.rb )
module API
module V1
module Authentication
extend ActiveSupport::Concern
included do
before do
error!("401 Unauthorized", 401) unless authenticated?
end
helpers do
def warden
env['warden']
end
def authenticated?
return true if warden.authenticated?
params[:access_token] && @user = User.find_by(authentication_token: params[:access_token])
end
def current_user
warden.user || @user
end
end
end
end
end
end
因此,每当我想确保使用身份验证令牌调用我的资源时,我只需通过调用:include API::V1::Authentication
添加到Grape资源即可添加:
module API
module V1
class Posts < Grape::API
include API::V1::Defaults
include API::V1::Authentication
现在我有另一个名为Users(users.rb)的Grape资源,在这里我实现了身份验证,注册和注销的方法。(我想我在这里混合了梨和苹果,我应该将登录/注销过程提取到另一个葡萄资源 - 会议)。
module API
module V1
class Users < Grape::API
include API::V1::Defaults
resources :users do
desc "Authenticate user and return user object, access token"
params do
requires :email, :type => String, :desc => "User email"
requires :password, :type => String, :desc => "User password"
end
post 'authenticate' do
email = params[:email]
password = params[:password]
if email.nil? or password.nil?
error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
return
end
user = User.find_by(email: email.downcase)
if user.nil?
error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
return
end
if !user.valid_password?(password)
error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
return
else
user.ensure_authentication_token!
user.save
status(201){status: 'ok', token: user.authentication_token }
end
end
desc "Register user and return user object, access token"
params do
requires :first_name, :type => String, :desc => "First Name"
requires :last_name, :type => String, :desc => "Last Name"
requires :email, :type => String, :desc => "Email"
requires :password, :type => String, :desc => "Password"
end
post 'register' do
user = User.new(
first_name: params[:first_name],
last_name: params[:last_name],
password: params[:password],
email: params[:email]
)
if user.valid?
user.save
return user
else
error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
end
end
desc "Logout user and return user object, access token"
params do
requires :token, :type => String, :desc => "Authenticaiton Token"
end
delete 'logout' do
user = User.find_by(authentication_token: params[:token])
if !user.nil?
user.remove_authentication_token!
status(200)
{
status: 'ok',
token: user.authentication_token
}
else
error!({:error_code => 404, :error_message => "Invalid token."}, 401)
end
end
end
end
end
end
我意识到我在这里提供了大量的代码,它可能没有意义,但这是我目前所拥有的,并且我能够使用authentication_token
来调用受模块保护的API Authentication
。
我觉得这个解决方案并不好,但我真的在寻找更简单的方法来通过API实现用户身份验证。我有几个问题,我在下面列出。
问题
Token
的实现。但我认为这几乎就像重新发明轮子一样,因为为此我可以实现OAuth2。我想有更轻松的解决方案。我为这么长的帖子道歉,但我希望更多的人有同样的问题,这可能会帮助我找到更多关于我的问题的答案。
答案 0 :(得分:21)
添加token_authenticable来设计模块(使用设计版本&lt; = 3.2)
在user.rb中添加:token_authenticatable到设计模块列表,它应该如下所示:
class User < ActiveRecord::Base
# ..code..
devise :database_authenticatable,
:token_authenticatable,
:invitable,
:registerable,
:recoverable,
:rememberable,
:trackable,
:validatable
attr_accessible :name, :email, :authentication_token
before_save :ensure_authentication_token
# ..code..
end
自行生成身份验证令牌(如果设计版本> 3.2)
class User < ActiveRecord::Base
# ..code..
devise :database_authenticatable,
:invitable,
:registerable,
:recoverable,
:rememberable,
:trackable,
:validatable
attr_accessible :name, :email, :authentication_token
before_save :ensure_authentication_token
def ensure_authentication_token
self.authentication_token ||= generate_authentication_token
end
private
def generate_authentication_token
loop do
token = Devise.friendly_token
break token unless User.where(authentication_token: token).first
end
end
为真实令牌添加迁移
rails g migration add_auth_token_to_users
invoke active_record
create db/migrate/20141101204628_add_auth_token_to_users.rb
编辑要添加的迁移文件:authentication_token列给用户
class AddAuthTokenToUsers < ActiveRecord::Migration
def self.up
change_table :users do |t|
t.string :authentication_token
end
add_index :users, :authentication_token, :unique => true
end
def self.down
remove_column :users, :authentication_token
end
end
运行迁移
rake db:migrate
为现有用户生成令牌
我们需要在每个用户实例上调用save,以确保每个用户都有身份验证令牌。
User.all.each(&:save)
使用身份验证令牌保护Grape API
您需要将以下代码添加到API :: Root中,以添加基于令牌的身份验证。如果您不了解API :: Root,请阅读Building RESTful API using Grape
在下面的示例中,我们基于两种情况对用户进行身份验证 - 如果用户登录到Web应用程序然后使用相同的会话 - 如果会话不可用并且传递了身份验证令牌,则根据令牌查找用户
# lib/api/root.rb
module API
class Root < Grape::API
prefix 'api'
format :json
rescue_from :all, :backtrace => true
error_formatter :json, API::ErrorFormatter
before do
error!("401 Unauthorized", 401) unless authenticated
end
helpers do
def warden
env['warden']
end
def authenticated
return true if warden.authenticated?
params[:access_token] && @user = User.find_by_authentication_token(params[:access_token])
end
def current_user
warden.user || @user
end
end
mount API::V1::Root
mount API::V2::Root
end
end
答案 1 :(得分:1)
虽然我喜欢@MZaragoza给出的问题和答案,但我认为值得注意的是,token_authentical已经从Devise中删除了一个原因!使用令牌很容易受到定时攻击。另见this post和Devise's blog因此,我没有赞成@ MZaragoza的回答。
如果您将API与Doorkeeper结合使用,您可以执行类似操作,但不是在User表/模型中检查authentication_token,而是在OauthAccessTokens表中查找令牌,即
def authenticated
return true if warden.authenticated?
params[:access_token] && @user = OauthAccessToken.find_by_token(params[:access_token]).user
end
这更安全,因为该令牌(即实际的access_token)仅存在一段时间。
请注意,为了能够执行此操作,您必须具有User模型和OauthAccessToken模型,并且:
class User < ActiveRecord::Base
has_many :oauth_access_tokens
end
class OauthAccessToken < ActiveRecord::Base
belongs_to :user, foreign_key: 'resource_owner_id'
end
编辑:另请注意,通常您不应在网址中包含access_token:http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-16#section-2.3