验证码是有效防止暴力破解的一种手段,常用做法是在服务端产生一串随机字符串与当前用户会话关联(我们通常说的放入 Session),然后向终端用户展现一张经过“扰乱”的图片,只有当用户输入的内容与服务端产生的内容相同时才允许进行下一步操作
作为演示,我们选择开源的验证码组件 kaptcha。这样,我们只需要简单配置一个 Servlet,页面通过 IMG 标签就可以展现图形验证码。
<servlet> <servlet-name>kaptcha</servlet-name> <servlet-class> com.google.code.kaptcha.servlet.KaptchaServlet </servlet-class> </servlet> <servlet-mapping> <servlet-name>kaptcha</servlet-name> <url-pattern>/images/kaptcha.jpg</url-pattern> </servlet-mapping>
扩展 UsernamePasswordTokenShiro 表单认证,页面提交的用户名密码等信息,用 UsernamePasswordToken 类来接收,很容易想到,要接收页面验证码的输入,我们需要扩展此类:
package javacommon.shiro; import org.apache.shiro.authc.UsernamePasswordToken; public class CaptchaUsernamePasswordToken extends UsernamePasswordToken { //验证码字符串 private String captcha; public CaptchaUsernamePasswordToken(String username, char[] password, boolean rememberMe, String host, String captcha) { super(username, password, rememberMe, host); this.captcha = captcha; } public String getCaptcha() { return captcha; } public void setCaptcha(String captcha) { this.captcha = captcha; } }
扩展 FormAuthenticationFilter
接下来我们扩展 FormAuthenticationFilter 类
package javacommon.shiro; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; import org.apache.shiro.web.util.WebUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class CaptchaFormAuthenticationFilter extends FormAuthenticationFilter { private static final Logger LOG = LoggerFactory.getLogger(CaptchaFormAuthenticationFilter.class); public CaptchaFormAuthenticationFilter() { } @Override /** * 登录验证 */ protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { CaptchaUsernamePasswordToken token = createToken(request, response); try { /*图形验证码验证*/ doCaptchaValidate((HttpServletRequest) request, token); Subject subject = getSubject(request, response); subject.login(token);//正常验证 LOG.info(token.getUsername()+"登录成功"); return onLoginSuccess(token, subject, request, response); }catch (AuthenticationException e) { LOG.info(token.getUsername()+"登录失败--"+e); return onLoginFailure(token, e, request, response); } } // 验证码校验 protected void doCaptchaValidate(HttpServletRequest request, CaptchaUsernamePasswordToken token) { //session中的图形码字符串 String captcha = (String) request.getSession().getAttribute( com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY); //比对 if (captcha != null && !captcha.equalsIgnoreCase(token.getCaptcha())) { throw new IncorrectCaptchaException("验证码错误!"); } } @Override protected CaptchaUsernamePasswordToken createToken(ServletRequest request, ServletResponse response) { String username = getUsername(request); String password = getPassword(request); String captcha = getCaptcha(request); boolean rememberMe = isRememberMe(request); String host = getHost(request); return new CaptchaUsernamePasswordToken(username, password.toCharArray(), rememberMe, host, captcha); } public static final String DEFAULT_CAPTCHA_PARAM = "captcha"; private String captchaParam = DEFAULT_CAPTCHA_PARAM; public String getCaptchaParam() { return captchaParam; } public void setCaptchaParam(String captchaParam) { this.captchaParam = captchaParam; } protected String getCaptcha(ServletRequest request) { return WebUtils.getCleanParam(request, getCaptchaParam()); } //保存异常对象到request @Override protected void setFailureAttribute(ServletRequest request, AuthenticationException ae) { request.setAttribute(getFailureKeyAttribute(), ae); } }
package javacommon.shiro; import org.apache.shiro.authc.AuthenticationException; public class IncorrectCaptchaException extends AuthenticationException { public IncorrectCaptchaException() { super(); } public IncorrectCaptchaException(String message, Throwable cause) { super(message, cause); } public IncorrectCaptchaException(String message) { super(message); } public IncorrectCaptchaException(Throwable cause) { super(cause); } }
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd"> <!-- Shiro Filter 拦截器相关配置 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- securityManager --> <property name="securityManager" ref="securityManager" /> <!-- 登录路径 --> <property name="loginUrl" value="/login.jsp" /> <!-- 登录成功后跳转路径 --> <property name="successUrl" value="/pages/index.jsp" /> <!-- 授权失败跳转路径 --> <property name="unauthorizedUrl" value="/login.jsp" /> <property name="filters"> <util:map> <entry key="authc" value-ref="myAuthenFilter" /> </util:map> </property> <!-- 过滤链定义 --> <property name="filterChainDefinitions"> <value> /login.jsp = authc /pages/* = authc /index.jsp* = authc /logout.do = logout <!-- 访问这些路径必须拥有某种权限 /role/edit/* = perms[role:edit] /role/save = perms[role:edit] /role/list = perms[role:view] --> </value> </property> </bean> <!-- 自定义验证拦截器 --> <bean id="myAuthenFilter" class="javacommon.shiro.CaptchaFormAuthenticationFilter" /> <!-- securityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="myRealm" /> </bean> <!-- <bean id="shiroCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManager" ref="cacheManager" /> </bean> --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" /> <!-- 自定义Realm实现 --> <bean id="myRealm" class="javacommon.shiro.CustomRealm"> <!-- <property name="cacheManager" ref="shiroCacheManager" /> --> </bean> </beans>
登录页面:
<%@page import="org.apache.shiro.web.filter.authc.FormAuthenticationFilter"%> <%@page import="javacommon.shiro.IncorrectCaptchaException"%> <%@page import="org.apache.shiro.authc.AuthenticationException"%> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <base href="<%=basePath%>"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> <style type="text/css"> .error { color: red; } </style> <script type="text/javascript"> function refreshCaptcha(){ document.getElementById("img_captcha").src="<%=basePath%>images/kaptcha.jpg?t=" + Math.random(); } </script> </head> <body> <% Object obj = request .getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME); String msg = ""; if (obj != null) { if (obj instanceof IncorrectCaptchaException) msg = "验证码错误!"; else msg = "账号或密码错误!"; } out.println("<div class='error'>" + msg + "</div>"); %> <form action="login.jsp" method="post"> <input type="hidden" name="rememberMe" value="true" /> <br /> <table> <tr> <td>用户帐号:</td> <td><input type="text" name="username" id="username" value="" /></td> </tr> <tr> <td>登录密码:</td> <td><input type="password" name="password" id="password" value="" /></td> </tr> <tr> <td>验证码:</td> <td><input type="text" name="captcha" /></td> </tr> <tr> <td> </td> <td><img alt="验证码" src="images/kaptcha.jpg" title="点击更换" id="img_captcha" onclick="javascript:refreshCaptcha();">(看不清<a href="javascript:void(0)" onclick="javascript:refreshCaptcha()">换一张</a>)</td> </tr> <tr> <td colspan="2"><input value="登录" type="submit"></td> </tr> </table> </form> </body> </html>