简介
- 在实现登录功能时,大多数情况需要验证码支持,目的是防止机器人暴力破解密码。
- 目前比较简单的验证码通过一些OCR工具就可以解析出来,复杂的验证码(一般通过扭曲、加线条或噪点等干扰)能够防止OCR识别。更复杂的如填字、算数等验证码则需要人工识别。
- 目前比较可靠的是手机验证码,但是对于用户来说比普通验证码要麻烦得多。
- 验证码图片可以通过Java提供的图片API实现,也可以借助如JCaptcha(Completely Automated Public Turing Test To Tell Computers adn Humans Apart)这种开源Java类库生成,它提供了常见的扭曲、加线条和噪点等干扰的支持。
示例
基于第十六章,是一个权限管理系统,但本例中后台管理不是主要内容,现新增一个jcaptcha包,放置所有和验证码有关的类,用于实现用户登录时获取验证码和提交表单后校验验证码的功能。
依赖:
<!--jcaptcha核心--> <dependency> <groupId>com.octo.captcha</groupId> <artifactId>jcaptcha</artifactId> <version>2.0-alpha-1</version> </dependency> <!--支持与servlet集成--> <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>
实际能用的依赖如下:
<!--jcaptcha验证码工具--> <dependency><!--jcaptcha核心--> <groupId>com.octo.captcha</groupId> <artifactId>jcaptcha</artifactId> <version>1.0</version> <exclusions> <!--不排除此包,tomcat启动出错--> <exclusion> <artifactId>servlet-api</artifactId> <groupId>javax.servlet</groupId> </exclusion> </exclusions>
com.octo.captcha
jcaptcha-api
1.0
GMailEngine:验证码图片生成引擎,继承自ListImageCaptchaEngine,只需要简单设置以下图片和字体大小等,即可仿照jcaptcha2.0生成类似GMail验证码的样式。
public class GMailEngine extends ListImageCaptchaEngine {
@Override
protected void buildInitialFactories() {
// 图片和字体大小设置
int minWordLength = 4;
int maxWordLength = 5;
int fontSize = 20;
int imageWidth = 100;
int imageHeight = 36;
WordGenerator dictionnaryWords = new ComposeDictionaryWordGenerator(
new FileDictionary("toddlist"));
// word2image components
TextPaster randomPaster = new DecoratedRandomTextPaster(minWordLength,
maxWordLength, new RandomListColorGenerator(new Color[]{
new Color(23, 170, 27), new Color(220, 34, 11),
new Color(23, 67, 172)}), new TextDecorator[]{});
BackgroundGenerator background = new UniColorBackgroundGenerator(
imageWidth, imageHeight, Color.white);
FontGenerator font = new RandomFontGenerator(fontSize, fontSize,
new Font[]{new Font("nyala", Font.BOLD, fontSize),
new Font("Bell MT", Font.PLAIN, fontSize),
new Font("Credit valley", Font.BOLD, fontSize)});
ImageDeformation postDef = new ImageDeformationByFilters(
new ImageFilter[]{});
ImageDeformation backDef = new ImageDeformationByFilters(
new ImageFilter[]{});
ImageDeformation textDef = new ImageDeformationByFilters(
new ImageFilter[]{});
WordToImage word2image = new DeformedComposedWordToImage(font,
background, randomPaster, backDef, textDef, postDef);
addFactory(new GimpyFactory(dictionnaryWords, word2image));
}
}
- MyManageableImageCaptchaService:使用GMailEngine生成验证码图片、比对验证码(每验证一次无论成功与否都会删除该验证码)。
public class MyManageableImageCaptchaService
extends DefaultManageableImageCaptchaService {
/**
* @Author haien
* @Description 构造器,生成验证码图片
* @Date 16:08 2019/4/19
* @Param [captchaStore, captchaEngine, minGuarantedStorageDelayInSeconds,
maxCaptchaStoreSize, captchaStoreLoadBeforeGarbageCollection]
**/
public MyManageableImageCaptchaService(CaptchaStore captchaStore,
CaptchaEngine captchaEngine,
int minGuarantedStorageDelayInSeconds,
int maxCaptchaStoreSize,
int captchaStoreLoadBeforeGarbageCollection)
{
super(captchaStore,captchaEngine,minGuarantedStorageDelayInSeconds,
maxCaptchaStoreSize, captchaStoreLoadBeforeGarbageCollection);
}
/**
* @Author haien
* @Description 根据会话id获取当前会话的验证码,与用户输入的比对
* @Date 2019/4/11
* @Param [id会话id, userCaptchaResponse用户输入的验证码]
* @return boolean
**/
public boolean validate(String id,String userCaptchaResponse){
//验证但不删除
return store.getCaptcha(id).validateResponse(userCaptchaResponse);
}
}
- JCaptcha:jcaptcha工具类,利用MyManageableImageCaptchaService生成和验证验证码。
public class JCaptcha {
public static final MyManageableImageCaptchaService captchaService=
new MyManageableImageCaptchaService(new FastHashMapCaptchaStore(),
new GMailEngine(),180,
100000,75000);
/**
* @Author haien
* @Description 比对当前请求输入的验证码是否正确,
并从CaptchaService中移除已使用过的验证码
* @Date 2019/4/11
* @Param [request, userCaptchaResponse用户输入的验证码]
* @return boolean
**/
public static boolean validateResponse(HttpServletRequest request,
String userCaptchaResponse){
//为true应该是没有session则新建
if(request.getSession(false)==null) return false;
boolean validated=false;
try {
//获取会话id
String id = request.getSession().getId();
//将用户输入的验证码与库存比对,并移除验证码与id(无论正确与否)
validated = captchaService.validateResponseForID(id, userCaptchaResponse)
.booleanValue();
}catch (CaptchaServiceException e){
e.printStackTrace();
}
return validated;
}
/**
* @Author haien
* @Description 比对,但不移除验证码,适用于Ajax异步验证,
此时不是真的验证,还不能删除验证码
* @Date 2019/4/11
* @Param [request, userCaptchaResponse]
* @return boolean
**/
public static boolean validate(HttpServletRequest request,
String userCaptchaResponse){
if (request.getSession(false)==null) return false;
boolean validated=false;
try {
String id=request.getSession().getId();
validated=captchaService.validate(id,userCaptchaResponse);
}catch (CaptchaServiceException e){
e.printStackTrace();
}
return validated;
}
}
- JCaptchaFilter:拦截前端的验证码请求,调用JCaptcha工具类生成并向前端写验证码图片。
//保证只执行一次,注:继承的是spring的Filter
public class JCaptchaFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,FilterChain chain)
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();
//captchaService使用当前会话的id作为key获取相应的验证码图片
BufferedImage bi=JCaptcha.captchaService.getImageChallengeForID(id);
ServletOutputStream out=response.getOutputStream();
ImageIO.write(bi,"jpg",out);
try {
out.flush();
}finally {
out.close();
}
}
}
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> <script> $(function() { $(".jcaptcha-btn").click(function() { $(".jcaptcha-img").attr("src", '${pageContext.request.contextPath}/jcaptcha.jpg?' //按钮对应url, //触发JCaptchaFilter +new Date().getTime()); }); }); </script>
web.xml:分配验证码请求给JCaptchaFilter拦截。
<!--验证码过滤器需要放到ShiroFilter之后,因为Shiro将包装HttpSession; 否则可能造成两次的session id不同--> <filter> <filter-name>JCaptchaFilter</filter-name> <filter-class>com.haien.chapter22.jcaptcha.JCaptchaFilter</filter-class> <async-supported>true</async-supported> </filter> <filter-mapping> <filter-name>JCaptchaFilter</filter-name> <!--前台登录页面点击“获取验证码”,其url为此,则执行该过滤链, 生成验证码图片并写到客户端--> <url-pattern>/jcaptcha.jpg</url-pattern> </filter-mapping>
JCaptchaValidateFilter:拦截验证码验证请求的拦截器,主要是调用JCaptcha工具类进行验证。
public class JCaptchaValidateFilter extends AccessControlFilter {
//是否开启验证码支持
private boolean jcaptchaEnabled=true;
//前台提交的验证码参数名
private String jcaptchaParam="jcaptchaCode";
//验证失败后存储到的属性名
private String failureKeyAttribute="shiroLoginFailure";
public void setJcaptchaEnabled(boolean jcaptchaEnabled) {
this.jcaptchaEnabled = jcaptchaEnabled;
}
public void setJcaptchaParam(String jcaptchaParam) {
this.jcaptchaParam = jcaptchaParam;
}
public void setFailureKeyAttribute(String failureKeyAttribute) {
this.failureKeyAttribute = failureKeyAttribute;
}
/**
* @Author haien
* @Description 验证验证码
* @Date 2019/4/19
* @Param [request, response, mappedValue]
* @return boolean
**/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response,
Object mappedValue) throws Exception {
//1. 设置验证码是否开启属性,页面据此决定是否显示验证码
request.setAttribute("jcaptchaEnabled",jcaptchaEnabled);
//将HttpServlet的request转为HttpServlet类型
HttpServletRequest httpServletRequest=WebUtils.toHttp(request);
//2. 判断验证码是否禁用|不是由表单提交的
if(jcaptchaEnabled==false ||
! "post".equalsIgnoreCase(httpServletRequest.getMethod())){
//则不用验证,直接结束
return true;
}
//3. 验证 验证码,正确则返回true,否则false
return JCaptcha.validateResponse(httpServletRequest,
httpServletRequest.getParameter(jcaptchaParam));
}
/**
* @Author haien
* @Description 验证失败处理
* @Date 2019/4/19
* @Param [request, response]
* @return boolean
**/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
throws Exception {
//若验证失败,则存储failureKeyAttribute属性;可通过getFailureKeyAttribute()获取
request.setAttribute(failureKeyAttribute,"jCaptcha.error");
return true;
}
}
- MyFormAuthenticationFilter:继承FormAuthenticationFilter,自定义身份验证拦截器,主要是增加验证码错误情况下不验证身份的条件。
public class MyFormAuthenticationFilter extends FormAuthenticationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response,
Object mappedValue) {
//若为登录请求,无论登录与否,直接进入onAccessDenied
if(isLoginRequest(request,response)) return false;
return super.isAccessAllowed(request, response, mappedValue);
}
@Override
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.haien.chapter22.jcaptcha.MyFormAuthenticationFilter"> <property name="usernameParam" value="username"/> <property name="passwordParam" value="password"/> <property name="rememberMeParam" value="rememberMe"/> <property name="failureKeyAttribute" value="shiroLoginFailure"/><!--值同JCaptchaValidateFilter--> </bean> <bean id="JCaptchaValidateFilter" class="com.haien.chapter22.jcaptcha.JCaptchaValidateFilter"> <property name="jcaptchaEnabled" value="true"/> <property name="jcaptchaParam" value="jcaptchaCode"/> <property name="failureKeyAttribute" value="shiroLoginFailure"/> </bean> <bean id="sysUserFilter" class="com.haien.chapter22.web.shiro.filter.SysUserFilter"/> <!-- 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"> <!--其实第一行可以不配置,因为spring-mvc.xml已经映射了静态资源?--> <value> /static/**=anon /jcaptcha*=anon /login = JCaptchaValidate,authc /logout = logout /authenticated = authc /** = user,sysUser </value> </property> </bean>
测试:访问http://localhost:8080/chapter22,将重定向到登录页面,显示验证码图片,点击可换一张。输入验证码,错误则返回登录页面,并提示验证码错误。
- 代码示例:ideaProjects/shiro-chapter22
- 《跟我学Shiro》第二十二章