最近公司有个项目被客户拿去进行漏洞扫描,发现用户登录太简单,容易被暴力破解。当然发现的问题很多,什么反射型XSS,存储型XSS,敏感信息泄露等等。但是我们今天不讲这么多,就说说如何修复暴力破解的问题。由于登录界面未设置图形验证码,也没有对同一用户在连续登录错误时进行限制,导致攻击者可以尝试破解任意已知用户的密码。所以针对这种情况,第一,对同一用户连续登录在规定时间内的次数进行限制,超过则锁定用户。第二,添加图形验证码。
首先给你们看看我的项目结构:
user.sql数据:
SET FOREIGN_KEY_CHECKS=0;-- ------------------------------ Table structure for user-- ----------------------------DROP TABLE IF EXISTS `user`;CREATE TABLE user (`id` bigint(20) NOT NULL AUTO_INCREMENT,`username` varchar(50) NOT NULL COMMENT '用户名',`password` varchar(32) NOT NULL COMMENT '密码,加密存储',`islocked` varchar(10) DEFAULT "0" COMMENT '是否锁定',`firsttime` varchar(50) DEFAULT NULL COMMENT '第一次登录错误时间',`count` varchar(10) DEFAULT NULL COMMENT '登录错误次数',PRIMARY KEY (`id`),UNIQUE KEY `username` (`username`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8 COMMENT='用户表';-- ------------------------------ Records of user-- ----------------------------INSERT INTO user VALUES (null, 'admin', 'e10adc3949ba59abbe56e057f20f883e', '0', null, null);INSERT INTO user VALUES (null, 'nanshan', 'e10adc3949ba59abbe56e057f20f883e', '0', null, null);
新建数据库test,导入user.sql即可。
web层:
VerifyCodeServlet.java
package .servlet;import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import .util.VerifyCodeUtils;/*** * @author saule * @date 6月28日 下午11:39:19* @Description 验证码servlet*/public class VerifyCodeServlet extends HttpServlet {private static final long serialVersionUID = 1L;public void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {doPost(request, response);}public void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {int width = 100;int height = 35;//获取验证码String verifyCode = VerifyCodeUtils.generateVerifyCode(4);//将验证码保存到sessionrequest.getSession().setAttribute("verifyCode", verifyCode);//向页面输出验证码 VerifyCodeUtils.outputImage(width, height, response.getOutputStream(), verifyCode);}}
LoginServlet.java
package .servlet;import java.io.IOException;import java.util.HashMap;import java.util.Map;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import net.sf.json.JSONArray;import .pojo.User;import .service.UserService;/*** 正常用户登录流程:* 1,判断验证码是否正确。*2,判断用户是否锁定。*3,根据用户名称查找,判断用户是否存在。*4,判断密码是否正确。*/public class LoginServlet extends HttpServlet {private static final long serialVersionUID = 1L;public void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {response.setContentType("text/xml;charset=utf-8");String username=request.getParameter("username");String password=request.getParameter("password");String vercode=request.getParameter("vercode");String sessionCode=(String) request.getSession().getAttribute("verifyCode");//清除session中的验证码request.getSession().removeAttribute("verifyCode");Map<String,String> map=new HashMap<>();//验证码错误if(sessionCode!=null && !sessionCode.equals(vercode.toUpperCase())){map.put("errcode", "0");// 转换成jsonString json = JSONArray.fromObject(map).toString();response.getWriter().write(json);response.getWriter().close();return;}System.out.println(username+":"+password);UserService userService=new UserService();User loginUser=new User();loginUser.setUsername(username);loginUser.setPassword(password);Map<String, Object> result = userService.login(loginUser);if(result.get("errcode").equals("4")){//验证成功request.getSession().setAttribute("username", username);request.getSession().setAttribute("password", password);
//用ajax请求用户登录,重定向和转发都是失效的。 //request.getRequestDispatcher("/WEB-INF/success.jsp").forward(request, response); }// 转换成jsonString json = JSONArray.fromObject(result).toString();response.getWriter().write(json);response.getWriter().close();}public void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {doGet(request, response);}}
Service层:
UserService.java
package .service;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Date;import java.util.HashMap;import java.util.Map;import .dao.UserDao;import .pojo.User;import .util.Md5Utils;/*** * @author saule * @date 6月27日 上午10:50:44* @Description*/public class UserService {private static final String USER_NOT_EXIST="1";private static final String USER_IS_LOCK="2";private static final String PASSWORD_ERROR="3";private static final String VERIFICATION_SUCCESS="4";SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//登录操作public Map<String,Object> login(User loginUser) {Map<String,Object> loginMap=new HashMap<String, Object>();UserDao userDao=new UserDao();User user=userDao.findByName(loginUser.getUsername());String pwd = Md5Utils.md5(loginUser.getPassword());if(user==null){//用户不存在loginMap.put("errcode", USER_NOT_EXIST);}else if(user.getIslocked().equals("1")){//用户被锁定loginMap.put("errcode", USER_IS_LOCK); }else if(!user.getPassword().equals(pwd)){//密码错误//超过规定时间,次数不超过4次登录时,将时间与次数重置。if(user.getFirsttime()!=null){try {long firsttime=sdf.parse(user.getFirsttime()).getTime();long nowtime=new Date().getTime();if((nowtime-firsttime)>5*60*1000){user.setFirsttime(null);user.setCount("0");userDao.update(user);}} catch (ParseException e) {e.printStackTrace();}}if(user.getFirsttime()==null){//初次登录密码错误,记录数据。user.setFirsttime(sdf.format(new Date()));user.setCount("1");userDao.update(user);}else if(user.getFirsttime()!=null && Integer.valueOf(user.getCount())<3){//连续登陆错误次数小于4次String newCount = String.valueOf(Integer.valueOf(user.getCount())+1);user.setCount(newCount);userDao.update(user);}else{//大于4次且时间间隔小于5分钟则锁定用户。禁止登录。try {long firsttime=sdf.parse(user.getFirsttime()).getTime();long nowtime=new Date().getTime();if((nowtime-firsttime)<5*60*1000){user.setFirsttime(null);user.setCount("0");user.setIslocked("1");//1表示锁定用户 userDao.update(user);}} catch (ParseException e) {e.printStackTrace();}}loginMap.put("errcode", PASSWORD_ERROR); loginMap.put("user", user);}else{loginMap.put("errcode", VERIFICATION_SUCCESS);}return loginMap;}}
Dao层:
UserDao.java
package .dao;import java.sql.Connection;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.SQLException;import .pojo.User;import .util.JdbcUtils;public class UserDao {//根据用户查询public User findByName(String username) {Connection conn=null;PreparedStatement pst=null;ResultSet rs=null;String sql="select * from user where username=?";try {conn=JdbcUtils.getConnection();pst=conn.prepareStatement(sql);pst.setString(1, username);rs=pst.executeQuery();if(rs.next()){User user=new User();user.setId(rs.getInt("id"));user.setUsername(rs.getString("username"));user.setPassword(rs.getString("password"));user.setIslocked(rs.getString("islocked"));user.setFirsttime(rs.getString("firsttime"));user.setCount(rs.getString("count"));return user;}} catch (SQLException e) {e.printStackTrace();}finally{try {JdbcUtils.close(conn, pst, rs);} catch (SQLException e) {e.printStackTrace();}}return null;}//修改用户信息public void update(User user) {Connection conn=null;PreparedStatement pst=null;String sql="update user set islocked=?,firsttime=?,count=? where id=?";try {conn=JdbcUtils.getConnection();pst = conn.prepareStatement(sql);pst.setString(1, user.getIslocked());pst.setString(2, user.getFirsttime());pst.setString(3, user.getCount());pst.setInt(4, user.getId());int row = pst.executeUpdate();if(row!=0){System.out.println("修改成功");}} catch (SQLException e) {e.printStackTrace();}finally{try {JdbcUtils.close(conn, pst, null);} catch (SQLException e) {e.printStackTrace();}}}}
实体类:
user.java
package .pojo;public class User {private Integer id;private String username;private String password;private String islocked;//是否锁定,0表示没有锁定;1表示锁定;private String firsttime;private String count;//连续登录错误次数public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public String getIslocked() {return islocked;}public void setIslocked(String islocked) {this.islocked = islocked;}public String getFirsttime() {return firsttime;}public void setFirsttime(String firsttime) {this.firsttime = firsttime;}public String getCount() {return count;}public void setCount(String count) {this.count = count;}}
帮助类:
VerifyCodeUtils.java
package .util;import javax.imageio.ImageIO;import java.awt.*;import java.awt.geom.AffineTransform;import java.awt.image.BufferedImage;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;import java.io.OutputStream;import java.util.Arrays;import java.util.Random;/*** 验证码生成工具* @author zl*/public class VerifyCodeUtils{//使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";private static Random random = new Random();/*** 使用系统默认字符源生成验证码* @param verifySize 验证码长度* @return*/public static String generateVerifyCode(int verifySize){return generateVerifyCode(verifySize, VERIFY_CODES);}/*** 使用指定源生成验证码* @param verifySize 验证码长度* @param sources 验证码字符源* @return*/public static String generateVerifyCode(int verifySize, String sources){if(sources == null || sources.length() == 0){sources = VERIFY_CODES;}int codesLen = sources.length();Random rand = new Random(System.currentTimeMillis());StringBuilder verifyCode = new StringBuilder(verifySize);for(int i = 0; i < verifySize; i++){verifyCode.append(sources.charAt(rand.nextInt(codesLen-1)));}return verifyCode.toString();}/*** 生成随机验证码文件,并返回验证码值* @param w* @param h* @param outputFile* @param verifySize* @return* @throws IOException*/public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException{String verifyCode = generateVerifyCode(verifySize);outputImage(w, h, outputFile, verifyCode);return verifyCode;}/*** 输出随机验证码图片流,并返回验证码值* @param w* @param h* @param os* @param verifySize* @return* @throws IOException*/public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException{String verifyCode = generateVerifyCode(verifySize);outputImage(w, h, os, verifyCode);return verifyCode;}/*** 生成指定验证码图像文件* @param w* @param h* @param outputFile* @param code* @throws IOException*/public static void outputImage(int w, int h, File outputFile, String code) throws IOException{if(outputFile == null){return;}File dir = outputFile.getParentFile();if(!dir.exists()){dir.mkdirs();}try{outputFile.createNewFile();FileOutputStream fos = new FileOutputStream(outputFile);outputImage(w, h, fos, code);fos.close();} catch(IOException e){throw e;}}/*** 输出指定验证码图片流* @param w* @param h* @param os* @param code* @throws IOException*/public static void outputImage(int w, int h, OutputStream os, String code) throws IOException{int verifySize = code.length();BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);Random rand = new Random();Graphics2D g2 = image.createGraphics();g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);Color[] colors = new Color[5];Color[] colorSpaces = new Color[] { Color.WHITE, Color.CYAN,Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,Color.PINK, Color.YELLOW };float[] fractions = new float[colors.length];for(int i = 0; i < colors.length; i++){colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];fractions[i] = rand.nextFloat();}Arrays.sort(fractions);g2.setColor(Color.GRAY);// 设置边框色g2.fillRect(0, 0, w, h);Color c = getRandColor(200, 250);g2.setColor(c);// 设置背景色g2.fillRect(0, 2, w, h-4);//绘制干扰线Random random = new Random();g2.setColor(getRandColor(160, 200));// 设置线条的颜色for (int i = 0; i < 20; i++) {int x = random.nextInt(w - 1);int y = random.nextInt(h - 1);int xl = random.nextInt(6) + 1;int yl = random.nextInt(12) + 1;g2.drawLine(x, y, x + xl + 40, y + yl + 20);}// 添加噪点float yawpRate = 0.05f;// 噪声率int area = (int) (yawpRate * w * h);for (int i = 0; i < area; i++) {int x = random.nextInt(w);int y = random.nextInt(h);int rgb = getRandomIntColor();image.setRGB(x, y, rgb);}shear(g2, w, h, c);// 使图片扭曲g2.setColor(getRandColor(100, 160));int fontSize = h-4;Font font = new Font("Algerian", Font.ITALIC, fontSize);g2.setFont(font);char[] chars = code.toCharArray();for(int i = 0; i < verifySize; i++){AffineTransform affine = new AffineTransform();affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize/2, h/2);g2.setTransform(affine);g2.drawChars(chars, i, 1, ((w-10) / verifySize) * i + 5, h/2 + fontSize/2 - 10);}g2.dispose();ImageIO.write(image, "jpg", os);}private static Color getRandColor(int fc, int bc) {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);}private static int getRandomIntColor() {int[] rgb = getRandomRgb();int color = 0;for (int c : rgb) {color = color << 8;color = color | c;}return color;}private static int[] getRandomRgb() {int[] rgb = new int[3];for (int i = 0; i < 3; i++) {rgb[i] = random.nextInt(255);}return rgb;}private static void shear(Graphics g, int w1, int h1, Color color) {shearX(g, w1, h1, color);shearY(g, w1, h1, color);}private static void shearX(Graphics g, int w1, int h1, Color color) {int period = random.nextInt(2);boolean borderGap = true;int frames = 1;int phase = random.nextInt(2);for (int i = 0; i < h1; i++) {double d = (double) (period >> 1)* Math.sin((double) i / (double) period+ (6.2831853071795862D * (double) phase)/ (double) frames);g.copyArea(0, i, w1, 1, (int) d, 0);if (borderGap) {g.setColor(color);g.drawLine((int) d, i, 0, i);g.drawLine((int) d + w1, i, w1, i);}}}private static void shearY(Graphics g, int w1, int h1, Color color) {int period = random.nextInt(40) + 10; // 50;boolean borderGap = true;int frames = 20;int phase = 7;for (int i = 0; i < w1; i++) {double d = (double) (period >> 1)* Math.sin((double) i / (double) period+ (6.2831853071795862D * (double) phase)/ (double) frames);g.copyArea(i, 0, 1, h1, 0, (int) d);if (borderGap) {g.setColor(color);g.drawLine(i, (int) d, i, 0);g.drawLine(i, (int) d + h1, i, h1);}}}public static void main(String[] args) throws IOException{File dir = new File("F:/verifies");int w = 200, h = 80;String verifyCode = generateVerifyCode(4);File file = new File(dir, verifyCode + ".jpg");outputImage(w, h, file, verifyCode);}}
Md5Utils.java
package .util;import java.math.BigInteger;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;public class Md5Utils {/*** 使用md5的算法进行加密*/public static String md5(String plainText) {byte[] secretBytes = null;try {secretBytes = MessageDigest.getInstance("md5").digest(plainText.getBytes());} catch (NoSuchAlgorithmException e) {throw new RuntimeException("没有md5这个算法!");}String md5code = new BigInteger(1, secretBytes).toString(16);// 16进制数字// 如果生成数字未满32位,需要前面补0for (int i = 0; i < 32 - md5code.length(); i++) {md5code = "0" + md5code;}return md5code;}}
JdbcUtils.java
package .util;import java.sql.Connection;import java.sql.DriverManager;import java.sql.ResultSet;import java.sql.SQLException;import java.sql.Statement;import java.util.ResourceBundle;/*** @author saule * @date 6月27日 上午10:23:58* @Description JDBC帮助类*/public class JdbcUtils {private static String driver="com.mysql.jdbc.Driver";private static String url="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8";private static String username="root";private static String password="root";static {try {// 将加载驱动操作,放置在静态代码块中.这样就保证了只加载一次. Class.forName(driver);} catch (ClassNotFoundException e) {e.printStackTrace();}}public static Connection getConnection() throws SQLException {//获取连接Connection con = DriverManager.getConnection(url, username, password);return con;}//关闭操作public static void close(Connection con,Statement st,ResultSet rs) throws SQLException{if(con!=null){con.close();}else if(st!=null){st.close();}else if(rs!=null){rs.close();}}}
web.xml
<?xml version="1.0" encoding="UTF-8"?><web-app xmlns:xsi="/2001/XMLSchema-instance" xmlns="/xml/ns/javaee" xsi:schemaLocation="/xml/ns/javaee /xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"><display-name>youdu</display-name><servlet><servlet-name>VerifyCodeServlet</servlet-name><servlet-class>.servlet.VerifyCodeServlet</servlet-class></servlet><servlet><servlet-name>LoginServlet</servlet-name><servlet-class>.servlet.LoginServlet</servlet-class></servlet><servlet-mapping><servlet-name>VerifyCodeServlet</servlet-name><url-pattern>/getVerifyCode</url-pattern></servlet-mapping><servlet-mapping><servlet-name>LoginServlet</servlet-name><url-pattern>/login</url-pattern></servlet-mapping><welcome-file-list><welcome-file>login.jsp</welcome-file></welcome-file-list></web-app>
前端页面:
login.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"pageEncoding="UTF-8"%><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "/TR/html4/loose.dtd"><html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>登录页面</title><style type="text/css">#login{border: 1px solid deepskyblue;background-color: blanchedalmond;width:400px;height:250px;padding-left: 100px;padding-top: 50px;}</style><script type="text/javascript">//创建一个ajax对象function getLoginAjax(){var loginRequest;if(window.XMLHttpRequest){loginRequest=new XMLHttpRequest();}else if(window.ActiveXObject){//window对象中有ActiveXObject属性存在就是IE浏览器的低版本try{loginRequest= new ActiveXObject("Msxml2.XMLHTTP");}catch(e){loginRequest= new ActiveXObject("Microsoft.XMLHTTP");}}return loginRequest;}//登录方法function userlogin(){//获取参数var username=document.getElementById("username").value;var password=document.getElementById("password").value;var vercode=document.getElementById("vercode").value;/* 此处省略非空校验 */var loginAjax=getLoginAjax();loginAjax.open("POST", "login", true);//请求头 loginAjax.setRequestHeader("Content-Type","application/x-www-form-urlencoded");loginAjax.send("username="+username+"&password="+password+"&vercode="+vercode);//设置回调函数 loginAjax.onreadystatechange = function() {//处理响应数据 当信息全部返回,并且是成功if (loginAjax.readyState == 4 && loginAjax.status == 200) {//返回的数值类型一般是json格式,比较常见console.log();var jsonObj = eval("(" + loginAjax.responseText + ")");for(var i=0;i<jsonObj.length;i++){var errcode = jsonObj[i].errcode;if(errcode==0){alert("验证码错误");identifyload();return false;}else if(errcode==1){alert("用户不存在");identifyload();return false;}else if(errcode==2){alert("用户被锁定,请联系管理。");return false;}else if(errcode==3){var user = jsonObj[i].user;var locked=user.islocked;var count=user.count;if(locked=='0' && count<4){alert("密码错误,你还有"+(4-count)+"次机会");identifyload();}else if(locked=='1'){alert("用户被锁定,请联系管理。");}return false;}else if(errcode==4){alert("登录成功");window.location="http://localhost:8080/youdu/success.jsp";}}}};}function identifyload() {document.getElementById("imgcode").src = 'getVerifyCode?nowtime='+ new Date().getTime();}</script></head><body><div id="login"><form action="./login" method="Post" name="login"><table><tr><td>用户名:</td><td><input name="username" type="text" placeholder="请输入昵称" id="username"/><br/><br/></td></tr><tr><td>密码:</td><td><input name="password" type="password" placeholder="请输入密码" id="password"/><br/><br/></td></tr><tr><td>验证码:</td><td><input name="vercode" type="text" id="vercode"/><img src="./getVerifyCode" height="32" id="imgcode" onclick="identifyload()" title="点击更换验证码"></td></tr><tr><td height="20"> </td></tr><tr><td><button type="button" value="登录" onclick="userlogin()">登录</button><button type="reset" value="重置">重置</button></td></tr></table></form></div></body></html>
success.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"pageEncoding="UTF-8"%><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "/TR/html4/loose.dtd"><html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>Insert title here</title></head><body><h3>登录成功</h3>用户名:${sessionScope.username}密码:${sessionScope.password}</body></html>
来个效果图:
最后说几句。虽然用户登录流程简单,但是用servlet和原生js、ajax实现还是有些坑的,特别是现在个个都习惯在框架下写代码,就比如ajax请求后,servlet重定向和转发都会失效,你得想办法登录成功后跳转系统页面。虽然都是比较基础的东西,对于初学者来说还是需要好好掌握。