世界上最伟大的投资就是投资自己的教育
认证系统之登录认证系统的进阶使用 (二)
1.如何思考
突然有一天,你在一个项目中,老板给你一个需求,你需要在后台登录系统中,添加超时的功能,所谓超时,就是管理员登录超过一定时间后,访问页面时就会自动要求其注销,并要重新登录。这个需求是符合逻辑的,因为,管理员总有离开电脑的时候,离开后回来要求其输入密码重新登录,这也是为了安全。或者说,另一个需求是这样的。假如有人写一些机器人程序来枚举你的用户名和密码,一般来说,很多网站,或许都有 admin 用户,或者这样说,攻击者事先知道了一些用户,那它就可以写脚本,来枚举你的用户名和密码,刚好你的密码很简单,说不定就给破解了。这个时候有个解决方法,当然未必是最好的,但有时候很适合,也很有效,就是像银行卡账号那样,输错固定次数的密码就把账号锁定。真正要解锁就得通过客服或者固定时间后自动解释。这样攻击的次数就有限了,由于有锁定,就算固定时间后解锁,一天内再怎么用机器人,次数也是被限制得很少。
或许你就摊上了这样的任务。或许你刚好是新手,面对这些问题无从下手,不知所措。有时候 google 也很难搜出答案。或许我能给你思路,你就搜搜看有没有类似的 gem 来解决这个问题。有的话如果合适就直接用,没有呢。其实 devise 就有这样的功能,但是你项目不一定用啊。这个时候,你就可以去研究 devise 的源码抽出那个功能。其实这样很慢的,因为 devise 源码你要从头研究是需要时间的,项目需求可不等人。一般来说,好的源码都是低耦合的,模块化的。你就找到相应的代码,能理解就好了。通过优秀项目的源码去找解决方案,是很有好处,不仅能学习好的代码和设计思想,也能让你走不少弯路。
2.具体实例
在 devise 的官方 github 库 readme 文档中就列出了 devise 默认的 10 个 module 的名字和说明了。我们挑上面所讲的两个来说一下。第一个是Timeoutable
,另一个是Lockable
,我们先来说Timeoutable
。
2.1 Timeoutable
首先,要在 devise 使用Timeoutable
也是很简单的。在 wiki 中就可以找到一篇文章就是说明怎么用它的。
How-To:-Add-timeout_in-value-dynamically。
其实很简单,就一个方法,用在 model 上的,例如 user.rb
def timeout_in
30.minutes
end
这样就好了,30 分钟后退出,简单明了,一切搞定。
好吧。如果我们要自己实现呢。
你翻看 devise 的源码就可以发现,它的所有 module 的功能都是分开放在一起的。就在这里lib/devise/models
找到 timeoutable.rb 这个文件,打开来看看。
没多少东西,我复制其中较为重要的三个方法
def timedout?(last_access)
return false if remember_exists_and_not_expired?
!timeout_in.nil? && last_access && last_access <= timeout_in.ago
end
def timeout_in
self.class.timeout_in
end
private
def remember_exists_and_not_expired?
return false unless respond_to?(:remember_created_at) && respond_to?(:remember_expired?)
remember_created_at && !remember_expired?
end
就是那个timeout_in
啦,我们在 model 用的就是它。它不过就是定义超时的时间罢了,真正发挥作用的是 timedout?方法,判断是否超时的,看该方法最后一行last_access && last_access <= timeout_in.ago
last_access 就是最后访问的意思嘛,最后访问的时间跟 timeout_in 前的时间比,大概这样,例如,最后访问的时间跟现在时间的 20 分钟之前相比,自己具体想一下就清楚啦。这个就是主要逻辑。具体使用 timedout?这个方法的代码在这里timeoutable.rb
大概看一下就好了。
具体的逻辑总结一下就是,最后一次访问的时间,跟当前时间的规定时间之前相比,例如,当前时间的二十分钟之前相比,就能判断是否超时啦。不管怎样,你就是要不断地存当前的时间,才能和当前时间的二十分钟之前相比。每访问一次就存一次。那就存 session 再加上一个 before_action 放 application_controller.rb 就好了。
可以这样做。
def expire_user_session
return if ! current_user
if session[:last_active_at].present? && session[:last_active_at].to_time < 30.minutes.ago
logout
redirect_to login_path, notice: '登录超时,请重新登录'
return
end
session[:last_active_at] = TimeCalculator.current_time
end
具体地自己慢慢领悟吧。
2.1 Lockable
这里有一篇关于 Lockable 的文章 how-to-lock-users-using-devise
先看一下,接下来,清空你的脑袋,思考一下。
假如就 5 次输错密码自动锁定。那总得有一个字段来保存用户输错的次数吧。输错 1 次要存数据库,2 次也存,到 5 次时,就得把用户锁定。还有,假设半个钟后解除锁定。那总得存锁定的时间吧,才好和现在时间进行比较,看是不是真的超过了半个钟。有存了锁定的时间,也就是证明被锁定了。
还是跟上面一样的分析方法,我在代码上加上注释,自己慢慢分析吧。学习在个人。
module Lockable
def self.included(base)
base.include InstanceMethods
base.class_eval do
class_attribute :maximum_attempts, :unlock_in
# 最多4次输错机会,每5次输错之后就会锁定账号
self.maximum_attempts = 5
# 设定30分钟后自动解锁
self.unlock_in = 30.minutes
end
end
module InstanceMethods
# 锁定
def lock_access!
self.locked_at = TimeCalculator.current_time
save(validate: false)
end
# 解锁
def unlock_access!
self.locked_at = nil
self.failed_attempts = 0
save(validate: false)
end
# 认证的逻辑
def authenticate(unencrypted_password)
if BCrypt::Password.new(password_digest).is_password?(unencrypted_password)
unlock_access! if lock_expired?
true
else
self.failed_attempts ||= 0
self.failed_attempts += 1
if attempts_exceeded?
lock_access! unless access_locked?
else
save(validate: false)
end
false
end
end
# 判断是否被锁定中
def access_locked?
locked_at.present? && !lock_expired?
end
# 判断是否是最后一次输错密码
def last_attempt?
self.failed_attempts == self.class.maximum_attempts - 1
end
# 判断是否到了最大输错密码的次数
def attempts_exceeded?
self.failed_attempts >= self.class.maximum_attempts
end
# 还没被锁定,但是输错过密码
def attempts_dirty?
!access_locked? && self.failed_attempts > 0
end
protected
# 锁定时间是否过期
def lock_expired?
locked_at && locked_at < self.class.unlock_in.ago
end
end
end # Lockable
以上就讲两个,其他的自己研究就好了。
3.各种 devise 插件
下面介绍几个 devise 的插件,我们的目的,是通过插件的用法或源码来学习代码之外的思想和知识。
3.1 devise-encryptable
这个是什么插件,为什么选择这个呢。这个 gem 是增强密码用的,选择它的理由有二,第一,它足够简单,第二,可以学习一些加密的技巧。
对于开发人员来说,一个常识就是,存用户的登录密码总不是明文存储的,除非那些不保护用户隐私,不负责任的网站。总得选择一种加密算法,把用户输入的密码加密成密文之后再存进数据库。而且就算用户得到了密文也不能推导出原来的密码,这才是比较好的加密算法。md5 是一种方案,不过单纯地用这种方法,在一定条件下,也是能根据密文推导出原来的密码。它的是原理是这样, 把原来的密码根据 hash 算法,生成固定长度的字符串,也就是说,你原来的密码是什么 ,就一定会生成同样的密文。假如,有人事先通过,把一些常见单词加上用 md5 加密后的密文存进数据库,你的密码刚好又是这些常见单词 (总有人这么干的),攻击者,通过匹配就能轻易获取你的密码。再说,你用 google 搜索一下 md5,就能发现各种加密解密 md5 的网站。一般来说,md5 常用来验证文件是否修改过。例如一些开源软件的下载,都有附带 md5 文件,让你验证该文件是否被修改过。通过下载后的文件的 md5 值和下载的 md5 文件的码来对是否被修改过。rails 中的编译过后的 application.js 和 application.css 后面就有附带 md5 值。这只简单了解一下。如果要求比较安全,md5 不适合来加密密码。那 devise 是如何做的呢。看这里database_authenticatable.rb
我也不都列出来,就列出来其中关键的三个方法。
# Generates password encryption based on the given value.
# 生成密文
def password=(new_password)
@password = new_password
self.encrypted_password = password_digest(@password) if @password.present?
end
# Verifies whether a password (ie from sign in) is the user password.
# 验证密码
def valid_password?(password)
Devise::Encryptor.compare(self.class, encrypted_password, password)
end
# Digests the password using bcrypt. Custom encryption should override
# this method to apply their own algorithm.
#
# See https://github.com/plataformatec/devise-encryptable for examples
# of other encryption engines.
# 产生密文的算法
def password_digest(password)
Devise::Encryptor.digest(self.class, password)
end
其实很简单,数据表中有encrypted_password
这个字段,用Devise::Encryptor.digest
加密用户输入的原密码后存入数据库表中。主要就是Devise::Encryptor.digest
这个方法的逻辑。具体可以看这里encryptor.rb了一下
我们来看 devise-encryptable 这个 gem 是做啥的
devise 是默认用一个字段来存加密后的密文。但这个是加了另一个字段 password_salt,这是一个加密领域算法的词,叫 salt,中文名可以叫盐。
原来也很简单,不是说,像 md5 之类的东西 ,可以通过枚举破解吗,那好,我的原文密码和存到数据库中的 salt 混合之后再加密存到密文中。这样就比单一的加密好多了点,毕竟你要枚举就要多考虑一个中间因素,而这个因素是变化的。因为 slat 是随机生成的。假如你的密码就是 123456,存到数据库的密文是 xxxx,刚好很简单就给枚举到了,但有 salt 就不一样了,你要加上 salt,也就是 123456 + salt 混合之后去枚举,由于 salt 是随机的,并且是存到数据库中的,你不可能知道,所以是枚举不到的。
这个 gem 既然是增强的功能,它也是重写了 devise 的加密代码的部分,还是我们之前说了,混合 salt 再加密,gem 的源码也很简单,也就几个文件,对比 devise,我列出四个方法
def password=(new_password)
self.password_salt = self.class.password_salt if new_password.present?
super
end
# Validates the password considering the salt.
def valid_password?(password)
return false if encrypted_password.blank?
encryptor_class.compare(encrypted_password, password, self.class.stretches, authenticatable_salt, self.class.pepper)
end
def password_digest(password)
if password_salt.present?
encryptor_class.digest(password, self.class.stretches, authenticatable_salt, self.class.pepper)
end
end
def authenticatable_salt
self.password_salt
end
一眼就能看出吧,慢慢体会。
现在推荐几个 devise 插件。
可以研究其背后是如何实现的。
本站文章均为原创内容,如需转载请注明出处,谢谢。
© 汕尾市求知科技有限公司 | Rails365 Gitlab | 知乎 | b 站 | csdn
粤公网安备 44152102000088号 | 粤ICP备19038915号
Top