在做用戶登錄功能時,很多時候都需要驗證碼支持,驗證碼的目的是為了防止機器人模擬真實用戶登錄而惡意訪問,如暴力破解用戶密碼 / 惡意評論等。目前也有一些驗證碼比較簡單,通過一些 OCR 工具就可以解析出來;另外還有一些驗證碼比較復(fù)雜(一般通過如扭曲、加線條 / 噪點等干擾)防止 OCR 工具識別;但是在中國就是人多,機器干不了的可以交給人來完成,所以在中國就有很多打碼平臺,人工識別驗證碼;因此即使比較復(fù)雜的如填字、算數(shù)等類型的驗證碼還是能識別的。所以驗證碼也不是絕對可靠的,目前比較可靠還是手機驗證碼,但是對于用戶來說相對于驗證碼還是比較麻煩的。
對于驗證碼圖片的生成,可以自己通過如 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
提供了判斷倉庫中是否有相應(yīng)的驗證碼存在。
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 工具類
提供相應(yīng)的 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 中刪除已經(jīng)生成的驗證碼; hasCaptcha():驗證當前請求輸入的驗證碼是否正確;但不從 CaptchaService 中刪除已經(jīng)生成的驗證碼(比如 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 獲取相應(yīng)的驗證碼圖片;另外需要設(shè)置響應(yīng)內(nèi)容不進行瀏覽器端緩存。
<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";//前臺提交的驗證碼參數(shù)名
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、設(shè)置驗證碼是否開啟屬性,頁面可以根據(jù)該屬性來決定是否顯示驗證碼
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);
}
}
即如果之前已經(jīng)錯了,那直接跳過即可。
八、spring-config-shiro.xml
<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>
<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="https://atts.w3cschool.cn/attachments/image/wk/shiro/jcaptcha.jpg" title="點擊更換驗證碼">
<a class="jcaptcha-btn" href="javascript:;">換一張</a>
<br/>
</c:if>
根據(jù) jcaptchaEbabled 來顯示驗證碼圖片。
十、測試
輸入 http://localhost:8080/chapter22
將重定向到登錄頁面;輸入正確的用戶名 / 密碼 / 驗證碼即可成功登錄,如果輸入錯誤的驗證碼,將顯示驗證碼錯誤頁面:
更多建議: