一、问题背景
为了防止垃圾信息发布机器人的自动提交攻击,采用CAPTCHA验证码来保护该模块,提高攻击者的成本。
二、验证码简介
全自动区分计算机和人类的图灵测试(Completely Automated Public Turing test to tell Computers and Humans Apart,简称CAPTCHA)俗称验证码,是一种区分用户是计算机和人的公共全自动程序。在CAPTCHA测试中,作为服务器的计算机会自动生成一个问题由用户来解答。这个问题可以由计算机生成并评判,但是必须只有人类才能解答。由于计算机无法解答CAPTCHA的问题,所以回答出问题的用户就可以被认为是人类。
验证码作为一种辅助安全手段在Web安全中有着特殊的地位,验证码安全和web应用中的众多漏洞相比似乎微不足道,但是千里之堤毁于蚁穴,有些时候如果能绕过验证码,则可以把手动变为自动,对于Web安全检测有很大的帮助。大部分验证码的设计者都不知道为什么要用到验证码,或者对于如何检验验证码的强度没有任何概念。大多数验证码在实现的时候只是把文字印到背景稍微复杂点的图片上就完事了,程序员没有从根本上了解验证码的设计理念。
JCaptcha即为Java版本的CAPTCHA项目,其是一个开源项目,支持生成图形和声音版的验证码,声音版的验证码需要使用到FreeTTS。
其原理是:服务器端首先随机产生一个字符串,生成一个图片,在后台存储一份。在做验证的时候,通过在后台传进去request参数获取到后台存储的值与输入的值进行比对。基于Servlet的使用方式可以参考官网的tutorial( https://jcaptcha.atlassian.net/wiki/display/general/5+minutes+application+integration+tutorial)
JCaptcha的架构图如下所示:
三、问题分析
JCaptcha默认的实现是基于单机模式(MapCaptchaStore存储信息单机HashMap中),为了适应集群环境可以把验证信息存储在session中,但是要求Web服务器配置session stick或者Session复制。为了实现负载均衡且避免session复制带来的性能损失,集群部署方案是完全分布式的,既不是session stick也不进行session复制。进行验证时,由A节点到B节点进行验证,B节点CaptchaStore中store中得不到当前验证码,无法进行验证。
由上可知,如果把验证码统一存储在一个地方,问题将迎刃而解,故考虑自定义CaptchaStore采用memcache来存储,如下图所示:
四、具体实施
1)在pom.xml加入依赖:
<
dependency
>
<
groupId
>com.octo.captcha</
groupId
>
<
artifactId
>jcaptcha-all</
artifactId
>
<
version
>1.0-RC6</
version
>
</
dependency
>
|
2)JCaptcha与Spring集成配置
applicationContext.xml:
<
bean
id
=
"imageCaptchaService"
class
=
"com.octo.captcha.service.image.DefaultManageableImageCaptchaService"
>
<
constructor-arg
type
=
"com.octo.captcha.service.captchastore.CaptchaStore"
index
=
"0"
>
<
ref
bean
=
"myCaptchaStore"
/>
</
constructor-arg
>
<!--which captcha Engine you use-->
<
constructor-arg
type
=
"com.octo.captcha.engine.CaptchaEngine"
index
=
"1"
>
<
ref
bean
=
"myCaptchaEngine"
/>
</
constructor-arg
>
<
constructor-arg
index
=
"2"
>
<
value
>180</
value
>
</
constructor-arg
>
<
constructor-arg
index
=
"3"
>
<
value
>100000</
value
>
</
constructor-arg
>
<
constructor-arg
index
=
"4"
>
<
value
>75000</
value
>
</
constructor-arg
>
</
bean
>
<
bean
id
=
"myCaptchaStore"
class
=
"com.xxx.util.MyCaptchaStore"
/>
<!--you can define more than one captcha engine here -->
<
bean
id
=
"myCaptchaEngine"
class
=
"com.xxx.util.MyCaptchaEngine"
/>
|
3)定制CaptchaStore
/**
* 定制CaptchaStore
* 线上集群环境,前端可能从A服务器取得验证码,而验证是到B服务器
* 默认的hashmap store是保存在单个Jvm内存中的,这样验证就会有问题
*
*/
public
class
MyCaptchaStore
implements
CaptchaStore {
//CacheService负责封装memcache访问功能
@Resource
private
CacheService cacheService;
@Override
public
boolean
hasCaptcha(String id) {
CaptchaAndLocale captcha = cacheService.getCaptcha(id);
return
captcha ==
null
?
false
:
true
;
}
@Override
public
void
storeCaptcha(String id, Captcha captcha)
throws
CaptchaServiceException {
try
{
cacheService.setCaptcha(id,
new
CaptchaAndLocale(captcha));
}
catch
(Exception e) {
throw
new
CaptchaServiceException(e);
}
}
@Override
public
void
storeCaptcha(String id, Captcha captcha, Locale locale)
throws
CaptchaServiceException {
try
{
c
acheService.setCaptcha(id,
new
CaptchaAndLocale(captcha,locale));
}
catch
(Exception e) {
throw
new
CaptchaServiceException(e);
}
}
@Override
public
boolean
removeCaptcha(String id) {
return c
acheService.removeCaptcha(id);
}
@Override
public
Captcha getCaptcha(String id)
throws
CaptchaServiceException {
CaptchaAndLocale captchaAndLocale = cacheService.getCaptcha(id);
return
captchaAndLocale !=
null
? (captchaAndLocale.getCaptcha()) :
null
;
}
@Override
public
Locale getLocale(String id)
throws
CaptchaServiceException {
CaptchaAndLocale captchaAndLocale = cacheService.getCaptcha(id);
return
captchaAndLocale !=
null
? (captchaAndLocale.getLocale()) :
null
;
}
@Override
public
int
getSize() {
return
0
;
}
@Override
public
Collection getKeys() {
return
null
;
}
@Override
public
void
empty() {
}
@Override
public
void
initAndStart() {
}
@Override
public
void
cleanAndShutdown() {
}
}
|
4)定制验证码Engine
/**
* 验证码Engine
*
*/
public
class
MyCaptchaEngine
extends
ListImageCaptchaEngine {
protected
void
buildInitialFactories() {
int
minWordLength =
4
;
int
maxWordLength =
4
;
int
fontSize =
20
;
int
imageWidth =
100
;
int
imageHeight =
30
;
WordGenerator wordGenerator =
new
RandomWordGenerator(
"0123456789abcdefghijklmnopqrstuvwxyz"
);
TextPaster randomPaster =
new
DecoratedRandomTextPaster(minWordLength,
maxWordLength,
new
RandomListColorGenerator(
new
Color[] {
new
Color(
23
,
170
,
27
),
new
Color(
220
,
34
,
11
),
new
Color(
23
,
67
,
172
) }),
new
TextDecorator[] {});
BackgroundGenerator background =
new
UniColorBackgroundGenerator(
imageWidth, imageHeight, Color.LIGHT_GRAY);
FontGenerator font =
new
RandomFontGenerator(fontSize, fontSize,
new
Font[] {
new
Font(
"nyala"
, Font.BOLD, fontSize),
new
Font(
"Bell MT"
, Font.PLAIN, fontSize),
new
Font(
"Credit valley"
, Font.BOLD, fontSize) });
ImageDeformation postDef =
new
ImageDeformationByFilters(
new
ImageFilter[] {});
ImageDeformation backDef =
new
ImageDeformationByFilters(
new
ImageFilter[] {});
ImageDeformation textDef =
new
ImageDeformationByFilters(
new
ImageFilter[] {});
WordToImage word2image =
new
DeformedComposedWordToImage(font,
background, randomPaster, backDef, textDef, postDef);
addFactory(
new
GimpyFactory(wordGenerator, word2image));
}
}
|
5)生成验证码图片
/**
* 验证码
*/
@Controller
public
class
CaptchaController {
private
static
final
Logger LOGGER = LoggerFactory.getLogger(CaptchaController.
class
);
@Resource
private
ImageCaptchaService imageCaptchaService;
@RequestMapping
(value =
"/jcaptcha"
)
public
void
ImageCaptcha(HttpServletRequest request , HttpServletResponse response)
throws
IOException {
String captchaId = UUID.randomUUID().toString();
BufferedImage image = imageCaptchaService.getImageChallengeForID(captchaId, request.getLocale());
response.setHeader(
"Cache-Control"
,
"no-store"
);
response.setHeader(
"Pragma"
,
"no-cache"
);
response.setDateHeader(
"Expires"
,
0
);
response.setContentType(
"image/jpeg"
);
Cookie cookie =
new
Cookie(Constants.CAPTCHA_COOKIE_NAME,captchaId);
cookie.setMaxAge(
30
*
60
);
response.addCookie(cookie);
ServletOutputStream responseOutputStream = response.getOutputStream();
ImageIO.write(image,
"jpg"
, responseOutputStream);
try
{
responseOutputStream.flush();
}
finally
{
responseOutputStream.close();
}
}
}
|
6)验证过程
@RequestMapping
(value =
"/test"
,method = RequestMethod.POST)
@ResponseBody
public
JsonResponse test(
@RequestParam
(value =
"content"
) String content,
@RequestParam
(value =
"authCode"
) String authCode,
HttpServletRequest request) {
String captchaId =
null
;
Cookie[] cookies = request.getCookies();
for
(Cookie cookie : cookies) {
if
(cookie.getName().equals(Constants.CAPTCHA_COOKIE_NAME)) {
captchaId = cookie.getValue();
break
;
}
}
if
(StringUtil.isBlank(captchaId)) {
return
new
JsonResponse(
40401
,
"验证码错误"
);
}
Boolean flag =
false
;
try
{
flag = imageCaptchaService.validateResponseForID(captchaId, authCode);
}
catch
(CaptchaServiceException cse) {
}
if
(!flag) {
return
new
JsonResponse(
40401
,
"验证码错误"
);
}
doSomething(); //业务任务
return
new
JsonResponse(
200
,
"success"
);
}
五、参考资料
|