使用 JWT 进行 Rails 身份验证的完整指南 Rails JWT 身份验证

2025-06-07

使用 JWT 进行 Rails 身份验证的完整指南

Rails JWT 身份验证

Rails JWT 身份验证

JSON Web Token(JWT)是一个 JSON 对象,用于在双方之间安全地传输信息。JWT 广泛用于在 REST API 中从客户端安全地验证和授权用户。在本文中,我将逐步讲解如何在 Rails API 中使用 JWT 实现身份验证。

我们需要的宝石:



gem 'bcrypt', '~> 3.1', '>= 3.1.12’

gem 'jwt', '~> 2.5

gem 'rack-cors'

gem 'active_model_serializers', '~> 0.10.12’


Enter fullscreen mode Exit fullscreen mode

添加 gemfile 运行后bundle install

创建路线



  post "/users", to: "users#create"
  get "/me", to: "users#me"
  post "/auth/login", to: "auth#login"


Enter fullscreen mode Exit fullscreen mode

我们将通过向 /users 发送 POST 请求来注册新用户。现有用户可以通过向 “/auth/login” 发送 POST 请求来登录,而新用户可以通过向 “/me” 发送 GET 请求来访问用户数据。我们至少需要 3 个路由,后续可以添加更多路由。

添加 CORS



Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end


Enter fullscreen mode Exit fullscreen mode

跨域资源共享 (CORS) 是一个中间件,它只接受来自一个客户端 URL 的 API 请求。我们希望允许发出请求的客户端 URL 将被添加到 中origins。目前,我们设置了 * origins,这意味着任何人都可以向我们的 API 发出请求。

创建用户模型:



rails g model user username password_digest bio --no-test-framework


Enter fullscreen mode Exit fullscreen mode

has_secure_password在用户模型中添加宏并验证用户名:



class User < ApplicationRecord
    has_secure_password
    validates :username, uniqueness: true
end


Enter fullscreen mode Exit fullscreen mode

has_secure_password是一个 bcrypt 方法,用于加密每个用户的密码。为了使此方法有效,我们password_digest在数据库表中添加了一个字段。但是,当我们向服务器发出 post 请求时,我们会发送password。剩下的事情由 Bcrypt 来处理。

示例请求:



fetch('URL/auth/login',{
method: POST,
headers: {
    'Content-type': 'application/json'
},
body: {
    username: 'randomUserName',
    password: 'ask^dsk34'
})



Enter fullscreen mode Exit fullscreen mode

将 JWT 添加到我们的 API

JSON Web Tokens 是一种开放的、符合行业标准的 RFC 7519 方法,用于在双方之间安全地表示声明。JWT Tokens 如下所示:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

图片描述

来源-JWT.io

它由三部分组成。第一部分是标头,包含算法和令牌类型。第二部分是有效负载,即我们想要存储在令牌中的数据。第三部分是签名,其中包含密钥。我们将从 application_controller.rb 生成 JWT 令牌。



class ApplicationController < ActionController::API

    def encode_token(payload)
        JWT.encode(payload, 'hellomars1211') 
    end

    def decoded_token
        header = request.headers['Authorization']
        if header
            token = header.split(" ")[1]
            begin
                JWT.decode(token, 'hellomars1211')
            rescue JWT::DecodeError
                nil
            end
        end
    end

end


Enter fullscreen mode Exit fullscreen mode

encode_token方法将有效载荷作为参数。我们将用户 ID 作为有效载荷传递。然后调用该JWT.encode(payload, 'hellomars1211')方法对令牌进行编码。有效载荷将是用户 ID,然后我们可以使用它来查找正确的用户。请注意,'hellomars1211'除了有效载荷之外,我们还传递了一个字符串:作为参数,该字符串将是我们的密钥,我们也将使用它来解码令牌。密钥可以是字符、符号、数字等的任意组合。我们将调用该decoded_token方法对 JWT 令牌进行解码。

每当我们向受保护的路由或资源发出请求时,我们都会将 JWT 令牌连同请求中的数据一起传递到 Authorization 标头中,并使用 Bearer 模式。以下是一个示例请求:



fetch("URL/me", {
  method: "GET",
  headers: {
    Authorization: `Bearer <token>`,
  },
});


Enter fullscreen mode Exit fullscreen mode

图片描述

我们从标头访问令牌并使用JWT.decode(token, 'hellomars1211', true, algorithm: 'HS256')方法解码令牌,我们还需要传递密钥才能解码令牌。

现在,我们可以user.id从解码后的令牌中访问了。我们将创建一个方法current_user,从解码后的令牌中获取用户 ID,并使用相同的用户 ID 查找用户。这将返回当前已登录的用户。我们将创建另一个方法authorized来检查 current_user 是否已登录。



def current_user 
    if decoded_token
        user_id = decoded_token[0]['user_id']
        @user = User.find_by(id: user_id)
    end
end

def authorized
    unless !!current_user
    render json: { message: 'Please log in' }, status: :unauthorized
    end
end


Enter fullscreen mode Exit fullscreen mode

最后,我们将向before_action控制器添加一条规则,该规则将authorized在执行任何操作之前调用该方法并检查用户是否已登录。对于任何未经授权的请求,我们将呈现一条消息:Please log in。这就是我们的 application_controller 的样子:



*app/controllers/application_controller.rb*

class ApplicationController < ActionController::API
    before_action :authorized

    def encode_token(payload)
        JWT.encode(payload, 'hellomars1211') 
    end

    def decoded_token
        header = request.headers['Authorization']
        if header
            token = header.split(" ")[1]
            begin
                JWT.decode(token, 'hellomars1211', true, algorithm: 'HS256')
            rescue JWT::DecodeError
                nil
            end
        end
    end

    def current_user 
        if decoded_token
            user_id = decoded_token[0]['user_id']
            @user = User.find_by(id: user_id)
        end
    end

    def authorized
        unless !!current_user
        render json: { message: 'Please log in' }, status: :unauthorized
        end
    end

end


Enter fullscreen mode Exit fullscreen mode

创建 users_controller



rails g controller users


Enter fullscreen mode Exit fullscreen mode

在用户控制器中,我们将创建一个用于创建或注册新用户的操作。如果用户使用有效数据注册,我们还将创建一个令牌,并将其与响应一起发送,这将使用户在注册我们的应用时立即登录。我们将在 users_controller.rb 中的 create 方法中处理注册功能。



class UsersController < ApplicationController
    rescue_from ActiveRecord::RecordInvalid, with: :handle_invalid_record

    def create 
        user = User.create!(user_params)
        @token = encode_token(user_id: user.id)
        render json: {
            user: UserSerializer.new(user), 
            token: @token
        }, status: :created
    end

    private

    def user_params 
        params.permit(:username, :password, :bio)
    end

    def handle_invalid_record(e)
            render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
    end
end


Enter fullscreen mode Exit fullscreen mode

我们将数据序列化,只返回用户 ID、用户名和个人简介。将密码返回给客户端毫无意义,而且密码在数据库中是加密的。



class UserSerializer < ActiveModel::Serializer
  attributes :id, :username, :bio
end


Enter fullscreen mode Exit fullscreen mode

现在,还记得我们before_action向 application_controller 添加规则的时候吗?如果我们已经登录,这将阻止我们创建新用户。但这没有任何意义。我们甚至还没有注册,又该如何登录呢?或者,如果我们根本无法创建新用户,又该如何登录呢?好吧,为了绕过授权,我们将向skip_before_action用户控制器添加一个,并仅对该create方法设置例外。这样,如果我们想要注册新用户,就可以跳过授权步骤。

此外,我们将创建方法me来获取用户的配置文件,该方法将返回current_user我们在 application_controller 中设置的用户配置文件。



*app/controllers/users_controller.rb*

class UsersController < ApplicationController
    skip_before_action :authorized, only: [:create]
    rescue_from ActiveRecord::RecordInvalid, with: :handle_invalid_record

    def create 
        user = User.create!(user_params)
        @token = encode_token(user_id: user.id)
        render json: {
            user: UserSerializer.new(user), 
            token: @token
        }, status: :created
    end

    def me 
        render json: current_user, status: :ok
    end

    private

    def user_params 
        params.permit(:username, :password, :bio)
    end

    def handle_invalid_record(e)
            render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
    end
end


Enter fullscreen mode Exit fullscreen mode

我们的注册已准备就绪。让我们尝试在 Postman 中发出一些请求:

图片描述

开始吧!!等等!我们还漏掉了什么?身份验证最重要的部分:登录

为了实现登录,我们将创建一个新的控制器,并将其命名为 auth_controller。



rails g controller auth


Enter fullscreen mode Exit fullscreen mode


*app/controllers/auth_controller.rb*

class AuthController < ApplicationController

    skip_before_action :authorized, only: [:login]
    rescue_from ActiveRecord::RecordNotFound, with: :handle_record_not_found

    def login 
        @user = User.find_by!(username: login_params[:username])
        if @user.authenticate(login_params[:password])
            @token = encode_token(user_id: @user.id)
            render json: {
                user: UserSerializer.new(@user),
                token: @token
            }, status: :accepted
        else
            render json: {message: 'Incorrect password'}, status: :unauthorized
        end

    end

    private 

    def login_params 
        params.permit(:username, :password)
    end

    def handle_record_not_found(e)
        render json: { message: "User doesn't exist" }, status: :unauthorized
    end
end


Enter fullscreen mode Exit fullscreen mode

在该login方法中,我们首先通过用户名查找用户,如果未找到用户,则返回一条错误消息:"User doesn't exist"。找到用户后,我们使用 bcrypt 的 authentication 方法通过密码对用户进行身份验证。身份验证完成后,我们将为用户创建一个令牌,并将用户与令牌一起返回。如果身份验证失败,则返回一条错误消息:'Incorrect password'。我们还在skip_before_action :authorized, only: [:login]此处添加了 ,就像在用户控制器中的 create 方法中一样。

让我们在邮递员中进行一些调用:

图片描述

图片描述

图片描述

我们的授权已完成。

用户身份验证分为三步。首先,我们验证数据;然后,我们使用正确的用户名和密码对用户进行身份验证;最后,我们授权用户。

验证————>身份验证————>授权

如果我们使用 JWT 进行身份验证,则无法构建注销功能。JWT 库没有提供用于销毁令牌的方法。那么,我们如何注销呢?这必须在客户端中处理。如果我们使用 React 客户端,我们可以在登录时将令牌存储在 localStorage 中,并在需要注销时将其从 localStorage 中移除。



fetch("http://localhost:3000/auth/login/", {
      method: "POST",
      headers: {
        "Content-type": "application/json",
      },
      body: JSON.stringify({
        username: username,
        password: password,
      }),
    })
      .then((res) => res.json())
      .then((data) => {
        localStorage.setItem("jwt", data.jwt);
      })


Enter fullscreen mode Exit fullscreen mode

我们只需从 localStorage 中删除 jwt 令牌即可注销:



localStorage.removeItem("jwt")


Enter fullscreen mode Exit fullscreen mode
文章来源:https://dev.to/mohhossain/a-complete-guide-to-rails-authentication-using-jwt-403p
PREV
REST API 设计的 8 个最佳实践
NEXT
微服务:反模式和一些安全威胁