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
到这里就是所有后台的代码了,前端的处理如倒计时、表单、显示错误信息等代码点这里