四时宝库

程序员的知识宝库

Rails 身份验证和授权初学者指南(rails guides)

介绍

随着我们扩展应用程序开发知识,让我们深入了解 Ruby on Rails 中的身份验证和授权世界。在本指南中,我们将介绍构建安全 Web 应用程序所需的基本概念和工具。身份验证和授权是确保正确的用户访问正确的资源的基础。我们将通过示例探讨 cookie 和会话、用于安全密码处理的 BCrypt、用户注册和登录过程、自动登录机制以及用户注销过程等主题。另外,我们将深入研究授权操作的艺术,以保护后端服务器免受不需要的请求的影响。让我们开始讨论 Web 应用程序开发的这个重要方面!

身份验证与授权

首先我们需要定义身份验证和授权之间的区别。

身份验证是验证用户身份的过程。换句话说,我们正在检查以确保用户的真实身份。

授权是允许某些用户访问应用程序中某些功能的过程。

基本上,身份验证回答“你是谁?”的问题。而授权决定了“你可以做什么?” 任何 Web 应用程序的安全性都在于其有效对用户进行身份验证和授权、保护敏感数据、防止未经授权的访问以及不断适应新出现的威胁的能力。

Cookie 和会话

虽然有很多方法可以对用户进行身份验证,但我们将探索如何通过 cookie 和会话来实现这一点。Cookie 和会话是 Web 开发中用于维护用户状态和增强安全性的基本工具。它们使我们能够在用户访问我们的应用程序的整个过程中跟踪用户数据和交互。

Cookie 是网络服务器发送到用户浏览器(服务器到客户端)的小数据片段。这些数据包是特定于域的,并在用户访问网站或 Web 应用程序后存储在用户的设备上,以便在后续访问时,服务器快速知道客户端是谁。

Cookie 有多种用途,包括存储用户身份验证令牌、购物车内容或用户偏好等信息。它们存储在客户端的浏览器中,并通过每个 HTTP 请求发送回服务器,从而使服务器能够识别并记住用户。在身份验证上下文中,cookie 通常存储用户会话信息,以保持用户在与 Web 应用程序的多次交互中保持登录状态。

会话是一种用于维护用户状态的服务器端机制。与存储在用户设备上的 cookie 不同,会话存储在服务器上。

当用户与 Web 应用程序交互时,唯一的会话标识符通常存储在用户设备上的 cookie 中。该会话标识符允许服务器将来自同一用户的后续请求与其存储在服务器上的会话数据相关联。在身份验证和授权的上下文中,用户会话数据可以包括用户 ID 等信息,这些信息用于确定允许用户在应用程序中执行哪些操作。

这是它的工作原理:

  1. 用户访问网站/应用程序并登录。
  2. 服务器在服务器端生成一个会话,并将包含会话标识符的 cookie 发送到客户端的浏览器。
  3. 客户端的浏览器存储此 cookie。
  4. 在后续请求中,客户端浏览器会自动将存储的 cookie 和会话标识符发送回服务器。
  5. 服务器使用此会话标识符从其会话存储中检索用户的数据并识别用户。

为了在我们的 Rails 应用程序中启用 cookie 和会话,我们需要设置一些东西。

在我们的 config/application.rb 中:

module MyApp
    class Application < Rails::Application
        # Add cookies and session middleware
        config.middleware.use ActionDispatch::Cookies
        config.middleware.use ActionDispatch::Session::CookieStore

        # Use SameSite=Strict for all cookies to help protect against CSRF
        config.action_dispatch.cookies_same_site_protection = :strict

        # Initialize configuration defaults for originally generated Rails version.
        config.load_defaults 6.1

        # Only loads a smaller set of middleware suitable for API only apps.
        # Middleware like session, flash, cookies can be added back manually.
        # Skip views, helpers and assets when generating a new resource.
        config.api_only = true
    end
end

在我们的 app/controllers/application_controller.rb 中:

class ApplicationController < ActionController::API
    include ActionController::Cookies
end

一旦这些文件正确设置,我们就可以使用 cookie 和会话。

BCrypt

当用户首次注册时,服务器必须将该信息存储在其数据库中。然而,需要记住的一个非常重要的概念是,由于安全风险,密码永远不会以纯文本形式保存在任何数据库中。存储密码的标准方法是通过加盐和散列进行加密。

加盐是用于增强密码存储安全性的过程。此过程涉及称为“盐”的随机且独特的数据,它是在应用哈希函数之前生成并与每个密码组合的。这可以确保具有相同密码的两个用户由于唯一的盐而具有不同的值。

散列是一种单向函数,它将密码转换为固定长度的字符串,称为“散列”。密码加盐后,将经历哈希过程。

良好的加密哈希函数的主要特征是它是不可逆的,这意味着您无法逆转该过程来检索原始密码。即使输入数据发生很小的变化,也会产生明显不同的哈希值。在密码安全方面,用户的密码经过加盐和散列处理,然后存储在数据库中。当用户尝试登录时,他们输入的密码将再次进行哈希处理,并将生成的哈希值与数据库中存储的哈希值进行比较。如果它们匹配,则登录尝试成功。

下面是它的工作原理:

  1. 我们有两个用户 John 和 Jane,具有相同的密码“Password123”。
  2. 当他们注册时,他们会获得两种独特的盐:约翰获得“sd9f6”,简获得“3x41p”。
  3. 加盐过程将 John 的密码变成“Password123sd9f6”,Jane 的密码变成“Password1233x41p”。
  4. 通过安全散列函数的散列过程将 John 的加盐密码转换为“d3f4e5g6h7i8j9k0”,将 Jane 的密码转换为“m1n2o3p4q5r6s7t8”。
  5. 服务器存储 John 的盐“sd9f6”和哈希密码“d3f4e5g6h7i8j9k0”以及 Jane 的盐“3x41p”和哈希密码“m1n2o3p4q5r6s7t8”。
  6. 当 John 或 Jane 登录时,盐和哈希函数会将他们输入的密码转换为哈希版本。如果哈希密码匹配,则登录成功。

为了简化 Web 开发人员的这个过程,我们可以使用 Ruby gem BCrypt。BCrypt 提供了has_secure_passwordUser 模型的方法。这个方法:

  • 自动对用户密码进行加盐和哈希处理
  • 自动password_confirmation向模型添加属性(用于注册和更新密码)。
  • 自动添加与密码相关的输入的验证,例如密码的存在和任何其他指定的要求(最小长度、所需字符等)
  • 提供authenticate会话控制器的方法。

has_secure_password所有这些功能都可以在用户模型中包含该方法后使用。

class User < ApplicationRecord
    has_secure_password
end

在继续之前我们需要做的最后一件事是将password_digest属性作为字符串添加到我们的用户模型中。这是我们存储加盐和哈希密码的地方。一旦我们包含此属性,BCrypt 就会自动以安全的方式管理密码。

注册

要在 Rails 中设置注册逻辑,我们首先需要在 config/routes.rb 中创建适当的路由。

Rails.application.routes.draw do
    # signing up
    post '/signup', to: 'users#create'
end

由于我们正在创建一个新的 User 实例,因此我们需要有指向控制器操作的路由users#create。

class UsersController < ApplicationController
    # post '/signup'
    def create
        render json: User.create!(user_params), status: :created
    rescue ActiveRecord::RecordInvalid => e
        render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
end

    private

    def user_params
        params.require(:user).permit(:username, :password, :password_confirmation)
    end
end

在我们的users#create行动中,我们:

  1. 创建 User 模型的新实例并使用姊妹方法将其保存到我们的数据库中create!()。这允许 Rails 捕获我们的rescue子句引发的任何异常。
  2. 利用在私有方法下定义的强参数。我们确保在允许的参数中包含:password和属性。:password_confirmation
  3. 确保包含 以status: :created使浏览器知道用户已成功创建。
  4. 包括rescue ActiveRecord::RecordInvalid => e捕获任何无效记录。我们以散列形式呈现此错误消息并设置status: :unprocessable_entity.

如果我们想让用户在创建新帐户后自动登录,我们需要在后端设置登录机制,并在前端 fetch post 请求后创建一个新会话。

登录

与注册过程一样,我们首先需要为此控制器操作创建一条新路由。

Rails.application.routes.draw do
    # signing up
    post '/signup', to: 'users#create'
    # logging in
    post '/login', to: 'sessions#create'
end


这次,我们指向我们的会话控制器。请记住,当用户登录时,他们必须请求服务器创建一个新会话,他们可以将其存储在服务器端,并在收到带有会话标识符的 cookie 时进行引用。因此,我们的控制器操作将是sessions#create。

class SessionsController < ApplicationController
    # post '/login'
    def create
        user = User.find_by(username: params[:username])

        if user&.authenticate(params[:password])
            session[:user_id] = user.id
            render json: user, status: :created
        else
            render json: { error: 'Invalid username or password' }, status: :unauthorized
        end
    end
end

在我们的sessions#create行动中,我们:

  1. 使用.find_by()我们的 User 模型上的方法通过用户名查找适当的用户。
  2. 使用该方法设置条件user&.authenticate(params[:password])。这是“如果对象存在,则调用其方法”&.的简写方式。否则,就返回。”userauthenticate(params[:password])nil
  3. 如果条件为 true,则使用键 :user_id 和设置为实际用户 ID (user.id) 的值创建新的会话哈希。该哈希值通常表示会话令牌或标识符,这对于后续经过身份验证的请求至关重要。
  4. 如果条件为 true,则呈现状态为::created 的响应。该响应通常包含与会话相关的信息。
  5. status: :unauthorized如果条件为 false,则呈现错误消息“无效的用户名或密码” 。

设置此控制器操作后,每当需要创建会话(登录)时,我们就可以从前端发出 fetch post 请求。如果我们设置了注册逻辑,前端通常会自动登录,因此前端也可以在注册过程结束时利用此获取请求。

自动登录

当用户重新加载页面或关闭并重新打开浏览器时,它通常会启动一个新的浏览会话。这意味着先前会话中的会话 cookie 不再可用,并且服务器无法将新请求与旧会话关联。此功能允许用户通过页面重新加载来维护其登录状态,而不是让用户每次都输入其凭据。

和以前一样,我们首先为新的控制器操作创建一条新路线。

Rails.application.routes.draw do
    # signing up
    post '/signup', to: 'users#create'
    # logging in
    post '/login', to: 'sessions#create'
    # auto login
    get '/me', to: 'users#show'
end

这次,我们指向该users#show操作,因为我们正在寻找现有的用户实例。

class UsersController < ApplicationController
    # post '/signup'
    def create
        render json: User.create!(user_params), status: :created
    rescue ActiveRecord::RecordInvalid => e
        render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
    end

    # get '/me'
    def show
        render json: User.find_by!(id: session[:user_id])
    rescue ActiveRecord::RecordNotFound
        render json: { error: 'Not authorized' }, status: :unauthorized
    end

    private

    def user_params
        params.require(:user).permit(:username, :password, :password_confirmation)
    end
end

在我们的users#show行动中,我们:

  1. 使用该find_by!(id: session[:user_id])方法通过 id 查找现有用户。
  2. status: :unauthorized如果用户未经过身份验证(如果会话已过期或用户未登录),则呈现错误消息。

通过此设置,我们确保前端在启动时触发对此路由的 fetch get 请求。在 React 上,这可以使用useEffect(() => {}, []).

一旦我们能够从后端检索用户信息,我们就可以正确设置前端来保存用户状态。在 React 上,可以通过useContext()在整个组件中保留这些数据来完成此操作。

注销

由于我们在登录时创建了一个新会话,因此我们需要删除该会话[:user_id]以进行注销。

Rails.application.routes.draw do
    # signing up
    post '/signup', to: 'users#create'
    # logging in
    post '/login', to: 'sessions#create'
    # auto login
    get '/me', to: 'users#show'
    # logging out
    delete '/logout', to: 'sessions#destroy'
end
class SessionsController < ApplicationController
    # post '/login'
    def create
        user = User.find_by(username: params[:username])

        if user&.authenticate(params[:password])
            session[:user_id] = user.id
            render json: user, status: :created
        else
            render json: { error: 'Invalid username or password' }, status: :unauthorized
        end
    end

    # delete '/logout'
    def destroy
        session.delete :user_id
        head :no_content
    end
end

在我们的sessions#destroy行动中,我们:

  1. 使用 .delete(:user_id) 方法删除会话[:user_id]。
  2. 发送一个空head的:no_content.

授权

为了添加另一层后端路由安全性,我们希望将对大多数控制器操作的访问限制为那些获得授权的人。要限制所有控制器操作,我们必须使用在应用程序控制器中创建自定义方法before_action。

class ApplicationController < ActionController::API
    include ActionController::Cookies
    before_action :authorize

    def authorize
        return render json: { error: "Not authorized" }, status: :unauthorized unless session.include? :user_id
    end
end

每当用户在登录前尝试访问控制器操作时,此自定义方法authorize都会发送错误消息“未授权”。但是,有一些操作必须免受此限制。

如果我们考虑一下,我们需要允许用户能够注册并登录,而不用此方法阻止他们。因此,我们需要为这些行为制定豁免。这是我们最终的用户和会话控制器应该是什么样子的。

class UsersController < ApplicationController
    skip_before_action :authorize, only: [:create]

    # post '/signup'
    def create
        render json: User.create!(user_params), status: :created
    rescue ActiveRecord::RecordInvalid => e
        render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
    end

    # get '/me'
    def show
        render json: User.find_by!(id: session[:user_id])
    rescue ActiveRecord::RecordNotFound
        render json: { error: 'Not authorized' }, status: :unauthorized
    end

    private

    def user_params
        params.require(:user).permit(:username, :password, :password_confirmation)
    end
end
class SessionsController < ApplicationController
    skip_before_action :authorize, only: [:create]

    # post '/login'
    def create
        user = User.find_by(username: params[:username])

        if user&.authenticate(params[:password])
            session[:user_id] = user.id
            render json: user, status: :created
        else
            render json: { error: 'Invalid username or password' }, status: :unauthorized
        end
    end

    # delete '/logout'
    def destroy
        session.delete(:user_id)
        head(:no_content)
    end
end

这skip_before_action允许我们从自定义方法中免除注册和登录操作authorize。

结论

我们探讨了 Web 应用程序安全性中身份验证和授权的关键概念。我们已经认识到区分这两个过程的重要性,确保只有授权用户才能访问受保护的资源,同时允许高效的身份验证过程。Cookie 和会话被视为安全管理用户会话的工具。此外,还讨论了使用 BCrypt 进行密码加密,以增强应用程序的安全性。通过实施这些实践并了解他们的角色,开发人员可以构建强大且安全的 Web 应用程序,保护用户数据和隐私。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接