rails7: jwt 与 api
jwt原理

名词
| 名词 | 解释 |
|---|---|
| jwt | JSON Web Tokens |
分析
- 这个用在登录的场景,所以与
sessions相关 - 前端登录的时候,会带
token在headers里,理论上,后端所有的api都需要经过这个token的验证,才能表明登录成功
场景
- 我希望用
rails完成如下功能- 我的路由是
/api/v1为前缀 - 我已经有了
user的 model (username/password_digest) username/password登录,并返回 token- 在
/api/v1下的controller都会有require_login来校验token是否存在
- 我的路由是
添加 gem
gem 'bcrypt', '~> 3.1.7'
gem 'jwt', '~> 2.2', '>= 2.2.3'Users
最基础的 users model
rails generate model User username:string password_digest:string
rails db:migrate# app/models/user.rb
class User < ApplicationRecord
has_secure_password
# Other validations or associations can be added here
end添加 controller
添加 controller
rails g controller Api::V1::Users# app/controllers/api/v1/users_controller.rb
class Api::V1::UsersController < ApplicationController
before_action :authenticate, only: [:profile]
def create
user = User.new(user_params)
if user.save
render json: { token: generate_token(user.id) }
else
render json: { error: user.errors.full_messages }, status: :unprocessable_entity
end
end
def login
user = User.find_by(username: params[:username])
if user && user.authenticate(params[:password])
render json: { token: generate_token(user.id) }
else
render json: { error: 'Invalid username or password' }, status: :unauthorized
end
end
private
def authenticate
token = request.headers['Authorization'].split(' ').last
payload = JWT.decode(token, ENV['SECRET_KEY']).first
@current_user = User.find(payload['user_id'])
end
def user_params
params.require(:user).permit(:username, :password)
end
def generate_token(user_id)
payload = { user_id: user_id }
JWT.encode(payload, ENV['SECRET_KEY'])
end
end思路清晰版
添国
app/controllers/concerns/json_web_token.rb公用模块,以下为.env文件
SECRET_KEY=faGOGX_lCPgpPHJAv1mnUtv0aYC1UZ7_require 'jwt'
module JsonWebToken
extend ActiveSupport::Concern
SECRET_KEY = ENV.fetch('SECRET_KEY_BASE')
def jwt_encode(payload, exp = 2.years.from_now)
JWT.encode(payload, SECRET_KEY)
end
def jwt_decode(token)
decoded_token = JWT.decode(token, SECRET_KEY)
HashWithIndifferentAccess.new(decoded_token[0])
end
end基础类
base_controller.rb,实际业务中可能要添加 skip_action
class ApplicationController < ActionController::API
include JsonWebToken
before_action :authenticate_request
private
def authenticate_request
header = request.headers['Authorization']
header = header.split(' ').last if header
begin
decoded = jwt_decode(header)
@current_user = User.find(decoded[:user_id])
rescue ActiveRecord::RecordNotFound => e
render json: { errors: e.message }, status: :unauthorized
rescue JWT::DecodeError => e
render json: { errors: e.message }, status: :unauthorized
end
end
end更详细的教程
- https://blog.appsignal.com/2023/08/23/secure-your-ruby-app-with-json-web-tokens.html
- https://ruby.mobidev.biz/posts/efficient-json-serialization-with-blueprinter-for-ruby-on-rails/
前端 与 jwt
- header.payload.signature
- header:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 - payload:
原始的:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7ImlkIjoxLCJlbWFpbF9hZGRyZXNzIjoidXNlckBleGFtcGxlLmNvbSJ9LCJleHAiOjE3NjIzNDIzNTEsImlhdCI6MTc2MjMyMDc1MSwianRpIjoiYjRjNjFmMDItYjMzNi00NGQ0LThmNjEtNTAxYzMyNmI1ODdkIn0.4HHVx3OjJv1RcE1Rj_9H2hRDL7LFegRWL-DksVc6bi0
const payloadBase64 = "eyJkYXRhIjp7ImlkIjoxLCJlbWFpbF9hZGRyZXNzIjoidXNlckBleGFtcGxlLmNvbSJ9LCJleHAiOjE3NjIzNDIzNTEsImlhdCI6MTc2MjMyMDc1MSwianRpIjoiYjRjNjFmMDItYjMzNi00NGQ0LThmNjEtNTAxYzMyNmI1ODdkIn0";
// Base64Url → Base64
const base64 = payloadBase64
.replace(/-/g, '+')
.replace(/_/g, '/');
const payload = JSON.parse(atob(base64));
console.log(payload);
// 得到如下结果:
{
data: {
id: 1,
email_address: "user@example.com"
},
exp: 1762342351, // 过期时间(Unix 时间戳)
iat: 1762320751, // 签发时间
jti: "b4c61f02-b336-44d4-8f61-501c326b587d" // 唯一 ID
}
// 这个时间 => 对应js的时间: 1762320751 * 1000
// 这个 token 的时长为(6小时): (1762342351 - 1762320751)/3600