在做用戶登錄功能時,非常多時候都須要驗證碼支持,驗證碼的目的是為了防止機器人模擬真有用戶登錄而惡意訪問,如暴力破解用戶password/惡意評論等。
眼下也有一些驗證碼比較簡單,通過一些OCR工具就能夠解析出來。另外另一些驗證碼比較復雜(一般通過如扭曲、加線條/噪點等干擾)防止OCR工具識別。可是在中國就是人多。機器干不了的能夠交給人來完畢,所以在中國就有非常多打碼平台。人工識別驗證碼;因此即使比較復雜的如填字、算數等類型的驗證碼還是能識別的。所以驗證碼也不是絕對可靠的,眼下比較可靠還是手機驗證碼,可是對於用戶來說相對於驗證碼還是比較麻煩的。
對於驗證碼圖片的生成。能夠自己通過如Java提供的圖像API自己去生成,也能夠借助如JCaptcha這樣的開源Java類庫生成驗證碼圖片;JCaptcha提供了常見的如扭曲、加噪點等干擾支持。本章代碼基於《第十六章 綜合實例》。
一、加入JCaptcha依賴
- <dependency>
- <groupId>com.octo.captcha</groupId>
- <artifactId>jcaptcha</artifactId>
- <version>2.0-alpha-1</version>
- </dependency>
- <dependency>
- <groupId>com.octo.captcha</groupId>
- <artifactId>jcaptcha-integration-simple-servlet</artifactId>
- <version>2.0-alpha-1</version>
- <exclusions>
- <exclusion>
- <artifactId>servlet-api</artifactId>
- <groupId>javax.servlet</groupId>
- </exclusion>
- </exclusions>
- </dependency>
com.octo.captcha . jcaptcha 提供了jcaptcha 核心;而jcaptcha-integration-simple-servlet提供了與Servlet集成。
二、GMailEngine
來自https://code.google.com/p/musicvalley/source/browse/trunk/musicvalley/doc/springSecurity/springSecurityIII/src/main/java/com/spring/security/jcaptcha/GMailEngine.java?spec=svn447&r=447(眼下無法訪問了),仿照JCaptcha2.0編寫類似GMail驗證碼的樣式;詳細請參考com.github.zhangkaitao.shiro.chapter22.jcaptcha.GMailEngine。
三、MyManageableImageCaptchaService
提供了推斷倉庫中是否有對應的驗證碼存在。
- public class MyManageableImageCaptchaService extends
- DefaultManageableImageCaptchaService {
- public MyManageableImageCaptchaService(
- com.octo.captcha.service.captchastore.CaptchaStore captchaStore,
- com.octo.captcha.engine.CaptchaEngine captchaEngine,
- int minGuarantedStorageDelayInSeconds,
- int maxCaptchaStoreSize,
- int captchaStoreLoadBeforeGarbageCollection) {
- super(captchaStore, captchaEngine, minGuarantedStorageDelayInSeconds,
- maxCaptchaStoreSize, captchaStoreLoadBeforeGarbageCollection);
- }
- public boolean hasCapcha(String id, String userCaptchaResponse) {
- return store.getCaptcha(id).validateResponse(userCaptchaResponse);
- }
- }
四、JCaptcha工具類
提供對應的API來驗證當前請求輸入的驗證碼是否正確。
- public class JCaptcha {
- public static final MyManageableImageCaptchaService captchaService
- = new MyManageableImageCaptchaService(new FastHashMapCaptchaStore(),
- new GMailEngine(), 180, 100000, 75000);
- public static boolean validateResponse(
- HttpServletRequest request, String userCaptchaResponse) {
- if (request.getSession(false) == null) return false;
- boolean validated = false;
- try {
- String id = request.getSession().getId();
- validated =
- captchaService.validateResponseForID(id, userCaptchaResponse)
- .booleanValue();
- } catch (CaptchaServiceException e) {
- e.printStackTrace();
- }
- return validated;
- }
- public static boolean hasCaptcha(
- HttpServletRequest request, String userCaptchaResponse) {
- if (request.getSession(false) == null) return false;
- boolean validated = false;
- try {
- String id = request.getSession().getId();
- validated = captchaService.hasCapcha(id, userCaptchaResponse);
- } catch (CaptchaServiceException e) {
- e.printStackTrace();
- }
- return validated;
- }
- }
validateResponse():驗證當前請求輸入的驗證碼否正確;並從CaptchaService中刪除已經生成的驗證碼;
hasCaptcha():驗證當前請求輸入的驗證碼是否正確;但不從CaptchaService中刪除已經生成的驗證碼(比方Ajax驗證時能夠使用。防止多次生成驗證碼);
五、JCaptchaFilter
用於生成驗證碼圖片的過濾器。
- public class JCaptchaFilter extends OncePerRequestFilter {
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
- response.setDateHeader("Expires", 0L);
- response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
- response.addHeader("Cache-Control", "post-check=0, pre-check=0");
- response.setHeader("Pragma", "no-cache");
- response.setContentType("image/jpeg");
- String id = request.getRequestedSessionId();
- BufferedImage bi = JCaptcha.captchaService.getImageChallengeForID(id);
- ServletOutputStream out = response.getOutputStream();
- ImageIO.write(bi, "jpg", out);
- try {
- out.flush();
- } finally {
- out.close();
- }
- }
- }
CaptchaService使用當前會話ID當作key獲取對應的驗證碼圖片;另外須要設置響應內容不進行瀏覽器端緩存。
- <!-- 驗證碼過濾器須要放到Shiro之后 由於Shiro將包裝HttpSession 假設不。可能造成兩次的sesison id 不一樣 -->
- <filter>
- <filter-name>JCaptchaFilter</filter-name>
- <filter-class>
- com.github.zhangkaitao.shiro.chapter22.jcaptcha.JCaptchaFilter
- </filter-class>
- </filter>
- <filter-mapping>
- <filter-name>JCaptchaFilter</filter-name>
- <url-pattern>/jcaptcha.jpg</url-pattern>
- </filter-mapping>
這樣就能夠在頁面使用/jcaptcha.jpg地址顯示驗證碼圖片。
六、JCaptchaValidateFilter
用於驗證碼驗證的Shiro過濾器。
- public class JCaptchaValidateFilter extends AccessControlFilter {
- private boolean jcaptchaEbabled = true;//是否開啟驗證碼支持
- private String jcaptchaParam = "jcaptchaCode";//前台提交的驗證碼參數名
- private String failureKeyAttribute = "shiroLoginFailure"; //驗證失敗后存儲到的屬性名
- public void setJcaptchaEbabled(boolean jcaptchaEbabled) {
- this.jcaptchaEbabled = jcaptchaEbabled;
- }
- public void setJcaptchaParam(String jcaptchaParam) {
- this.jcaptchaParam = jcaptchaParam;
- }
- public void setFailureKeyAttribute(String failureKeyAttribute) {
- this.failureKeyAttribute = failureKeyAttribute;
- }
- protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
- //1、設置驗證碼是否開啟屬性,頁面能夠依據該屬性來決定是否顯示驗證碼
- request.setAttribute("jcaptchaEbabled", jcaptchaEbabled);
- HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
- //2、推斷驗證碼是否禁用 或不是表單提交(同意訪問)
- if (jcaptchaEbabled == false || !"post".equalsIgnoreCase(httpServletRequest.getMethod())) {
- return true;
- }
- //3、此時是表單提交。驗證驗證碼是否正確
- return JCaptcha.validateResponse(httpServletRequest, httpServletRequest.getParameter(jcaptchaParam));
- }
- protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
- //假設驗證碼失敗了。存儲失敗key屬性
- request.setAttribute(failureKeyAttribute, "jCaptcha.error");
- return true;
- }
- }
七、MyFormAuthenticationFilter
用於驗證碼驗證的Shiro攔截器在用於身份認證的攔截器之前執行;可是假設驗證碼驗證攔截器失敗了。就不須要進行身份認證攔截器流程了;所以須要改動下如FormAuthenticationFilter身份認證攔截器,當驗證碼驗證失敗時不再走身份認證攔截器。
- public class MyFormAuthenticationFilter extends FormAuthenticationFilter {
- protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
- if(request.getAttribute(getFailureKeyAttribute()) != null) {
- return true;
- }
- return super.onAccessDenied(request, response, mappedValue);
- }
- }
即假設之前已經錯了,那直接跳過就可以。
八、spring-config-shiro.xml
- <!-- 基於Form表單的身份驗證過濾器 -->
- <bean id="authcFilter"
- class="com.github.zhangkaitao.shiro.chapter22.jcaptcha.MyFormAuthenticationFilter">
- <property name="usernameParam" value="username"/>
- <property name="passwordParam" value="password"/>
- <property name="rememberMeParam" value="rememberMe"/>
- <property name="failureKeyAttribute" value="shiroLoginFailure"/>
- </bean>
- <bean id="jCaptchaValidateFilter"
- class="com.github.zhangkaitao.shiro.chapter22.jcaptcha.JCaptchaValidateFilter">
- <property name="jcaptchaEbabled" value="true"/>
- <property name="jcaptchaParam" value="jcaptchaCode"/>
- <property name="failureKeyAttribute" value="shiroLoginFailure"/>
- </bean>
- <!-- Shiro的Web過濾器 -->
- <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
- <property name="securityManager" ref="securityManager"/>
- <property name="loginUrl" value="/login"/>
- <property name="filters">
- <util:map>
- <entry key="authc" value-ref="authcFilter"/>
- <entry key="sysUser" value-ref="sysUserFilter"/>
- <entry key="jCaptchaValidate" value-ref="jCaptchaValidateFilter"/>
- </util:map>
- </property>
- <property name="filterChainDefinitions">
- <value>
- /static/** = anon
- /jcaptcha* = anon
- /login = jCaptchaValidate,authc
- /logout = logout
- /authenticated = authc
- /** = user,sysUser
- </value>
- </property>
- </bean>
九、login.jsp登錄頁面
- <c:if test="${jcaptchaEbabled}">
- 驗證碼:
- <input type="text" name="jcaptchaCode">
- <img class="jcaptcha-btn jcaptcha-img"
- src="${pageContext.request.contextPath}/jcaptcha.jpg" title="點擊更換驗證碼">
- <a class="jcaptcha-btn" href="javascript:;">換一張</a>
- <br/>
- </c:if>
依據jcaptchaEbabled來顯示驗證碼圖片。
十、測試
輸入http://localhost:8080/chapter22將重定向到登錄頁面。輸入正確的username/password/驗證碼就可以成功登錄,假設輸入錯誤的驗證碼,將顯示驗證碼錯誤頁面:
演示樣例源碼:https://github.com/zhangkaitao/shiro-example
本文借鑒於:http://jinnianshilongnian.iteye.com/blog/2046041