木曜日, 7月 24, 2025
木曜日, 7月 24, 2025
- Advertisment -
ホームニューステックニュース【Salesforce】クライアントログイン情報フローでRailsアプリケーションとAPI連携してみた #Rails - Qiita

【Salesforce】クライアントログイン情報フローでRailsアプリケーションとAPI連携してみた #Rails – Qiita



【Salesforce】クライアントログイン情報フローでRailsアプリケーションとAPI連携してみた #Rails - Qiita

Railsアプリケーションで作成した求人データを、SalesforceにAPI連携する処理を実装してみました。

馴染みがあるのは、OAuth 2.0 ユーザー名パスワードフローでした。
しかし、公式が非推奨としていたので、クライアントログイン情報フロー(Client Credentials Flow) を使用しました。

特別なシナリオの_OAuth_2_0_ユーザー名パスワードフロー.png

Railsアプリケーション(求人管理システム)
Cursor_と_Salesforce_Api_Practice.png

Salesforce_Api_Practice.png

Salesforce_Api_Practice.png

Salesforce(Developer Edition)
デザイナー募集___求人___Salesforce_🔊.png

サーバー間通信専用の認証方式で、アプリケーション自体の認証情報だけでSalesforce APIにアクセスできます。
つまり、 アプリ同士が「合言葉」(Client ID + Secret)で認証し合って、ユーザー情報なしでデータをやり取りできる仕組みです。

従来の方式との違い

従来(ユーザー名パスワードフロー)

Rails → Salesforce
「ユーザー名: [email protected]」
「パスワード: password123」
「セキュリティトークン: ABC123」

❌ 問題点: 実際のユーザー情報が必要、セキュリティリスク高

クライアントログイン情報フロー

Rails → Salesforce  
「Client ID: アプリのID」
「Client Secret: アプリの秘密鍵」

✅ メリット: ユーザー情報不要、アプリ専用の認証

Salesforce

Railsアプリケーション

  • Ruby: 3.3.0
  • Rails: 8.0.2
  • PostgreSQL
  • TailwindCSS

今回作成したRailsアプリケーション(求人管理システム)では、以下の機能を実装しました。
CursorのProプランに登録したので、フル活用して作成しました。

1. 基本的なCRUD機能

  • 求人の作成・表示・編集・削除

2. Salesforce API連携機能

  • クライアントログイン情報フローによる認証
  • 求人データの自動同期
  • 個別同期機能
  • 接続テスト機能

3. 論理削除機能

  • 求人の論理削除
  • 削除済みデータの復元機能
  • Salesforce側との同期も含めた削除処理

4. データクリーンアップ機能

  • 6ヶ月以上前の削除済みデータの自動物理削除
  • Rakeタスクによる定期実行
  • 管理画面からの手動実行

5. 重複防止機能

  • Salesforce側の外部ID(RailsJobID__c)による重複防止
  • upsert処理(作成/更新の自動判定)

Job.rb

class Job  ApplicationRecord
  validates :title, presence: true
  validates :company, presence: true
  validates :description, presence: true
  validates :location, presence: true

  # 論理削除機能
  default_scope { where(deleted_at: nil) }
  
  scope :active, -> { where(deleted_at: nil) }
  scope :deleted, -> { unscoped.where.not(deleted_at: nil) }
  scope :soft_deleted_old, -> { deleted.where('deleted_at , 6.months.ago) }

  # Salesforce連携用の属性
  attr_accessor :sync_to_salesforce

  # 論理削除メソッド
  def soft_delete!
    update!(deleted_at: Time.current)
  end

  def restore!
    update!(deleted_at: nil)
  end

  # 6ヶ月以上前の削除済みレコードをクリーンアップ
  def self.cleanup_old_deleted_records
    old_records = soft_deleted_old
    deleted_count = 0
    
    old_records.find_each do |job|
      # Salesforce側も削除
      service = SalesforceService.new
      service.delete_job_record(job)
      
      # 物理削除
      job.destroy
      deleted_count += 1
    end
    
    { success: true, deleted_count: deleted_count }
  end
end

JobsController.rb

class JobsController  ApplicationController
  # 求人作成時のSalesforce同期
  def create
    @job = Job.new(job_params)

    if @job.save
      if should_sync_to_salesforce?
        sync_result = sync_to_salesforce_client_credentials(@job)
        # 同期結果に応じたフラッシュメッセージ
      end
      redirect_to @job
    end
  end

  # 論理削除(Salesforce側も削除)
  def destroy
    @job.soft_delete!
    
    # Salesforce側も削除
    sync_result = SalesforceService.new.delete_job_record(@job)
    
    flash[:notice] = "求人が削除され、Salesforceからも削除されました"
    redirect_to jobs_path
  end

  # 削除済み求人の復元
  def restore
    @job.restore!
    
    # Salesforceにも復元(再作成)
    sync_result = sync_to_salesforce_client_credentials(@job)
    
    redirect_to @job
  end

  # 古い削除済みデータのクリーンアップ
  def cleanup_old_deleted
    result = Job.cleanup_old_deleted_records
    
    if result[:success]
      if result[:deleted_count] > 0
        flash[:notice] = "#{result[:deleted_count]}件の古い削除済みレコードを物理削除しました"
      else
        flash[:info] = "クリーンアップ対象のレコードはありませんでした"
      end
    end
    
    redirect_to deleted_jobs_path
  end
end

Salesforce側で、カスタムオブジェクトと外部クライアント接続アプリケーションを作成します。

1. Job__cオブジェクトの作成

Rails側の求人データを格納するためのカスタムオブジェクトを作成します。

設定手順:

  1. 設定 → オブジェクトマネージャー → 作成 → カスタムオブジェクト
  2. オブジェクト名:Job
  3. API参照名:Job__c
  4. レコード名:求人タイトル

2. カスタム項目の作成

以下の項目をJob__cオブジェクトに追加します:

項目名 API参照名 データ型 説明
会社名 Company__c テキスト(255) 会社名
詳細 Description__c ロングテキストエリア 詳細説明
勤務地 Location__c テキスト(255) 勤務地情報
給与 Salary__c 数値(18,0) 年収
雇用形態 EmploymentType__c 選択リスト 正社員/契約社員等
投稿日 Posted_Date__c 日付/時間 求人投稿日
Rails求人ID RailsJobID__c テキスト(255) 外部ID(重複防止用)

3. 外部IDの設定(重要)

RailsJobID__c項目を外部IDとして設定します:

  1. RailsJobID__c項目の編集画面を開く
  2. 「外部ID」にチェックを入れる
  3. 「一意」にチェックを入れる

これにより、Rails側のIDをキーとしたupsert処理が可能になります。

クライアントログイン情報フローを使用するための接続アプリケーションを設定します。

1. 接続アプリケーションの作成手順

  1. 設定 → アプリケーション → 外部クライアントアプリケーションマネージャー
  2. 「新規外部クライアントアプリケーション」をクリック
  3. 基本情報を入力:
    • 接続アプリケーション名: 任意
    • API参照名: 任意
    • 取引先責任者メール: 自分のメールアドレス

外部クライアントアプリケーションマネージャー___Salesforce.png

2. OAuth設定

「OAuth設定の有効化」にチェックを入れる。
外部クライアントアプリケーションマネージャー___Salesforce.png

以下を設定:

  • コールバックURL: https://localhost:3000/(ダミーURL)
  • 選択したOAuth範囲:

    • API を使用してユーザーデータを管理 (api)
    • フルアクセス (full)
    • いつでも要求を実行 (refresh_token, offline_access)

外部クライアントアプリケーションマネージャー___Salesforce.png

3. クライアントログイン情報フローの有効化

「クライアントログイン情報フローを有効化」にチェックを入れます。
外部クライアントアプリケーションマネージャー___Salesforce.png

4. 認証情報の取得

作成後、以下の情報を控えておきます:

  • コンシューマー鍵(Client ID)
  • コンシューマーの秘密(Client Secret)

外部クライアントアプリケーションマネージャー___Salesforce.png

Rails側でSalesforce APIとの連携を担当するSalesforceServiceクラスを実装します。

1. 基本構成

# app/services/salesforce_service.rb
class SalesforceService
  require 'net/http'
  require 'uri'
  require 'json'
  require 'restforce'

  def initialize
    @token_data = nil
    @client = nil
  end

  # 接続テスト
  def test_connection
    token_data = get_access_token
    return { success: false, error: "トークン取得に失敗しました" } unless token_data

    client = create_restforce_client(token_data)
    org_info = client.query("SELECT Id, Name FROM Organization LIMIT 1").first
    
    {
      success: true,
      message: "Client Credentials Flow 接続成功",
      organization: org_info.Name
    }
  rescue => e
    { success: false, error: "接続エラー: #{e.message}" }
  end
end

2. Client Credentials Flow認証の実装

private

def get_access_token
  return @token_data if @token_data

  begin
    uri = URI("https://#{ENV['SALESFORCE_HOST']}/services/oauth2/token")

    params = {
      "grant_type" => "client_credentials",
      "client_id" => ENV["SALESFORCE_CLIENT_ID"],
      "client_secret" => ENV["SALESFORCE_CLIENT_SECRET"]
    }

    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    http.verify_mode = OpenSSL::SSL::VERIFY_PEER
    http.open_timeout = 10
    http.read_timeout = 30

    request = Net::HTTP::Post.new(uri)
    request.set_form_data(params)
    request["Content-Type"] = "application/x-www-form-urlencoded"
    request["Accept"] = "application/json"

    response = http.request(request)
    
    if response.code == "200"
      @token_data = JSON.parse(response.body)
      Rails.logger.info "Salesforce認証成功: #{@token_data['instance_url']}"
      @token_data
    else
      Rails.logger.error "Salesforce認証失敗: #{response.code} - #{response.body}"
      nil
    end
  rescue => e
    Rails.logger.error "Salesforce認証エラー: #{e.message}"
    nil
  end
end

def create_restforce_client(token_data)
  Restforce.new(
    oauth_token: token_data['access_token'],
    instance_url: token_data['instance_url'],
    api_version: '61.0',
    ssl: { verify: true }
  )
end

3. upsert処理の実装(重複防止)

def upsert_job_record(job)
  client = get_authenticated_client
  return { success: false, error: "認証に失敗しました" } unless client

  begin
    job_data = {
      Name: job.title,
      Company__c: job.company,
      Description__c: job.description,
      Location__c: job.location,
      Salary__c: job.salary,
      Employment_Type__c: job.employment_type,
      Requirements__c: job.requirements,
      Posted_Date__c: job.posted_at&.iso8601,
      RailsJobID__c: job.id.to_s  # 外部IDとして使用
    }

    # upsert!メソッドで作成/更新を自動判定
    # 第1引数: オブジェクト名
    # 第2引数: 外部ID項目名
    # 第3引数: データ
    result = client.upsert!("Job__c", "RailsJobID__c", job_data)

    # 戻り値で作成/更新を判定
    operation = result.is_a?(String) ? "created" : "updated"
    salesforce_id = result.is_a?(String) ? result : "更新済み"

    {
      success: true,
      salesforce_id: salesforce_id,
      operation: operation,
      message: "Salesforceに#{operation == 'created' ? '作成' : '更新'}されました"
    }
  rescue Restforce::ResponseError => e
    Rails.logger.error "Salesforce API エラー: #{e.message}"
    { success: false, error: "API エラー: #{e.message}" }
  rescue => e
    Rails.logger.error "予期しないエラー: #{e.message}"
    { success: false, error: "予期しないエラー: #{e.message}" }
  end
end

private

def get_authenticated_client
  token_data = get_access_token
  return nil unless token_data
  
  @client ||= create_restforce_client(token_data)
end

4. 削除処理の実装

def delete_job_record(job)
  client = get_authenticated_client
  return { success: false, error: "認証に失敗しました" } unless client

  begin
    # 外部IDでSalesforceのレコードを検索
    query = "SELECT Id FROM Job__c WHERE RailsJobID__c="#{job.id}" LIMIT 1"
    result = client.query(query)

    if result.empty?
      return {
        success: true,
        message: "Salesforceにレコードが見つかりませんでした(既に削除済みの可能性)"
      }
    end

    salesforce_id = result.first.Id
    
    # Salesforceからレコードを削除
    client.destroy!("Job__c", salesforce_id)

    {
      success: true,
      salesforce_id: salesforce_id,
      message: "SalesforceのレコードID: #{salesforce_id} を削除しました"
    }
  rescue Restforce::ResponseError => e
    Rails.logger.error "Salesforce削除エラー: #{e.message}"
    { success: false, error: "API エラー: #{e.message}" }
  rescue => e
    Rails.logger.error "削除処理エラー: #{e.message}"
    { success: false, error: "削除エラー: #{e.message}" }
  end
end

5. 全件同期処理の実装

def sync_all_jobs
  client = get_authenticated_client
  return { success: false, error: "認証に失敗しました" } unless client

  success_count = 0
  error_count = 0
  errors = []

  begin
    # アクティブな求人を同期
    Job.active.find_each do |job|
      result = upsert_job_record(job)
      if result[:success]
        success_count += 1
      else
        error_count += 1
        errors  "Job ID #{job.id}: #{result[:error]}"
      end
    end

    # 削除済み求人をSalesforceからも削除
    Job.deleted.find_each do |job|
      result = delete_job_record(job)
      if result[:success]
        success_count += 1
      else
        error_count += 1
        errors  "Job ID #{job.id} (削除): #{result[:error]}"
      end
    end

    {
      success: true,
      success_count: success_count,
      error_count: error_count,
      errors: errors,
      message: "同期完了: 成功 #{success_count}件, エラー #{error_count}件"
    }
  rescue => e
    Rails.logger.error "全件同期エラー: #{e.message}"
    { success: false, error: "全件同期エラー: #{e.message}" }
  end
end

環境変数の設定

.envファイルの作成

# .env
SALESFORCE_HOST=your-domain.develop.my.salesforce.com
SALESFORCE_CLIENT_ID=your_client_id_here
SALESFORCE_CLIENT_SECRET=your_client_secret_here

GemfileにRestforceとdotenv-railsを追加

# Gemfile
gem "restforce"
gem "dotenv-rails"

バッチ処理の実装

def bulk_upsert_jobs(jobs)
  client = get_authenticated_client
  return { success: false, error: "認証に失敗しました" } unless client

  # 複数レコードを一度に処理
  job_data_array = jobs.map do |job|
    {
      Name: job.title,
      Company__c: job.company,
      RailsJobID__c: job.id.to_s
      # 他の項目...
    }
  end

  begin
    # バルクAPIを使用(大量データの場合)
    results = client.bulk.upsert("Job__c", job_data_array, "RailsJobID__c")
    
    {
      success: true,
      processed_count: results.size,
      results: results
    }
  rescue => e
    { success: false, error: "バルク処理エラー: #{e.message}" }
  end
end

今回は、クライアントログイン情報フローでRailsアプリケーションとAPI連携を実装しました。

最初は、ユーザー名パスワードフローで実装を試みましたが、パスワード認証が失敗し続けて苦戦しました。
ログイン履歴___Salesforce.png

今回使用したクライアントログイン情報フローの方が、セキュリティ的にも安全かつ実装も簡単でした。
(Railsアプリケーション側はCursor頼みでしたが…)

今後は、Slackなどの外部ツールとのAPI連携についても学んでいきたいと思います。





Source link

Views: 2

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -