如题,我用 rails 做服务器端,提供 api,json 通信,但是不开放给第三方,所以没必要 oauth1/2。请问大家,1:移动端的用户登陆以及随后的操作一般是怎么做的?(我没做过自动端,感觉不会是每次发送用户名和密码) 举个例子,用户登陆之后可以写 blog。或者 2:有什么现成的 gem,还是自行处理 token 和 uid?3:只有 token 安全吗?或者怎样才算安全? 三个问题,谢谢大家。
我都是用 OAuth 2 Resource Owner Password Credentials Flow,登录的时候发用户名密码以及 client id、client secret 来直接获取 access token。另外很自然地,用 https 而不是 http。
User Model 是手写的。OAuth 2 Server 用了 doorkeeper
gem。
好处是: 客户端的开发者可用用自己喜欢的 OAuth2 实现,不管是手写还是使用第三方库,都 OK。 服务端也不用去纠结 token 怎么生成怎么过期怎么认证。手写的 User Model 里面用 BCrypt 已经算是通行的 best practice 了。
API Server 这边大致是这样(Rails 4.1.4, Mongoid 4.0.0):
app/models/user.rb
class User
include Mongoid::Document
include Mongoid::Timestamps
include ActiveModel::MassAssignmentSecurity
attr_accessible :email, :password, :password_confirmation
attr_accessible :display_name
attr_accessor :password
validates :password, confirmation: true, on: :create
validates :password_confirmation, presence: true, on: :create
validates :email, presence: true, uniqueness: true, email: true
validates :display_name, presence: true, length: { minimum: 2, maximum: 42}
before_save :encrypt_password
field :email, type: String
field :password_hash, type: String
field :password_salt, type: String
field :display_name, type: String
def self.authenticate!(email, password)
user = find_by(email: email) rescue nil
if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt)
user
else
nil
end
end
def encrypt_password
if password.present?
self.password_salt = BCrypt::Engine.generate_salt
self.password_hash = BCrypt::Engine.hash_secret(password, password_salt)
end
end
end
config/initializers/doorkeeper.rb
module Doorkeeper
class Application
include ActiveModel::MassAssignmentSecurity
end
class AccessGrant
include ActiveModel::MassAssignmentSecurity
end
class AccessToken
include ActiveModel::MassAssignmentSecurity
end
end
Doorkeeper.configure do
# Change the ORM that doorkeeper will use.
# Currently supported options are :active_record, :mongoid2, :mongoid3, :mongo_mapper
orm :mongoid4
resource_owner_from_credentials do
User.authenticate! params[:username], params[:password]
end
native_redirect_uri 'urn:ietf:wg:oauth:2.0:oob'
use_refresh_token
client_credentials :from_basic
access_token_methods :from_bearer_authorization
grant_flows %w(password)
end
刚刚在一个项目中手写了认证,说说自己的感想。用过 Devise 来做认证,但是这个项目不需要邮箱地址,想想也不是特别难,就自己写了。可以去 RailsCasts 上面搜搜,有个 Authentication from Scratch 的视频。其实重点就是has_secure_password
,有了这个,认证什么的基本不是特别难。
由于只是后台 API,为了简单就不需要提供 Session 机制了,我选用的是 Username 和 Token,并且把两者放到 HTTP Header 中,每次需要授权的操作就带上Header['X-Api-Username']
和Header['X-Api-Token']
,然后使用before_action
来做认证。当然,如果你用 Devise 和 Grape 的话,也非常简单。不过基于 Token 的认证,好像已经从 Devise 去掉了,需要自己写。
我个人认为如果维持 Token 的复杂度,是可以在一定程度上保证安全的吧,当然,安全不是绝对的。至于如何生成 Token,我一直使用的是下面的代码:
loop do
token = SecureRandom.hex
break token unless self.class.exists?(auth_token: token)
end
至于 Token 的比较,我抄袭了 Devise 中的一个方法。
def self.secure_compare(a, b)
return false if a.blank? || b.blank? || a.bytesize != b.bytesize
l = a.unpack "C#{a.bytesize}"
res = 0
b.each_byte { |byte| res |= byte ^ l.shift }
res == 0
end
当然,不知道这个方法是否合适,但是目前自己用起来还是 OK 的。
我只去验证 token
额 生成 token 的方法 略简单
def generate_token_and_expired_at
expired_at = 1.month.from_now
token = (Digest::MD5.hexdigest "#{SecureRandom.urlsafe_base64(nil, false)}-#{Time.now.to_i}")
end
Ruby-China?刚看了一个 python 的 app 做法,大致做法是客户端注册时,生成 access_token
access_token = generate_access_token()
def generate_access_token():
# uuid.uuid1().hex => a3541742154011e49beb180373634345
return uuid.uuid1().hex
随后设置过期时间,并保存到 redis,返回给客户端
expire = time.time() + 30 * 24 * 3600
set_access_token(access_token, {'uid': str(uid)})
return {
'access_token': access_token,
'expire': expire
}, 201
ACCESS_TOKEN_KEY_FMT = "auth:access_token:{access_token}"
def set_access_token(access_token, login_info, expire=30 * 24 * 3600):
#' ACCESS_TOKEN_KEY_FMT.format(access_token=access_token) => auth:access_token:a3541742154011e49beb180373634345'
g.memdb.setex(ACCESS_TOKEN_KEY_FMT.format(access_token=access_token), expire, json.dumps(login_info))
登陆是携带 access_token 放入 Authorization Authorization 形如 YWNjZXNzX3Rva2VuPTIwNzMwNDVhZTgyNzExZTM4MTMwYjhlODU2MmFhZGM4 是把 access_token=2073045ae82711e38130b8e8562aadc8 base64.urlsafe_b64encode(b'access_token=2073045ae82711e38130b8e8562aadc8') 后的结果, 服务端先解码,然后解析为数组,取出 access_token,然后从 redis 取出 uid,再从 mongodb 取得用户。定义的是一个 login_required 包裹
def login_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
'''
Check and parse the ``Authorization`` Header to get ``access_token`` and valify it.
If user has logined, then this function will call the handler function directory, or it will
return ``401 Unauthorized``.
'''
reqparser = reqparse.RequestParser()
reqparser.add_argument('Authorization', type=str, required=True, location='headers')
reqargs = reqparser.parse_args()
try:
token_encoded = parse_qs(base64.urlsafe_b64decode(reqargs['Authorization']))
if b'access_token' not in token_encoded:
abort(401, message='Access token is required')
access_token = token_encoded[b'access_token'][0].decode()
except:
abort(401, message='Malformed access token')
login_info = get_access_token(access_token)
if login_info is None:
abort(401, message='Access token is expired')
g.current_uid = ObjectId(login_info['uid'])
return func(*args, **kwargs)
return wrapper
登出代码
class LogoffHandler(restful.Resource):
@login_required
def get(self):
argparser = reqparse.RequestParser()
argparser.add_argument('Authorization', type=str, location='headers')
args = argparser.parse_args()
token_encoded = parse_qs(base64.urlsafe_b64decode(args['Authorization']))
access_token = token_encoded[b'access_token'][0].decode()
del_access_token(access_token)
app.logger.info('User `%s` logout' % str(g.current_uid))
return {}
请问有什么问题吗? 这篇帖子也不错 http://www.cnblogs.com/TankXiao/archive/2012/09/26/2695955.html
#6 楼 @assyer 感觉 ruby-china 的做法还不够,没有过期时间限制。
module RubyChina
module APIHelpers
def warden
env['warden']
end
# topic helpers
def max_page_size
100
end
def default_page_size
15
end
def page_size
size = params[:size].to_i
[size.zero? ? default_page_size : size, max_page_size].min
end
# user helpers
def current_user
token = params[:token] || oauth_token
@current_user ||= User.where(private_token: token).first
end
def oauth_token
# 此处的是为 ruby-china-for-ios 的 token Auth 特别设计的,不是所谓的 OAuth
# 由于 NSRails 没有特别提供独立的 token 参数, 所以直接用 OAuth 那个参数来代替
token = env['HTTP_AUTHORIZATION'] || ""
token.split(" ").last
end
def authenticate!
error!({ "error" => "401 Unauthorized" }, 401) unless current_user
end
end
end
class SessionsController < Devise::SessionsController
def new
super
session['user_return_to'] = request.referrer
end
def create
resource = warden.authenticate!(scope: resource_name, recall: "#{controller_path}#new")
set_flash_message(:notice, :signed_in) if is_navigational_format?
sign_in(resource_name, resource)
resource.ensure_private_token!
respond_to do |format|
format.html { redirect_to after_sign_in_path_for(resource) }
format.json { render status: '201', json: resource.as_json(only: [:login, :email, :private_token]) }
end
end
end
# 重新生成 Private Token
def update_private_token
random_key = "#{SecureRandom.hex(10)}:#{self.id}"
self.update_attribute(:private_token, random_key)
end
楼上各位,其实还有 2 个安全防护点:
初期的产品简单的 token 就足以,然后再去慢慢迭代安全相关的特性
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
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