認證系統之登入認證系統的進階使用 (二)
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外掛。
可以研究其背後是如何實現的。
相關文章
- 建站篇-使用者認證系統-自定義登入系統
- DRF內建認證元件之自定義認證系統元件
- 【登陸認證】oracle的作業系統認證和口令檔案認證方式(轉載)Oracle作業系統
- 【Python】Django--認證系統-登入註冊PythonDjango
- Ubuntu部署Maxkey單點登入認證系統Ubuntu
- URL載入系統之四:認證與TLS鏈驗證TLS
- 系統登入認證流程對比(cookie方式與jwt)CookieJWT
- 在windows透過作業系統認證登入ORACLEWindows作業系統Oracle
- 在windows通過作業系統認證登入ORACLEWindows作業系統Oracle
- 禁用作業系統認證作業系統
- Oracle 作業系統認證Oracle作業系統
- Oracle作業系統認證Oracle作業系統
- 系統多種使用者角色認證登入授權如何實現?
- Django 使用者認證系統:註冊Django
- HTTP認證之基本認證——Basic(二)HTTP
- 【連線】禁止以作業系統認證方式登入資料庫作業系統資料庫
- 網路認證計費系統
- 建站篇-使用者認證系統-管理員登陸後臺
- 作業系統認證與ORACLE密碼檔案認證方式作業系統Oracle密碼
- 基於 Laravel Passport API 的多使用者多欄位認證系統(二):多使用者登入LaravelPassportAPI
- Redis使用認證密碼登入Redis密碼
- React & Redux 實現註冊登入認證系統(31 個視訊)ReactRedux
- Django使用者認證系統(一)User物件Django物件
- express實現JWT使用者認證系統ExpressJWT
- 建站篇-使用者認證系統-開始
- Django 使用者認證系統:基本設定Django
- 寬頻認證計費系統的認證技術主要有哪些
- 4種微服務系統認證策略微服務
- 統一身份認證系統的簡單看法
- [Laravel 8 使用者認證] Jetstream 之 如何自定義登入驗證Laravel
- 自定義Django認證系統的技術方案Django
- Go gRPC進階-TLS認證+自定義方法認證(七)GoRPCTLS
- Flask Session 登入認證模組FlaskSession
- passport API 認證 -- 多表登入PassportAPI
- 1.6.4.2. 準備作業系統認證作業系統
- JAVA 多使用者商城系統b2b2c-SSO單點登入之OAuth2.0登入認證(1)JavaOAuth
- 為什麼無線認證系統能被人們認可
- KubeSphere 使用 OpenLDAP 進行統一認證完全指南LDA