feat: add remember me functionality to login

- Implement remember me checkbox in login form
- Update sessions controller to handle remember me logic
- Enhance session management to prevent session hijacking
- Add tests for remember me functionality

This commit introduces a "Remember me" feature that allows users to
stay logged in across sessions. It includes updates to the login
form, session handling in the controller, and additional tests to
ensure the functionality works as expected. The changes also
improve security by validating session tokens to prevent session
hijacking.
This commit is contained in:
songtianlun 2025-01-02 17:49:06 +08:00
parent f110f26c0b
commit 63cebef027
8 changed files with 73 additions and 3 deletions

View File

@ -174,3 +174,16 @@ input {
} }
} }
.checkbox {
margin-top: -10px;
margin-bottom: 10px;
span {
margin-left: 20px;
font-weight: normal;
}
}
#session_remember_me {
width: auto;
margin-left: 0;
}

View File

@ -8,7 +8,7 @@ class SessionsController < ApplicationController
# if user && user.authenticate(params[:session][:password]) # if user && user.authenticate(params[:session][:password])
if user&.authenticate(params[:session][:password]) if user&.authenticate(params[:session][:password])
reset_session reset_session
remember user params[:session][:remember_me] == '1' ? remember(user) : forget(user)
log_in user log_in user
redirect_to user redirect_to user
else else

View File

@ -1,6 +1,8 @@
module SessionsHelper module SessionsHelper
def log_in(user) def log_in(user)
session[:user_id] = user.id session[:user_id] = user.id
# 防范会话重放攻击
session[:session_token] = user.session_token
end end
def remember(user) def remember(user)
@ -11,7 +13,10 @@ module SessionsHelper
def current_user def current_user
if (user_id = session[:user_id]) if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id) user = User.find_by(id: user_id)
if user && session[:session_token] == user.session_token
@current_user = user
end
elsif (user_id = cookies.encrypted[:user_id]) elsif (user_id = cookies.encrypted[:user_id])
user = User.find_by(id: user_id) user = User.find_by(id: user_id)
if user && user.authenticated?(cookies[:remember_token]) if user && user.authenticated?(cookies[:remember_token])

View File

@ -23,6 +23,13 @@ class User < ApplicationRecord
def remember def remember
self.remember_token = User.new_token self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token)) update_attribute(:remember_digest, User.digest(remember_token))
remember_digest
end
# 返回一个会话令牌,防止会话劫持
# 简单起见,直接使用记忆令牌
def session_token
remember_digest || remember
end end
class << self class << self

View File

@ -10,6 +10,11 @@
<%= f.label :password %> <%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %> <%= f.password_field :password, class: 'form-control' %>
<%= f.label :remember_me, class: "checkbox inline" do %>
<%= f.check_box :remember_me %>
<span>Remember me on this computer</span>
<% end %>
<%= f.submit "Log in", class: "btn btn-primary" %> <%= f.submit "Log in", class: "btn btn-primary" %>
<% end %> <% end %>

View File

@ -0,0 +1,19 @@
require 'test_helper'
class SessionsHelperTest < ActionView::TestCase
def setup
@user = users(:michael)
remember(@user)
end
test "current_user returns right user when session is nil" do
assert_equal @user, current_user
assert is_logged_in?
end
test "current_user returns nil when remember digest is wrong" do
@user.update_attribute(:remember_digest, User.digest(User.new_token))
assert_nil current_user
end
end

View File

@ -50,4 +50,15 @@ class UsersLoginTest < ActionDispatch::IntegrationTest
assert_select "a[href=?]", logout_path, count: 0 assert_select "a[href=?]", logout_path, count: 0
assert_select "a[href=?]", user_path(@user), count: 0 assert_select "a[href=?]", user_path(@user), count: 0
end end
test "login with remembering" do
log_in_as(@user, remember_me: '1')
assert_not cookies[:remember_token].blank?
end
test "login without remembering" do
log_in_as(@user, remember_me: '1')
log_in_as(@user, remember_me: '0')
assert cookies[:remember_token].blank?
end
end end

View File

@ -17,6 +17,16 @@ module ActiveSupport
!session[:user_id].nil? !session[:user_id].nil?
end end
# Add more helper methods to be used by all tests here... # def log_in_as(user)
# session[:user_id] = user.id
# end
end
class ActionDispatch::IntegrationTest
def log_in_as(user, password: 'password', remember_me: '1')
post login_path, params: { session: { email: user.email,
password: password,
remember_me: remember_me } }
end
end end
end end