001/* 002 * Shredzone Commons 003 * 004 * Copyright (C) 2012 Richard "Shred" Körber 005 * http://commons.shredzone.org 006 * 007 * This program is free software: you can redistribute it and/or modify 008 * it under the terms of the GNU Library General Public License as 009 * published by the Free Software Foundation, either version 3 of the 010 * License, or (at your option) any later version. 011 * 012 * This program is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 015 * GNU General Public License for more details. 016 * 017 * You should have received a copy of the GNU Library General Public License 018 * along with this program. If not, see <http://www.gnu.org/licenses/>. 019 */ 020package org.shredzone.commons.captcha.impl; 021 022import java.awt.image.BufferedImage; 023import java.util.Random; 024 025import javax.annotation.Resource; 026import javax.servlet.http.HttpSession; 027 028import org.shredzone.commons.captcha.CaptchaGenerator; 029import org.shredzone.commons.captcha.CaptchaService; 030import org.springframework.stereotype.Component; 031 032/** 033 * Default implementation of {@link CaptchaService}. 034 * 035 * @author Richard "Shred" Körber 036 */ 037@Component("captchaService") 038public class DefaultCaptchaService implements CaptchaService { 039 040 private static final String CHARSET = "ABCDEFGHJLMNOPQRSTUWZ"; 041 private static final int NUMBER_OF_CHARS = 5; 042 private static final String CAPTCHA_NAME = "captcha.position"; 043 private static final String LASTCLICK_NAME = "captcha.lastclick"; 044 045 private final Random rnd = new Random(); 046 047 @Resource 048 private CaptchaGenerator captchaGenerator; 049 050 @Override 051 public BufferedImage createCaptcha(HttpSession session) { 052 int captchaPos = computeCaptchaPosition(session); 053 return captchaGenerator.createCaptcha(computeChars(captchaPos)); 054 } 055 056 @Override 057 public boolean isValidCaptcha(HttpSession session, int x, int y) { 058 Integer pos = getCaptchaPosition(session); 059 060 if (pos == null) { 061 // There was no captcha generated yet, so the answer is always false. 062 return false; 063 } 064 065 int cw = captchaGenerator.getWidth(); 066 int ch = captchaGenerator.getHeight(); 067 068 if (x < 0 || y < 0 || x >= cw || y >= ch) { 069 // The click was outside of the captcha, so the answer is always false. 070 return false; 071 } 072 073 if (x == 0 && y == 0) { 074 // Ignore the simplest possible coordinate. No human being would click 075 // there... ;-) 076 return false; 077 } 078 079 int boxWidth = cw / NUMBER_OF_CHARS; 080 int answer = x / boxWidth; 081 082 setLastclickPosition(session, answer); 083 084 return answer == pos; 085 } 086 087 /** 088 * Compute a random set of characters, with exactly one 'X' at the given position. 089 * 090 * @param pos 091 * position of the 'X' 092 * @return captcha text 093 */ 094 private char[] computeChars(int pos) { 095 char[] chars = new char[NUMBER_OF_CHARS]; 096 097 for (int ix = 0; ix < NUMBER_OF_CHARS; ix++) { 098 if (ix == pos) { 099 chars[ix] = 'X'; 100 } else { 101 chars[ix] = CHARSET.charAt(rnd.nextInt(CHARSET.length())); 102 } 103 } 104 105 return chars; 106 } 107 108 /** 109 * Computes the position of the correct captcha answer. 110 * <p> 111 * Makes sure the new correct answer is never at the same position as the previous 112 * click. This will keep spammers from just clicking at the same position until they 113 * gave the right answer by lucky chance. 114 * 115 * @param session 116 * {@link HttpSession} with captcha data 117 * @return position of the correct answer 118 */ 119 private int computeCaptchaPosition(HttpSession session) { 120 int newPos; 121 122 Integer oldPos = getLastclickPosition(session); 123 if (oldPos != null) { 124 // Make sure newPos is always != oldPos 125 newPos = rnd.nextInt(NUMBER_OF_CHARS - 1); 126 if (newPos >= oldPos) newPos++; 127 } else { 128 newPos = rnd.nextInt(NUMBER_OF_CHARS); 129 } 130 131 setCaptchaPosition(session, newPos); 132 return newPos; 133 } 134 135 /** 136 * Gets the last captcha position from the session. 137 * 138 * @param session 139 * {@link HttpSession} 140 * @return the last captcha position, or {@code null} if there is none yet 141 */ 142 private Integer getCaptchaPosition(HttpSession session) { 143 return (Integer) session.getAttribute(CAPTCHA_NAME); 144 } 145 146 /** 147 * Sets the captcha position. 148 * 149 * @param session 150 * {@link HttpSession} 151 * @param pos 152 * captcha position to store 153 */ 154 private void setCaptchaPosition(HttpSession session, int pos) { 155 session.setAttribute(CAPTCHA_NAME, pos); 156 } 157 158 /** 159 * Gets the position of the last click. 160 * 161 * @param session 162 * {@link HttpSession} 163 * @return position of the last click, or {@code null} if the user did not click yet 164 */ 165 private Integer getLastclickPosition(HttpSession session) { 166 return (Integer) session.getAttribute(LASTCLICK_NAME); 167 } 168 169 /** 170 * Sets the position of the last click. 171 * 172 * @param session 173 * {@link HttpSession} 174 * @param pos 175 * position of the last click 176 */ 177 private void setLastclickPosition(HttpSession session, int pos) { 178 session.setAttribute(LASTCLICK_NAME, pos); 179 } 180 181}