Rails 使用云片和 China sms 发送验证信息
rails 6,云片,china_sms,需要实现短信验证和语音验证。
1.添加 china_sms 的 gem 包:
# China SMS client gem ‘china_sms‘, github: ‘saberma/china_sms‘, branch: ‘master‘
然后运行:
bundle install
2.配置
在 ‘config/initializers/china_sms.rb‘ 文件中添加一行配置:
ChinaSMS.use :yunpian, password: Figaro.env.YUNPIAN_API_KEY
在 ‘config/initializers/inflections.rb‘ 文件中添加:
inflect.acronym ‘SMS‘
在 ‘config/application.yml.example‘ 文件中添加在云片注册的 key 和 sign
# 云片 YUNPIAN_API_KEY: ‘‘ YUNPIAN_SIGN: ‘‘
还需要在 config 下手动新建 yunpian_blocklist.txt 和 yunpian_blockmobile.txt 文件,他们是关键词、关键字和手机号黑名单
3.创建模型
我们需要个模型来存储验证码、手机号、验证次数等等信息,模型如下,
create_table :verification_codes do |t| t.string :mobile, index: true t.string :code t.datetime :expired_at t.integer :failed_attempts, default: 0 t.timestamps end
verification_code.rb:
class VerificationCode < ApplicationRecord # 过期时间 : 不得超过 5 分钟 EXPIRED_DURATION = 5.minutes # 尝试激活次数 : 不得超过 5 次 MAXIMUM_ATTEMPTS = 5
mobile: true 请点这里
# mobile 不能为空,以及验证格式等 validates :mobile, presence: true, mobile: true validates :code, presence: true validates :expired_at, presence: true before_validation :setup_mobile, on: :create before_validation :setup_code, on: :create before_validation :setup_expired_at, on: :create scope :expired, -> { where(‘? > expired_at‘, Time.current) } # 在重新发送验证码时,删除之前该手机号所有已经过期数据并重新创建 def self.regenerate_verification_code_for_mobile!(mobile) transaction do where(mobile: mobile).expired.destroy_all create!(mobile: mobile) end end # 验证手机号、验证码是否通过 def self.authenticate(mobile, unauthenticated_code) where(mobile: mobile).each do |record| # 调用 __send__ 调用私有方法 authenticate(unauthenticated_code) 判断验证信息是否通过 return true if record.__send__(:authenticate, unauthenticated_code) end; false end private # def authenticate(unauthenticated_code) # 过期或者验证次数是否超出 return false if expired? || attempts_exceeded? # 验证码是否正确 return valid_code?(unauthenticated_code) end # 是否过期 def expired? expired_at < Time.current end # 验证次数是否超出 def attempts_exceeded? failed_attempts >= MAXIMUM_ATTEMPTS end # 验证码是否正确 def valid_code?(unauthenticated_code) if code == unauthenticated_code true else increment!(:failed_attempts) false end end # 去除前后空格 def setup_mobile self.mobile = mobile.to_s.strip end # 获取0-9999随机4位数验证码 def setup_code self.code = rand(0..9999).to_s.rjust(4, ‘0‘) end # 过期时间为当前时间 + 5 分钟 def setup_expired_at self.expired_at = Time.current + EXPIRED_DURATION end end
4.编写接口
在 app/libs 文件夹下新建 external_api.rb 文件,内容为:
module ExternalAPI def self.sms end def self.voice end end
然后在 libs 文件夹下新建 external_api 文件夹,在新建的 external_api 文件夹下新建 sms.rb、voice.rb,分别是发送短信验证、推送语音验证
sms.rb:
module ExternalAPI class SMS # 通过错误码向 controller 抛出错误信息 Error = Class.new(StandardError) KeywordViolationError = Class.new(Error) RateLimitExceededError = Class.new(Error) TemplateLimitExceededError = Class.new(Error) # 黑名单匹配 BLOCKLIST = File.read(Rails.root.join(‘config/yunpian_blocklist.txt‘)).lines.map(&:strip) BLOCKMOBILE = File.read(Rails.root.join(‘config/yunpian_blockmobile.txt‘)).lines.map(&:strip) def deliver(mobile, message) # 黑名单匹配 message = message.tr(‘【‘, ‘[‘).tr(‘】‘, ‘]‘) BLOCKLIST.each { |word| message.gsub!(word, ‘*‘ * word.size) } if BLOCKMOBILE.include?(mobile) Rails.logger.info "[ExternalAPI::SMS.deliver] blocked mobile: #{mobile}" return end # 格式化短信 sms = format(‘【%s】%s‘, Figaro.env.YUNPIAN_SIGN, message) # 发送短信 r = ChinaSMS.to(mobile, sms) # 判断返回码 if r.key?(‘code‘) case r[‘code‘] when 0 then return when 4 then raise KeywordViolationError, r[‘detail‘] when 8 then raise RateLimitExceededError, r[‘detail‘] when 33 then raise TemplateLimitExceededError, r[‘detail‘] else raise Error, r[‘detail‘] end else case r[‘data‘][0][‘code‘] when 0 then return when 4 then raise KeywordViolationError, r[‘data‘][0][‘msg‘] when 8 then raise RateLimitExceededError, r[‘data‘][0][‘msg‘] when 33 then raise TemplateLimitExceededError, r[‘data‘][0][‘msg‘] else raise Error, r[‘data‘][0][‘msg‘] end end end # 异步处理 def deliver_async(mobile, message, at: nil) end end end
voice.rb
module ExternalAPI class Voice Error = Class.new(StandardError) RateLimitExceededError = Class.new(Error) TemplateLimitExceededError = Class.new(Error) def deliver(mobile, code) r = ChinaSMS.voice_to(mobile, code) if r.key?(‘code‘) case r[‘code‘] when 0 then return when 8 then raise RateLimitExceededError, r[‘detail‘] when 33 then raise TemplateLimitExceededError, r[‘detail‘] else raise Error, r[‘detail‘] end else raise r[‘msg‘].presence || ‘语音验证码推送失败‘ unless r[‘count‘].to_i.positive? end end # 异步处理 def deliver_async(mobile, code, at: nil) end end end
发送短信嘛,肯定是要异步发送,不能堵塞,所以我们在 workers 文件夹下新建 external_api 文件夹,其中有两个文件,分别对应发送短信和推送语音验证。
sms_deliver_worker.rb:
module ExternalAPI class SMSDeliverWorker include Sidekiq::Worker sidekiq_options retry: false def perform(mobile, message) ExternalAPI.sms.deliver(mobile, message) end end end
voice_deliver_worker.rb:
module ExternalAPI class VoiceDeliverWorker include Sidekiq::Worker sidekiq_options retry: false def perform(mobile, code) ExternalAPI.voice.deliver(mobile, code) end end end
写好之后我们还需要在 sms 和 voice 的 API 对应的 deliver_async 方法添加代码:
def deliver_async(mobile, message, at: nil) SMSDeliverWorker.perform_at(at || Time.current, mobile, message) end
def deliver_async(mobile, message, at: nil) VoiceDeliverWorker.perform_at(at || Time.current, mobile, code) end
接口就已经写好了,我们再新建 Controller,来响应前端发送短信/推送语音的请求。
5.编写 Controller
需要两个 Controller 来处理短信验证和语音验证请求,我们分别命名为:verification_codes_controller、voice_codes_controller,
render_ajax_success、render_ajax_failure 点这里
verification_codes_controller.rb:
class VerificationCodesController < ApplicationController skip_before_action :authenticate_user!, only: [:create] def create verification_code = VerificationCode.regenerate_verification_code_for_mobile!(verification_code_params[:mobile]) # 这里是为了方便在开发时做测试,不需要浪费云片资源, if Figaro.env.OMNIAUTH_ALLOW_DEVELOPER_STRATEGY.present? render_ajax_success(developer_notice: format(‘模拟登录已开启,您的验证码是%s‘, verification_code.code)) else # # 通过 ExternalAPI.sms.deliver 调用并传参就好了 ExternalAPI.sms.deliver(verification_code.mobile, format(‘您的验证码是%s‘, verification_code.code)) render_ajax_success end # 这里通过在 ExternalAPI 中已经定义好的获取返回码的错误信息来进行处理 rescue ExternalAPI::SMS::RateLimitExceededError render_ajax_failure(message: ‘操作过于频繁,请稍后再试‘) rescue ExternalAPI::SMS::TemplateLimitExceededError render_ajax_failure(message: ‘操作过于频繁,请稍后再试‘) rescue ActiveRecord::RecordInvalid => e return render_ajax_failure(message: ‘手机号不能为空‘) if e.record.errors.added?(:mobile, :blank) return render_ajax_failure(message: e.record.errors[:mobile]) if e.record.errors.key?(:mobile) raise end private def verification_code_params params.require(:verification_code).permit(:mobile) end end
voice_codes_controller.rb:
class VoiceCodesController < ApplicationController skip_before_action :authenticate_user!, only: [:create] def create verification_code = VerificationCode.regenerate_verification_code_for_mobile!(voice_code_params[:mobile]) # 通过 ExternalAPI.voice.deliver 调用并传参就好了 ExternalAPI.voice.deliver(verification_code.mobile, verification_code.code) render_ajax_success rescue ExternalAPI::Voice::RateLimitExceededError render_ajax_failure(message: ‘操作过于频繁,请稍后再试‘) rescue ExternalAPI::Voice::TemplateLimitExceededError render_ajax_failure(message: ‘操作过于频繁,请稍后再试‘) rescue ActiveRecord::RecordInvalid => e return render_ajax_failure(message: ‘手机号不能为空‘) if e.record.errors.added?(:mobile, :blank) return render_ajax_failure(message: e.record.errors[:mobile]) if e.record.errors.key?(:mobile) raise end private def voice_code_params params.require(:voice_code).permit(:mobile) end end
到这里就是所有后台的代码了,前端的处理如倒计时、表单、显示错误信息等代码点