目录
简介
验证码是防止有人利用机器人自动批量注册、对特定的注册用户用特定程序暴力破解方 式进行不断的登录、灌水。因为验证码是一个混合了数字或符号的图片,人眼看起来都费劲, 机器识别起来就更困难。
分析
登录表单很可能遭到模拟登录的暴力破解攻击,要么轻易获得特定账户的登录信息,要么给服务器增加了大量的负荷。解决的办法,一般就是在登录前给出一个随机的信息(验证码),非法的非 Web 途径登录者会看不到这个验证码,从而让用户安全登录。为防止攻击者破获验证码,需将验证信息作为图像显示在 Web 上。
代码实现:
利用 Servlet 实现一个 4 位的彩色验证码数据。
import java.io.*;
import java.awt.*;
import java.awt.image.*;
import java.util.*;
import javax.imageio.*;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@WebServlet("/Image")
public class Image extends HttpServlet {
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("image/jpeg");
//禁止图像缓存
response.setHeader("Pragma","No-cache");
response.setHeader("Cache-Control","no-cache");
response.setDateHeader("Expires", 0);
HttpSession session=request.getSession();
// 在内存中创建图象
int width=100, height=40;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 获取图形上下文
Graphics g = image.getGraphics();
//生成随机类
Random random = new Random();
// 设定背景色
g.setColor(getRandColor(200,250));
g.fillRect(0, 0, width, height);
//设定字体
g.setFont(new Font("Times New Roman",Font.PLAIN,28));
// 随机产生155条干扰线,使图象中的认证码不易被其它程序探测到
g.setColor(getRandColor(160,200));
for (int i=0;i<155;i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x,y,x+xl,y+yl);
}
// 取随机产生的认证码(4位数字)
String sRand="";
//准备一个数字加字母字典
String string = "1234567890abcdefghijklmnopqrstuvwxyz";
for (int i=0;i<4;i++){
char rand=string.charAt(random.nextInt(string.length()));
sRand+=rand;
// 将认证码显示到图象中
//调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
g.setColor(new Color(20+random.nextInt(110),20+random.nextInt(110),20+random.nextInt(110)));
g.drawString(Character.toString(rand),13*i+25,30);
}
// 将认证码存入SESSION
session.setAttribute("rand",sRand);
// 图象生效
g.dispose();
ServletOutputStream responseOutputStream =response.getOutputStream();
// 输出图象到页面
ImageIO.write(image, "JPEG", responseOutputStream);
//以下关闭输入流!
responseOutputStream.flush();
responseOutputStream.close();
}
//给定范围获得随机颜色
Color getRandColor(int fc,int bc){
Random random = new Random();
if(fc>255) fc=255;
if(bc>255) bc=255;
int r=fc+random.nextInt(bc-fc);
int g=fc+random.nextInt(bc-fc);
int b=fc+random.nextInt(bc-fc);
return new Color(r,g,b);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
}
写完后保存文件,启动服务器,访问 Servlet 后可以看到页面中生成了一张图片验证码
测试与运行
该 Servlet 仅仅是一个验证码图像,接下来我们用 jsp 编写一个包含该验证码的登录页面
jsp 程序代码如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" +request.getServerPort() + path;
%>
<!DOCTYPE HTML>
<html>
<head>
<title>表单验证</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<META HTTP-EQUIV="Pragma" CONTENT="no-cache">
<META HTTP-EQUIV="Cache-Control" CONTENT="no-cache">
<META HTTP-EQUIV="Expires" CONTENT="0">
<style>
div {
width: 240px;
height: 100%;
margin: 30px auto;
}
label {
margin: 5px auto;
height: 22px;
line-height: 22px;
float: left;
}
.input {
margin: 5px auto;
height: 20px;
float: right;
}
#submit {
width: 100px;
height: 35px;
margin: 5px 8px;
}
</style>
</head>
<body>
<div>
<form id="myForm" action="" method="post" autocomplete="off" >
<label>用户名:</label><input class="input" type="text" name="name" placeholder="请输入用户名"><br>
<label>密码:</label><input class="input" type="password" name="password" placeholder="请输入密码"><br>
<div style="margin-top: 10px;">
<div style="height: 10px;width: 200px;margin-bottom: 0px"> </div>
<input style="width: 110px;height: 33px;margin-right: 20px;float: left " type="text" placeholder="图片验证码" name="code">
<img style="float: right" title="看不清?点击切换。" id="changeServletImg" src="<%=basePath%>/Image/Image">
</div>
<input style="margin-left: 140px" id="submit" type="button" value="登录">
</form>
<div id="errorTips" style="text-align: center;font-size: 18px;color: red"></div>
</div>
</body>
<script>
window.onload = function () {
var changeServletImgDOM = document.getElementById('changeServletImg');
//点击图片时改变验证码
changeServletImgDOM.onclick=change;
function change() {
changeServletImgDOM.src="<%=basePath%>/Image/Image?"+new Date().getTime();
};
document.getElementById('submit').onclick = check;
function check () {
//获取表单数据
var name = document.getElementsByName('name')[0].value;
var password = document.getElementsByName('password')[0].value;
var code = document.getElementsByName('code')[0].value;
var formInfo = 'name=' + name + '&password=' + password + '&code=' + code;
var xhr = new XMLHttpRequest();
//设置请求参数
xhr.open("post","Example_13",true);
xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded;charset=utf-8");
//发送请求
xhr.send(formInfo);
// 设置响应 HTTP 请求状态变化的函数
xhr.onreadystatechange = function() {
if (xhr.status == 200 && xhr.readyState == 4) {
if(xhr.responseText == 'OK') {
// 验证成功时跳转页面
location.href = 'loginSuccess.html';
} else {
// 验证失败时给出错误提示
document.getElementById('errorTips').innerText = xhr.responseText;
}
}
}
}
}
</script>
</html>
loginSuccess.html 代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>欢迎</title>
</head>
<body>
<h1>登录成功!</h1>
</body>
</html>
由于 jsp 先于 java 执行,导致 jsp 页面图片验证码获取的session值始终是示前一个。
所以无法直接在填写表单的页面完成验证码的验证。
接下来编写一个 Servlet 来接收由 AJAX 发送的表单数据,然后通过 AJAX 完成错误信息的回显,以及页面跳转。
(为了方便,这里用 equals 进行验证,不连接数据库) 【用户名和密码为 admin】
servlet 程序代码如下:
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet("/Example_13")
public class Example_13 extends HttpServlet {
//判断用户是否存在 (为了方便,这里用 equals 进行验证,不连接数据库)
public boolean isAdmin(String name,String password){
//假设用户不存在
boolean isExist = false;
if("admin".equals(name) && "admin".equals(password)) {
isExist = true;
}
return isExist;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();
//获取表单数据
String name = req.getParameter("name");
String password = req.getParameter("password");
String code = req.getParameter("code");
//获取图片验证码
HttpSession session = req.getSession();
String rand = (String)session.getAttribute("rand");
//验证图片验证码
if(rand.equals(code)) {
getConnect();
//验证用户名 密码
if(isAdmin(name,password)) {
closeConnect();
out.print("OK");
} else {
out.print("用户名或密码错误!");
}
} else {
out.print("验证码错误,验证失败!");
}
}
}
效果如下: