rails: 创建 auth-api 应用

如何使用 rails 创建API 应用,自带基本的登录功能;原生实现,并没有接入 jwt 标准

01 创建项目

我的习惯是在github 上建好项目如: mockapi-rails

# 到项目目录
cd mockapi-rails
# 创建 rails api 项目
rails new . --api --skip-test

02 针对针对老款mpb报错

可能报错处理

-

处理办法

# rails
bundle

# mbp15
gem install puma -v '7.1.0' -- --with-opt-dir="$(brew --prefix openssl@3)"
rails db:migrate

03 pm2 运行项目

建议文件 ecosystem.config.js,启动命令:pm2 start ecosystem.config.js

module.exports = {
  apps: [
    {
      name: 'mockapi-rails',
      namespace: 'jsw',
      script: 'bundle exec rails s -p 3004',
      ignore_watch: ['node_modules', 'logs', 'log', 'tmp', '*.pyc']
    }
  ]
};

04 配置应用时区为 Asia/Shanghai

保证入库的信息是当前时区,更详细的代码看这里: rails: 修改时区为 Asia/Shanghai

module RailsAuthApi
  class Application < Rails::Application
    # 省略很多代码 ...
    config.time_zone = "Asia/Shanghai"
    config.active_record.default_timezone = :local
    # 省略很多代码 ...
  end
end

05 准备 models

准备以下几个Model

  • session
  • user
    • 还有一个 current

如果使用 rails g model 实现如下

# 用户相关
bin/rails g model User email_address:string:index password_digest:string
bin/rails g model Session user:references token:string:index user_agent:string ip_address:string

# 每个用户,独立的 session 存储
bin/rails g model Current --skip-migration

用户 app/models/user.rb ,可以根据需要改名,扩展

class User < ApplicationRecord
  has_secure_password
  has_many :sessions, dependent: :destroy

  normalizes :email_address, with: ->(e) { e.strip.downcase }
end

app/models/session.rb

class Session < ApplicationRecord
  belongs_to :user
  before_create :generate_token # Here call

  private
  def generate_token # Here implement, generate the token as you wish.
    self.token = Digest::SHA1.hexdigest([ Time.now, rand ].join)
  end
end

app/models/current.rb

class Current < ActiveSupport::CurrentAttributes
  attribute :session
  delegate :user, to: :session, allow_nil: true
end
```1:5:app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  attribute :session
  delegate :user, to: :session, allow_nil: true
end
```

- **ActiveSupport::CurrentAttributes**: 提供“每次请求/线程隔离”的全局存储区。每个请求都有自己的 `Current` 实例,不会互相污染。
- **attribute :session**: 声明一个请求级别的属性 `session`,可在控制器、模型等任意位置通过 `Current.session` 读写。
- **delegate :user, to: :session, allow_nil: true**: 将 `user` 调用委托给 `session.user`。当 `Current.session` 存在时,`Current.user` 等价于 `Current.session.user`;`allow_nil: true` 确保 `session` 为空时不会抛异常,而是返回 `nil`。
- **用途**: 认证后把当前会话对象放进 `Current.session`,后续业务代码可直接用 `Current.user` 获取当前用户,无需层层传参。Rails 会在请求结束时自动清理这些值。

06 rails db 数据库相关

与数据库相关的 migration

class CreateUsers < ActiveRecord::Migration[8.0]
  def change
    create_table :users do |t|
      t.string :email_address, null: false
      t.string :password_digest, null: false

      t.timestamps
    end
    add_index :users, :email_address, unique: true
  end
end
class CreateSessions < ActiveRecord::Migration[8.0]
  def change
    create_table :sessions do |t|
      t.references :user, null: false, foreign_key: true
      t.string :ip_address
      t.string :user_agent
      t.string :token     # HERE

      t.timestamps
    end
  end
end

07 相关的 controller

公用部分 app/controllers/concerns/authentication.rb

module Authentication
  extend ActiveSupport::Concern

  included do
    before_action :require_authentication
    # helper_method :authenticated? # APIs 不使用视图 helper
  end

  class_methods do
    def allow_unauthenticated_access(**options)
      skip_before_action :require_authentication, **options
    end
  end

  private
    def authenticated?
      resume_session
    end

    def require_authentication
      resume_session || request_authentication
    end

    def resume_session
      Current.session ||= find_session_by_authorization_header
    end

    def find_session_by_authorization_header
      authorization = request.headers["Authorization"] || request.authorization
      return unless authorization

      match = authorization.match(/^Bearer\s+(.+)$/i)
      return unless match

      token = match[1]
      Session.find_by(token: token)
    end

    def request_authentication
      render json: {}, status: :unauthorized
    end

    def start_new_session_for(user)
      user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
        Current.session = session
      end
    end

    def terminate_session
      Current.session&.destroy
    end
end

App base: app/controllers/application_controller.rb

class ApplicationController < ActionController::API
  include Authentication
end

核心 ctrl: app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  allow_unauthenticated_access only: %i[ create register ]
  # 说明:对登录/注册统一限流,命中限制时返回 429 JSON。
  rate_limit to: 10, within: 3.minutes, only: %i[ create register ], with: -> {
    render json: { error: "Try again later." }, status: :too_many_requests
  }

  def create
    # 原理:使用 has_secure_password 提供的 authenticate_by 校验账号密码
    # 成功后通过 AuthenticationConcern.start_new_session_for 创建 Session,并将其放入 Current.session
    # 再把生成的 token 返回给客户端,客户端后续以 Authorization: Bearer <token> 调用受保护接口
    if user = User.authenticate_by(login_params)
      start_and_render_token(user)
    else
      render json: {}, status: :unauthorized
    end
  end


  def me
    # 原理:在 AuthenticationConcern 中会从请求头解析 Bearer Token,恢复 Current.session 与 Current.user
    # 若缺少或无效,则这里返回 401;否则返回用户的最小必要字段,避免暴露敏感信息
    user = Current.user
    return render json: {}, status: :unauthorized unless user

    render json: { data: user.slice(:id, :email_address) }
  end

  def destroy
    # 原理:销毁当前 Session(基于 token 的会话),使 token 立即失效
    terminate_session
    render json: { data: { success: true } }, status: :ok
  end

  private
    # 统一创建会话并返回 token 的响应
    def start_and_render_token(user, status = :ok)
      start_new_session_for user
      render json: { data: { token: Current.session.token } }, status: status
    end

    # 参数许可:登录
    def login_params
      params.permit(:email_address, :password)
    end
end

08 添加路由

重要的路由 login/logout/me;以下是2点约定

  • me: 当前用户的信息
  • profile: 任何用户的信息,可以带资源ID
Rails.application.routes.draw do
  # API session routes (no pages)
  post "login" => "sessions#create"
  delete "logout" => "sessions#destroy"
  get "me" => "sessions#me"
end
rails mock api pm2