图片验证码的使用场景就不用说了,在这里分享一下我是如何做登录图片验证码的。
生成验证码字符
-> 生成图片验证码
[-> 使用
]
第三步的意思是,当你拿到图片验证码时,看你的具体需求了,是直接随着 form 表单渲染, 还是浏览器异步地请求图片验证码,或者其它什么的,反正无所谓了。
simple_captcha2 是基于 ImageMagick 来生成图片,基于数据库来存储验证码数据的。
提供了 Controller Based
和 Model Based
两种用法。
Controller Based
simple_captcha2 提供了一对方法: show_simple_captcha
和 simple_captcha_valid?
。
前者是一个 view helper,让我们能够在某个地方显示一张验证码图片,后者让我们可以去判定用户提交的验证码是否正确。
想知道更多的用法可以移步到它的文档。
实现原理
simple_captcha2 基于数据库维护了一对 key -> value 数据,数据结构如下:
Table "public.simple_captcha_data"
Column | Type | Modifiers
------------+-----------------------------+------------------------------------------------------------------
id | integer | not null default nextval('simple_captcha_data_id_seq'::regclass)
key | character varying(40) |
value | character varying(6) |
created_at | timestamp without time zone |
updated_at | timestamp without time zone |
Indexes:
"simple_captcha_data_pkey" PRIMARY KEY, btree (id)
"simple_captcha_data_key" btree (key)
当我们调用 show_simple_captcha
时就会生成一对这样的值,然后存放到数据库(MySQL|Postgresql|Redis),然后在 view 里生成这样的内容。
<input type="hidden", name="captcha_key" value="d850ec25ca962ba6606cfe7c84f9568c8473e93e"
<img alt="captcha" src="/simple_captcha?code=d850ec25ca962ba6606cfe7c84f9568c8473e93e&time=1431749439" />
这样,它的工作原理就很清楚了。
key 的生成算法如下:
Digest::SHA1.hexdigest([session[:id], "captcha", Time.now.to_s].join)
value 的算法如下:
def generate_simple_captcha_data(code)
value = ''
case code
when 'numeric' then
SimpleCaptcha.length.times{value << (48 + rand(10)).chr}
else
SimpleCaptcha.length.times{value << (65 + rand(26)).chr}
end
return value
end
simple_captcha 实现了一个 middleware,以返回验证码图片。
module SimpleCaptcha
class Middleware
# ...
def call(env) # :nodoc:
if env["REQUEST_METHOD"] == "GET" && captcha_path?(env['PATH_INFO'])
make_image(env)
else
@app.call(env)
end
end
# ...
def make_image(env, headers = {}, status = 404)
request = Rack::Request.new(env)
code = request.params["code"]
body = []
if !code.blank? && Utils::simple_captcha_value(code)
return send_file(generate_simple_captcha_image(code), :type => 'image/jpeg', :disposition => 'inline', :filename => 'simple_captcha.jpg')
end
[status, headers, body]
end
#...
在生成验证码图片这一步,它是调用了 ImageMagick 的命令来生成图片的。
def self.run(cmd, params = "", expected_outcodes = 0)
command = %Q[#{cmd} #{params}].gsub(/\s+/, " ")
command = "#{command} 2>&1"
unless (image_magick_path = SimpleCaptcha.image_magick_path).blank?
command = File.join(image_magick_path, command)
end
# convert -size 100x100 -gravity "Center" -implode 0.2 <path/to/file>
output = `#{command}`
unless [expected_outcodes].flatten.include?($?.exitstatus)
raise ::StandardError, "Error while running #{cmd}: #{output}"
end
output
end
总结
分三步走:
simple_captcha2 为图片验证码提供了一整套的解决方案,在 Rails 里使用非常简单。 但仍有做得不足的地方,例如没有暴露生成验证码图片的接口给我们,使得我们能够直接通过 ajax 来请求验证码图片。 虽然可以通过 mokey patch 来做的,但是我自己想了一个更简单的方案。
我的思路:
伪代码
# in someone controller
# GET /captcha
def captcha
text = SecureRandom.hex(2).upcase
session[:captcha] = text
base64Image = Captcha.generate text, 126, 40 # 验证码的内容,图片的宽度,图片的高度
render json: { image: base64Image }
end
最关键的就是如何实现 Captcha.generate
了,使用纯 Ruby 生成图片成本高,需要许多知识,所以我还是选择了依赖于 ImageMagick。
思路也很简单:
convert
命令所需要的参数module Captcha
extend self
def generate text, width = 100, height = 28
text = text.upcase
params = ['-fill darkblue', '-background white']
params << "-size #{width}x#{height}"
params << "-wave #{distortion}"
params << '-gravity "Center"'
params << '-pointsize 22'
params << '-implode 0.2'
dst = Tempfile.new(['neolion_captcha', '.png'], Dir::tmpdir)
dst.binmode
params << "label:'#{text}' \"#{File.expand_path(dst.path)}\""
run(params.join(' '))
dst.close
read_as_base64 File.expand_path(dst.path)
end
private
def distortion
[0 + rand(2), 80 + rand(20)].join('x')
end
def run params = "", expected_outcodes = 0
command = %Q[convert #{params}].gsub(/\s+/, " ")
command = "#{command} 2>&1"
output = `#{command}`
unless [expected_outcodes].flatten.include?($?.exitstatus)
raise ::StandardError, "Error while running #{command}"
end
output
end
def read_as_base64 filepath
data = Base64.encode64(File.binread(filepath))
['data:image/png;base64,', data].join.gsub(/\n/, '')
end
end
至于判定用户的验证码是否正确,就很简单了
class ApplicationController < ActionController::Base
# ...
def valid_captcha?
session[:captcha] == params[:captcha]
end
# ...
end
要求是 Rails4.x+ 或者是 :db_store
,因为在 Rails 4 之前,如果是 :cookie_store,那么 session 数据在客户端是能够读取的,因为没有加密。