验证码原理详解与案例

来源:互联网 发布:sleep php函数毫秒 编辑:程序博客网 时间:2024/06/06 04:53

验证码技术的出现是为了防止对服务和数据库进行暴力攻击而设置的一道墙,客户端与服务端交互步骤如下图:

剩下的细节问题还有:

1, 验证码如何加噪成图片

2, 服务端如何维护验证码

 

案例代码在:https://github.com/yejingtao/forblog/tree/master/demo-securityCode

核心代码详解:

前端:

<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">    <head>        <title>Create user </title>    </head>    <body>        <form th:action="@{/login}" method="post">            <div><label> User Name : <input type="text" name="name"/> </label></div>            <div><label> User Password : <input type="password" name="password"/> </label></div>            <img src="/security" onclick="refreshSecurityCode(this);" />                         <input name="securityCode" size="8" />            <div><input type="submit" value="Login"/></div>        </form>    </body>    <script>function refreshSecurityCode(obj) {        obj.src = "/security?_t=" + Math.random();    }</script></html>

验证码code生成:原理很简单,就是随机字符串

public class SecurityCodeUtil {/** * 验证码难度级别,Simple只包含数字,Medium包含数字和小写英文,MediumPlus包含大小英文,Hard包含数字和大小写英文 */public enum SecurityCodeLevel {Simple, Medium, MediumPlus, Hard};/** * 产生默认验证码,4位中等难度 *  * @return String 验证码 */public static String getSecurityCode() {return getSecurityCode(4, SecurityCodeLevel.MediumPlus, false);}/** * 产生长度和难度任意的验证码 *  * @param length *            长度 * @param level *            难度级别 * @param isCanRepeat *            是否能够出现重复的字符,如果为true,则可能出现 5578这样包含两个5,如果为false,则不可能出现这种情况 * @return String 验证码 */public static String getSecurityCode(int length, SecurityCodeLevel level, boolean isCanRepeat) {// 随机抽取len个字符int len = length;// 字符集合(除去易混淆的数字0、数字1、字母l、字母o、字母O)char[] codes = { '1', '2', '3', '4', '5', '6', '7', '8', '9', //'a', 'b', 'c', 'd', 'e', 'f', 'g', //'h', 'i', 'j', 'k', 'm', 'n', //'p', 'q', 'r', 's', 't', //'u', 'v', 'w', 'x', 'y', 'z', //'A', 'B', 'C', 'D', 'E', 'F', 'G', //'H', 'I', 'J', 'K', 'L', 'M', 'N', //'P', 'Q', 'R', 'S', 'T', //'U', 'V', 'W', 'X', 'Y', 'Z' };// 根据不同的难度截取字符数组if (level == SecurityCodeLevel.Simple) {codes = ArrayUtils.copyOfRange(codes, 0, 9);} else if (level == SecurityCodeLevel.Medium) {codes = ArrayUtils.copyOfRange(codes, 0, 33);} else if (level == SecurityCodeLevel.MediumPlus) {codes = ArrayUtils.copyOfRange(codes, 34, codes.length);}// 字符集合长度int n = codes.length;// 抛出运行时异常if (len > n && isCanRepeat == false) {throw new RuntimeException(String.format("调用SecurityCode.getSecurityCode(%1$s,%2$s,%3$s)出现异常," //+ "当isCanRepeat为%3$s时,传入参数%1$s不能大于%4$s", len, level, isCanRepeat, n));}// 存放抽取出来的字符char[] result = new char[len];// 判断能否出现重复的字符if (isCanRepeat) {for (int i = 0; i < result.length; i++) {// 索引 0 and n-1int r = (int) (Math.random() * n);// 将result中的第i个元素设置为codes[r]存放的数值result[i] = codes[r];}} else {for (int i = 0; i < result.length; i++) {// 索引 0 and n-1int r = (int) (Math.random() * n);// 将result中的第i个元素设置为codes[r]存放的数值result[i] = codes[r];// 必须确保不会再次抽取到那个字符,因为所有抽取的字符必须不相同。// 因此,这里用数组中的最后一个字符改写codes[r],并将n减1codes[r] = codes[n - 1];n--;}}return String.valueOf(result);}}

前端技术很容易获取文本版的验证码,所以要以二进制流的形式返回加噪后的验证码,主要靠java.awt里的包:

public class SecurityImageSupport {/** * 返回验证码图片的流格式 *  * @param securityCode *            验证码 * @return ByteArrayInputStream 图片流 */public static ByteArrayInputStream getImageAsInputStream(String securityCode) {BufferedImage image = createImage(securityCode);return convertImageToStream(image);}public static byte[] getImageAsByte(String securityCode) {BufferedImage image = createImage(securityCode);return convertImageToByte(image);}/** * 生成验证码图片 *  * @param securityCode *            验证码字符 * @return BufferedImage 图片 */private static BufferedImage createImage(String securityCode) {// 验证码长度int codeLength = securityCode.length();// 字体大小int fSize = 13;int fWidth = fSize + 1;// 图片宽度int width = codeLength * fWidth + 15;// 图片高度int height = (int) (fSize * 1.5) + 1;// 图片BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);Graphics2D g = image.createGraphics();Color bgColor = new Color(239, 241, 249);// 设置背景色g.setColor(bgColor);// 填充背景g.fillRect(0, 0, width, height);// 设置边框颜色g.setColor(bgColor);// 边框字体样式g.setFont(new Font("Arial", Font.BOLD, height - 2));// 绘制边框g.drawRect(10, 10, width - 1, height - 1);// 绘制噪点Random rand = new Random();// 设置噪点颜色g.setColor(Color.LIGHT_GRAY);for (int i = 0; i < codeLength * 6; i++) {int x = rand.nextInt(width);int y = rand.nextInt(height);// 绘制1*1大小的矩形g.drawRect(x, y, 1, 1);}// 绘制验证码int codeY = height - 5;// 设置字体颜色和样式g.setColor(new Color(80, 25, 28));g.setFont(new Font("Georgia", Font.BOLD | Font.ITALIC, fSize));for (int i = 0; i < codeLength; i++) {g.drawString(String.valueOf(securityCode.charAt(i)), i * 16 + 5, codeY);}g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);Random r = new Random();CubicCurve2D cubic = new CubicCurve2D.Float(2, height / 2 + r.nextInt(8) - 4, //2 + width * 1 / 3, height / 2 + r.nextInt(8) - 4, //2 + width * 2 / 3, height / 2 + r.nextInt(8) - 4, //width - 2, height / 2 + r.nextInt(8) - 4);g.draw(cubic);// 关闭资源g.dispose();return image;}/** * 将BufferedImage转换成ByteArrayInputStream *  * @param image *            图片 * @return ByteArrayInputStream 流 */private static ByteArrayInputStream convertImageToStream(BufferedImage image) {byte[] bts = convertImageToByte(image);if(bts!=null) {return new ByteArrayInputStream(bts);}else {return null;}}private static byte[] convertImageToByte(BufferedImage image) {ByteArrayOutputStream bos = new ByteArrayOutputStream();try {ImageIO.write(image, "jpeg", bos);image.flush();byte[] bts = bos.toByteArray();return bts;} catch (IOException e) {e.printStackTrace();return null;}}}

与Redis交互:

@Servicepublic class SecurityCacheServiceImpl implements SecurityCacheService{public static final String REDIS_KEY = "sessionMap";@Autowiredprivate RedisTemplate<String,String> redisTemplate;@Overridepublic void setCodeCache(String sessionID, String securityCode) {HashOperations<String, String, String> hashOp = redisTemplate.opsForHash();hashOp.put(REDIS_KEY,sessionID,securityCode);}@Overridepublic String getCodeCache(String sessionID) {HashOperations<String, String, String> hashOp = redisTemplate.opsForHash();return hashOp.get(REDIS_KEY, sessionID);}}

Controller在生成验证码之前需要维护下Redis缓存:

@RequestMapping("/security")public ResponseEntity<byte[]> securityCode(HttpServletRequest httpRequest) {//获取验证码文本String securityCode = SecurityCodeUtil.getSecurityCode();//Redis缓存验证码信息securityCacheService.setCodeCache(getSessionId(httpRequest), securityCode);byte[] bytes = SecurityImageSupport.getImageAsByte(securityCode);HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.IMAGE_JPEG);return new ResponseEntity<byte[]>(bytes, headers,HttpStatus.OK);}

验证码效果图:







原创粉丝点击