inertia-rails-auth
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseInertia Rails Authentication & Authorization
Inertia Rails 认证与授权
Guide to implementing authentication and authorization in Inertia Rails applications.
在Inertia Rails应用中实现认证与授权的指南。
Key Principle
核心原则
Inertia uses your existing Rails authentication infrastructure. No special OAuth or token-based auth required. Since your frontend and backend share the same domain, session-based auth works seamlessly.
Inertia 会复用你现有的Rails认证基础设施,无需特殊的OAuth或基于令牌的认证方案。由于前端与后端共享同一域名,基于会话的认证可以无缝工作。
Authentication with Devise
使用 Devise 实现认证
Setup
配置步骤
ruby
undefinedruby
undefinedGemfile
Gemfile
gem 'devise'
```bash
bundle install
rails generate devise:install
rails generate devise User
rails db:migrategem 'devise'
```bash
bundle install
rails generate devise:install
rails generate devise User
rails db:migrateShare Authentication State
共享认证状态
ruby
undefinedruby
undefinedapp/controllers/application_controller.rb
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
inertia_share do
{
auth: {
user: current_user&.as_json(only: [:id, :name, :email, :avatar_url]),
signed_in: user_signed_in?
}
}
end
end
undefinedclass ApplicationController < ActionController::Base
inertia_share do
{
auth: {
user: current_user&.as_json(only: [:id, :name, :email, :avatar_url]),
signed_in: user_signed_in?
}
}
end
end
undefinedCreate Login Page
创建登录页面
ruby
undefinedruby
undefinedapp/controllers/sessions_controller.rb
app/controllers/sessions_controller.rb
class SessionsController < Devise::SessionsController
def new
render inertia: {}
end
def create
self.resource = warden.authenticate(auth_options)
if resource
sign_in(resource_name, resource)
redirect_to after_sign_in_path_for(resource), notice: 'Signed in successfully!'
else
redirect_to new_session_path(resource_name), inertia: {
errors: { email: 'Invalid email or password' }
}
endend
def destroy
sign_out(resource_name)
redirect_to root_path, notice: 'Signed out successfully!'
end
end
undefinedclass SessionsController < Devise::SessionsController
def new
render inertia: {}
end
def create
self.resource = warden.authenticate(auth_options)
if resource
sign_in(resource_name, resource)
redirect_to after_sign_in_path_for(resource), notice: 'Signed in successfully!'
else
redirect_to new_session_path(resource_name), inertia: {
errors: { email: 'Invalid email or password' }
}
endend
def destroy
sign_out(resource_name)
redirect_to root_path, notice: 'Signed out successfully!'
end
end
undefinedLogin Component (Vue)
登录组件(Vue)
vue
<!-- app/frontend/pages/sessions/new.vue -->
<script setup>
import { useForm, Link } from '@inertiajs/vue3'
const form = useForm({
email: '',
password: '',
remember: false,
})
function submit() {
form.post('/users/sign_in', {
onSuccess: () => form.reset('password'),
})
}
</script>
<template>
<form @submit.prevent="submit">
<h1>Sign In</h1>
<div>
<label>Email</label>
<input v-model="form.email" type="email" autofocus />
<span v-if="form.errors.email" class="error">{{ form.errors.email }}</span>
</div>
<div>
<label>Password</label>
<input v-model="form.password" type="password" />
</div>
<div>
<label>
<input v-model="form.remember" type="checkbox" />
Remember me
</label>
</div>
<button type="submit" :disabled="form.processing">
{{ form.processing ? 'Signing in...' : 'Sign In' }}
</button>
<p>
<Link href="/users/sign_up">Create an account</Link>
<Link href="/users/password/new">Forgot password?</Link>
</p>
</form>
</template>vue
<!-- app/frontend/pages/sessions/new.vue -->
<script setup>
import { useForm, Link } from '@inertiajs/vue3'
const form = useForm({
email: '',
password: '',
remember: false,
})
function submit() {
form.post('/users/sign_in', {
onSuccess: () => form.reset('password'),
})
}
</script>
<template>
<form @submit.prevent="submit">
<h1>Sign In</h1>
<div>
<label>Email</label>
<input v-model="form.email" type="email" autofocus />
<span v-if="form.errors.email" class="error">{{ form.errors.email }}</span>
</div>
<div>
<label>Password</label>
<input v-model="form.password" type="password" />
</div>
<div>
<label>
<input v-model="form.remember" type="checkbox" />
Remember me
</label>
</div>
<button type="submit" :disabled="form.processing">
{{ form.processing ? 'Signing in...' : 'Sign In' }}
</button>
<p>
<Link href="/users/sign_up">Create an account</Link>
<Link href="/users/password/new">Forgot password?</Link>
</p>
</form>
</template>Login Component (React)
登录组件(React)
jsx
// app/frontend/pages/sessions/new.jsx
import { useForm, Link } from '@inertiajs/react'
export default function Login() {
const { data, setData, post, processing, errors, reset } = useForm({
email: '',
password: '',
remember: false,
})
function submit(e) {
e.preventDefault()
post('/users/sign_in', {
onSuccess: () => reset('password'),
})
}
return (
<form onSubmit={submit}>
<h1>Sign In</h1>
<div>
<label>Email</label>
<input
type="email"
value={data.email}
onChange={(e) => setData('email', e.target.value)}
autoFocus
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<label>Password</label>
<input
type="password"
value={data.password}
onChange={(e) => setData('password', e.target.value)}
/>
</div>
<div>
<label>
<input
type="checkbox"
checked={data.remember}
onChange={(e) => setData('remember', e.target.checked)}
/>
Remember me
</label>
</div>
<button type="submit" disabled={processing}>
{processing ? 'Signing in...' : 'Sign In'}
</button>
<p>
<Link href="/users/sign_up">Create an account</Link>
<Link href="/users/password/new">Forgot password?</Link>
</p>
</form>
)
}jsx
// app/frontend/pages/sessions/new.jsx
import { useForm, Link } from '@inertiajs/react'
export default function Login() {
const { data, setData, post, processing, errors, reset } = useForm({
email: '',
password: '',
remember: false,
})
function submit(e) {
e.preventDefault()
post('/users/sign_in', {
onSuccess: () => reset('password'),
})
}
return (
<form onSubmit={submit}>
<h1>Sign In</h1>
<div>
<label>Email</label>
<input
type="email"
value={data.email}
onChange={(e) => setData('email', e.target.value)}
autoFocus
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<label>Password</label>
<input
type="password"
value={data.password}
onChange={(e) => setData('password', e.target.value)}
/>
</div>
<div>
<label>
<input
type="checkbox"
checked={data.remember}
onChange={(e) => setData('remember', e.target.checked)}
/>
Remember me
</label>
</div>
<button type="submit" disabled={processing}>
{processing ? 'Signing in...' : 'Sign In'}
</button>
<p>
<Link href="/users/sign_up">Create an account</Link>
<Link href="/users/password/new">Forgot password?</Link>
</p>
</form>
)
}Authentication with has_secure_password
使用 has_secure_password 实现认证
User Model
用户模型
ruby
undefinedruby
undefinedapp/models/user.rb
app/models/user.rb
class User < ApplicationRecord
has_secure_password
validates :email, presence: true, uniqueness: true
validates :password, length: { minimum: 8 }, allow_nil: true
end
undefinedclass User < ApplicationRecord
has_secure_password
validates :email, presence: true, uniqueness: true
validates :password, length: { minimum: 8 }, allow_nil: true
end
undefinedSessions Controller
会话控制器
ruby
undefinedruby
undefinedapp/controllers/sessions_controller.rb
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
skip_before_action :authenticate!, only: [:new, :create]
def new
render inertia: {}
end
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
session[:user_id] = user.id
redirect_to dashboard_path, notice: 'Welcome back!'
else
redirect_to login_path, inertia: {
errors: { email: 'Invalid email or password' }
}
endend
def destroy
session.delete(:user_id)
redirect_to root_path, notice: 'Signed out successfully!'
end
end
class SessionsController < ApplicationController
skip_before_action :authenticate!, only: [:new, :create]
def new
render inertia: {}
end
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
session[:user_id] = user.id
redirect_to dashboard_path, notice: 'Welcome back!'
else
redirect_to login_path, inertia: {
errors: { email: 'Invalid email or password' }
}
endend
def destroy
session.delete(:user_id)
redirect_to root_path, notice: 'Signed out successfully!'
end
end
Routes
路由
config/routes.rb
config/routes.rb
get 'login', to: 'sessions#new'
post 'login', to: 'sessions#create'
delete 'logout', to: 'sessions#destroy'
undefinedget 'login', to: 'sessions#new'
post 'login', to: 'sessions#create'
delete 'logout', to: 'sessions#destroy'
undefinedApplication Controller Helper
应用控制器助手方法
ruby
undefinedruby
undefinedapp/controllers/application_controller.rb
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :authenticate!
helper_method :current_user, :user_signed_in?
inertia_share do
{
auth: {
user: current_user&.as_json(only: [:id, :name, :email]),
signed_in: user_signed_in?
}
}
end
private
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
def user_signed_in?
current_user.present?
end
def authenticate!
unless user_signed_in?
redirect_to login_path, alert: 'Please sign in to continue'
end
end
end
undefinedclass ApplicationController < ActionController::Base
before_action :authenticate!
helper_method :current_user, :user_signed_in?
inertia_share do
{
auth: {
user: current_user&.as_json(only: [:id, :name, :email]),
signed_in: user_signed_in?
}
}
end
private
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
def user_signed_in?
current_user.present?
end
def authenticate!
unless user_signed_in?
redirect_to login_path, alert: 'Please sign in to continue'
end
end
end
undefinedAuthorization
授权
Passing Permissions as Props
以Props形式传递权限
Since frontend components can't access server-side authorization helpers, pass permission results as props:
ruby
undefined由于前端组件无法访问服务器端的授权助手方法,需要将权限验证结果以Props形式传递:
ruby
undefinedapp/controllers/users_controller.rb
app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
render inertia: {
can: {
create_user: policy(User).create?
},
users: User.all.map do |user|
serialize_user(user)
end
}
end
def show
user = User.find(params[:id])
render inertia: {
user: serialize_user(user),
can: {
edit: policy(user).edit?,
delete: policy(user).destroy?
}
}end
private
def serialize_user(user)
user.as_json(only: [:id, :name, :email]).merge(
can: {
edit: policy(user).edit?,
delete: policy(user).destroy?
}
)
end
end
undefinedclass UsersController < ApplicationController
def index
render inertia: {
can: {
create_user: policy(User).create?
},
users: User.all.map do |user|
serialize_user(user)
end
}
end
def show
user = User.find(params[:id])
render inertia: {
user: serialize_user(user),
can: {
edit: policy(user).edit?,
delete: policy(user).destroy?
}
}end
private
def serialize_user(user)
user.as_json(only: [:id, :name, :email]).merge(
can: {
edit: policy(user).edit?,
delete: policy(user).destroy?
}
)
end
end
undefinedUsing Pundit
使用 Pundit
ruby
undefinedruby
undefinedGemfile
Gemfile
gem 'pundit'
gem 'pundit'
app/controllers/application_controller.rb
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Pundit::Authorization
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
redirect_to root_path, alert: 'You are not authorized to perform this action'
end
end
class ApplicationController < ActionController::Base
include Pundit::Authorization
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
redirect_to root_path, alert: 'You are not authorized to perform this action'
end
end
app/policies/user_policy.rb
app/policies/user_policy.rb
class UserPolicy < ApplicationPolicy
def index?
true
end
def show?
true
end
def create?
user.admin?
end
def update?
user.admin? || record == user
end
def destroy?
user.admin? && record != user
end
end
undefinedclass UserPolicy < ApplicationPolicy
def index?
true
end
def show?
true
end
def create?
user.admin?
end
def update?
user.admin? || record == user
end
def destroy?
user.admin? && record != user
end
end
undefinedUsing Action Policy
使用 Action Policy
ruby
undefinedruby
undefinedGemfile
Gemfile
gem 'action_policy'
gem 'action_policy'
app/controllers/users_controller.rb
app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
render inertia: {
can: {
create_user: allowed_to?(:create?, User)
},
users: User.all.map do |user|
user.as_json(only: [:id, :name]).merge(
can: {
edit: allowed_to?(:edit?, user),
delete: allowed_to?(:destroy?, user)
}
)
end
}
end
end
undefinedclass UsersController < ApplicationController
def index
render inertia: {
can: {
create_user: allowed_to?(:create?, User)
},
users: User.all.map do |user|
user.as_json(only: [:id, :name]).merge(
can: {
edit: allowed_to?(:edit?, user),
delete: allowed_to?(:destroy?, user)
}
)
end
}
end
end
undefinedFrontend Permission Checks (Vue)
前端权限验证(Vue)
vue
<script setup>
import { Link, usePage } from '@inertiajs/vue3'
const props = defineProps(['users', 'can'])
const { auth } = usePage().props
</script>
<template>
<div>
<h1>Users</h1>
<!-- Global permission -->
<Link v-if="can.create_user" href="/users/new" class="btn">
Create User
</Link>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }}
<!-- Per-record permissions -->
<Link v-if="user.can.edit" :href="`/users/${user.id}/edit`">
Edit
</Link>
<Link
v-if="user.can.delete"
:href="`/users/${user.id}`"
method="delete"
as="button"
>
Delete
</Link>
</li>
</ul>
</div>
</template>vue
<script setup>
import { Link, usePage } from '@inertiajs/vue3'
const props = defineProps(['users', 'can'])
const { auth } = usePage().props
</script>
<template>
<div>
<h1>Users</h1>
<!-- 全局权限 -->
<Link v-if="can.create_user" href="/users/new" class="btn">
Create User
</Link>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }}
<!-- 单条记录权限 -->
<Link v-if="user.can.edit" :href="`/users/${user.id}/edit`">
Edit
</Link>
<Link
v-if="user.can.delete"
:href="`/users/${user.id}`"
method="delete"
as="button"
>
Delete
</Link>
</li>
</ul>
</div>
</template>Frontend Permission Checks (React)
前端权限验证(React)
jsx
import { Link, usePage } from '@inertiajs/react'
export default function UsersIndex({ users, can }) {
const { auth } = usePage().props
return (
<div>
<h1>Users</h1>
{can.create_user && (
<Link href="/users/new" className="btn">
Create User
</Link>
)}
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name}
{user.can.edit && (
<Link href={`/users/${user.id}/edit`}>Edit</Link>
)}
{user.can.delete && (
<Link
href={`/users/${user.id}`}
method="delete"
as="button"
>
Delete
</Link>
)}
</li>
))}
</ul>
</div>
)
}jsx
import { Link, usePage } from '@inertiajs/react'
export default function UsersIndex({ users, can }) {
const { auth } = usePage().props
return (
<div>
<h1>Users</h1>
{can.create_user && (
<Link href="/users/new" className="btn">
Create User
</Link>
)}
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name}
{user.can.edit && (
<Link href={`/users/${user.id}/edit`}>Edit</Link>
)}
{user.can.delete && (
<Link
href={`/users/${user.id}`}
method="delete"
as="button"
>
Delete
</Link>
)}
</li>
))}
</ul>
</div>
)
}History Encryption
历史记录加密
Prevent sensitive data exposure via browser back button after logout:
ruby
undefined登出后,防止敏感数据通过浏览器后退按钮泄露:
ruby
undefinedconfig/initializers/inertia_rails.rb
config/initializers/inertia_rails.rb
InertiaRails.configure do |config|
Enable globally
config.encrypt_history = true
end
InertiaRails.configure do |config|
全局启用
config.encrypt_history = true
end
Or per-controller for sensitive areas
或针对敏感区域的控制器单独设置
class Admin::BaseController < ApplicationController
inertia_config(encrypt_history: true)
end
class Admin::BaseController < ApplicationController
inertia_config(encrypt_history: true)
end
Or per-request
或针对单个请求设置
def show
render inertia: { secret_data: data }, encrypt_history: true
end
undefineddef show
render inertia: { secret_data: data }, encrypt_history: true
end
undefinedClear History on Logout
登出时清除历史记录
ruby
def destroy
sign_out(current_user)
# Clear encrypted history (rotates encryption key)
redirect_to root_path, inertia: { clear_history: true }
endClient-side:
javascript
import { router } from '@inertiajs/vue3'
function logout() {
router.clearHistory()
router.post('/logout')
}ruby
def destroy
sign_out(current_user)
# 清除加密历史记录(轮换加密密钥)
redirect_to root_path, inertia: { clear_history: true }
end客户端实现:
javascript
import { router } from '@inertiajs/vue3'
function logout() {
router.clearHistory()
router.post('/logout')
}Protected Routes
受保护路由
Middleware Approach
中间件方式
ruby
undefinedruby
undefinedapp/controllers/application_controller.rb
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :authenticate!
private
def authenticate!
unless user_signed_in?
redirect_to login_path, alert: 'Please sign in'
end
end
end
class ApplicationController < ActionController::Base
before_action :authenticate!
private
def authenticate!
unless user_signed_in?
redirect_to login_path, alert: 'Please sign in'
end
end
end
Skip for public controllers
为公开控制器跳过验证
class PagesController < ApplicationController
skip_before_action :authenticate!, only: [:home, :about]
end
undefinedclass PagesController < ApplicationController
skip_before_action :authenticate!, only: [:home, :about]
end
undefinedRole-Based Access
基于角色的访问控制
ruby
class Admin::BaseController < ApplicationController
before_action :require_admin!
private
def require_admin!
unless current_user&.admin?
redirect_to root_path, alert: 'Admin access required'
end
end
endruby
class Admin::BaseController < ApplicationController
before_action :require_admin!
private
def require_admin!
unless current_user&.admin?
redirect_to root_path, alert: 'Admin access required'
end
end
endCSRF Protection
CSRF 防护
Inertia handles CSRF automatically. Ensure Rails is configured:
ruby
undefinedInertia 会自动处理CSRF防护。确保Rails已正确配置:
ruby
undefinedapp/controllers/application_controller.rb
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
Axios (used by Inertia) automatically:
1. Reads `XSRF-TOKEN` cookie set by Rails
2. Sends `X-XSRF-TOKEN` header with requestsclass ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
Inertia 使用的Axios会自动:
1. 读取Rails设置的`XSRF-TOKEN` Cookie
2. 在请求中发送`X-XSRF-TOKEN`请求头Registration Flow
注册流程
ruby
undefinedruby
undefinedapp/controllers/registrations_controller.rb
app/controllers/registrations_controller.rb
class RegistrationsController < ApplicationController
skip_before_action :authenticate!
def new
render inertia: {}
end
def create
user = User.new(user_params)
if user.save
session[:user_id] = user.id
redirect_to dashboard_path, notice: 'Welcome!'
else
redirect_to register_path, inertia: { errors: user.errors }
endend
private
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
end
undefinedclass RegistrationsController < ApplicationController
skip_before_action :authenticate!
def new
render inertia: {}
end
def create
user = User.new(user_params)
if user.save
session[:user_id] = user.id
redirect_to dashboard_path, notice: 'Welcome!'
else
redirect_to register_path, inertia: { errors: user.errors }
endend
private
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
end
undefinedPassword Reset Flow
密码重置流程
ruby
undefinedruby
undefinedapp/controllers/password_resets_controller.rb
app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
skip_before_action :authenticate!
def new
render inertia: {}
end
def create
user = User.find_by(email: params[:email])
user&.send_password_reset_email
# Always show success to prevent email enumeration
redirect_to login_path, notice: 'Check your email for reset instructions'end
def edit
user = User.find_by(password_reset_token: params[:token])
if user&.password_reset_valid?
render inertia: { token: params[:token] }
else
redirect_to login_path, alert: 'Invalid or expired reset link'
endend
def update
user = User.find_by(password_reset_token: params[:token])
if user&.password_reset_valid? && user.update(password_params)
user.clear_password_reset!
session[:user_id] = user.id
redirect_to dashboard_path, notice: 'Password updated!'
else
redirect_to edit_password_reset_path(token: params[:token]),
inertia: { errors: user&.errors || { token: 'Invalid token' } }
endend
private
def password_params
params.permit(:password, :password_confirmation)
end
end
undefinedclass PasswordResetsController < ApplicationController
skip_before_action :authenticate!
def new
render inertia: {}
end
def create
user = User.find_by(email: params[:email])
user&.send_password_reset_email
# 始终显示成功提示,防止邮箱枚举
redirect_to login_path, notice: 'Check your email for reset instructions'end
def edit
user = User.find_by(password_reset_token: params[:token])
if user&.password_reset_valid?
render inertia: { token: params[:token] }
else
redirect_to login_path, alert: 'Invalid or expired reset link'
endend
def update
user = User.find_by(password_reset_token: params[:token])
if user&.password_reset_valid? && user.update(password_params)
user.clear_password_reset!
session[:user_id] = user.id
redirect_to dashboard_path, notice: 'Password updated!'
else
redirect_to edit_password_reset_path(token: params[:token]),
inertia: { errors: user&.errors || { token: 'Invalid token' } }
endend
private
def password_params
params.permit(:password, :password_confirmation)
end
end
undefinedBest Practices
最佳实践
1. Never Trust Client-Side Auth Checks
1. 绝不信任客户端的认证检查
Always verify permissions server-side:
ruby
def destroy
user = User.find(params[:id])
authorize user # Pundit/ActionPolicy check
user.destroy
redirect_to users_path
end始终在服务器端验证权限:
ruby
def destroy
user = User.find(params[:id])
authorize user # Pundit/ActionPolicy 检查
user.destroy
redirect_to users_path
end2. Minimize Exposed User Data
2. 最小化暴露的用户数据
ruby
undefinedruby
undefinedBad
不良示例
inertia_share auth: { user: current_user }
inertia_share auth: { user: current_user }
Good
良好示例
inertia_share auth: {
user: current_user&.as_json(only: [:id, :name, :email])
}
undefinedinertia_share auth: {
user: current_user&.as_json(only: [:id, :name, :email])
}
undefined3. Use Secure Session Configuration
3. 使用安全的会话配置
ruby
undefinedruby
undefinedconfig/initializers/session_store.rb
config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
key: '_myapp_session',
secure: Rails.env.production?,
httponly: true,
same_site: :lax
undefinedRails.application.config.session_store :cookie_store,
key: '_myapp_session',
secure: Rails.env.production?,
httponly: true,
same_site: :lax
undefined4. Handle Session Expiry Gracefully
4. 优雅处理会话过期
javascript
// Check auth state on each navigation
router.on('navigate', () => {
const { auth } = usePage().props
if (!auth.signed_in && requiresAuth(page.component)) {
router.visit('/login')
}
})javascript
// 在每次导航时检查认证状态
router.on('navigate', () => {
const { auth } = usePage().props
if (!auth.signed_in && requiresAuth(page.component)) {
router.visit('/login')
}
})5. Implement Rate Limiting
5. 实现速率限制
ruby
undefinedruby
undefinedGemfile
Gemfile
gem 'rack-attack'
gem 'rack-attack'
config/initializers/rack_attack.rb
config/initializers/rack_attack.rb
Rack::Attack.throttle('login attempts', limit: 5, period: 60) do |req|
req.ip if req.path == '/login' && req.post?
end
undefinedRack::Attack.throttle('login attempts', limit: 5, period: 60) do |req|
req.ip if req.path == '/login' && req.post?
end
undefined