From 63cebef0271354b4a634141d79cd72020806607c Mon Sep 17 00:00:00 2001 From: songtianlun Date: Thu, 2 Jan 2025 17:49:06 +0800 Subject: [PATCH] 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. --- app/assets/stylesheets/custom.scss | 13 +++++++++++++ app/controllers/sessions_controller.rb | 2 +- app/helpers/sessions_helper.rb | 7 ++++++- app/models/user.rb | 7 +++++++ app/views/sessions/new.html.erb | 5 +++++ test/helpers/sessions_helper_test.rb | 19 +++++++++++++++++++ test/integration/users_login_test.rb | 11 +++++++++++ test/test_helper.rb | 12 +++++++++++- 8 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 test/helpers/sessions_helper_test.rb diff --git a/app/assets/stylesheets/custom.scss b/app/assets/stylesheets/custom.scss index 3de68a4..c1e3b9c 100644 --- a/app/assets/stylesheets/custom.scss +++ b/app/assets/stylesheets/custom.scss @@ -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; +} diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 1fff20c..bf8fe07 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -8,7 +8,7 @@ class SessionsController < ApplicationController # if user && user.authenticate(params[:session][:password]) if user&.authenticate(params[:session][:password]) reset_session - remember user + params[:session][:remember_me] == '1' ? remember(user) : forget(user) log_in user redirect_to user else diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb index b7cf36c..fc1dcf2 100644 --- a/app/helpers/sessions_helper.rb +++ b/app/helpers/sessions_helper.rb @@ -1,6 +1,8 @@ module SessionsHelper def log_in(user) session[:user_id] = user.id + # 防范会话重放攻击 + session[:session_token] = user.session_token end def remember(user) @@ -11,7 +13,10 @@ module SessionsHelper def current_user 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]) user = User.find_by(id: user_id) if user && user.authenticated?(cookies[:remember_token]) diff --git a/app/models/user.rb b/app/models/user.rb index 2e83727..aec527c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -23,6 +23,13 @@ class User < ApplicationRecord def remember self.remember_token = User.new_token update_attribute(:remember_digest, User.digest(remember_token)) + remember_digest + end + + # 返回一个会话令牌,防止会话劫持 + # 简单起见,直接使用记忆令牌 + def session_token + remember_digest || remember end class << self diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index e19ea07..bd4fe03 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -10,6 +10,11 @@ <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> + <%= f.label :remember_me, class: "checkbox inline" do %> + <%= f.check_box :remember_me %> + Remember me on this computer + <% end %> + <%= f.submit "Log in", class: "btn btn-primary" %> <% end %> diff --git a/test/helpers/sessions_helper_test.rb b/test/helpers/sessions_helper_test.rb new file mode 100644 index 0000000..500b3b8 --- /dev/null +++ b/test/helpers/sessions_helper_test.rb @@ -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 \ No newline at end of file diff --git a/test/integration/users_login_test.rb b/test/integration/users_login_test.rb index 818f3e5..46cfb8b 100644 --- a/test/integration/users_login_test.rb +++ b/test/integration/users_login_test.rb @@ -50,4 +50,15 @@ class UsersLoginTest < ActionDispatch::IntegrationTest assert_select "a[href=?]", logout_path, count: 0 assert_select "a[href=?]", user_path(@user), count: 0 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 diff --git a/test/test_helper.rb b/test/test_helper.rb index 45f3e7e..93c8556 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -17,6 +17,16 @@ module ActiveSupport !session[:user_id].nil? 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