rails: 创建 auth-api 应用
如何使用 rails 创建API 应用,自带基本的登录功能;原生实现,并没有接入 jwt 标准
01 创建项目
我的习惯是在github 上建好项目如: mockapi-rails
# 到项目目录
cd mockapi-rails
# 创建 rails api 项目
rails new . --api --skip-test02 针对针对老款mpb报错
可能报错处理
-处理办法
# rails
bundle
# mbp15
gem install puma -v '7.1.0' -- --with-opt-dir="$(brew --prefix openssl@3)"
rails db:migrate03 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
end05 准备 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 }
endapp/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
endapp/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
endclass 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
end07 相关的 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
endApp 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
end08 添加路由
重要的路由
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