最近项目要求增加验证码、密码有效期、限制用户登录之类的功能,于是花了三天去看CAS源码和耶鲁的官网User-manual。
基于CAS 3.3.5,Tomcat 6.29,JDK 1.6 以下部分红色表示新增,蓝色表示修改,绿色表示移除 一.增加验证码功能
配置:cas\WEB-INF\cas-servlet.xml,handlerMappingC下增加一个属性 <bean id="handlerMappingC" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="mappings"> <props> ………… <prop key="/openid/*">openIdProviderController</prop> <prop key="/captcha.htm">captchaImageCreateController</prop> </props> </property> <property name="alwaysUseFullPath" value="true" /> </bean>
cas-servlet.xml后面添加server的bean文件,这里包括验证码生成工具,密码验证类。 <bean id="captchaErrorCountAction" class="com.ist.cas.CaptchaErrorCountAction"/> <bean id="captchaValidateAction" class="com.ist.cas.CaptchaValidateAction" p:captchaService-ref="jcaptchaService" p:captchaValidationParameter="j_captcha_response"/> <bean id="captchaImageCreateController" class="com.ist.cas.CaptchaImageCreateController"> <property name="jcaptchaService" ref="jcaptchaService"/> </bean> <bean id="fastHashMapCaptchaStore" class="com.octo.captcha.service.captchastore.FastHashMapCaptchaStore" /> <bean id="jcaptchaService" class="com.octo.captcha.service.image.DefaultManageableImageCaptchaService"> <constructor-arg type="com.octo.captcha.service.captchastore.CaptchaStore" index="0"> <ref bean="fastHashMapCaptchaStore"/> </constructor-arg> <constructor-arg type="com.octo.captcha.engine.CaptchaEngine" index="1"> <bean class="com.ist.cas.JCaptchaEngineEx"/> </constructor-arg> <constructor-arg index="2"> <value>180</value> </constructor-arg> <constructor-arg index="3"> <value>100000</value> </constructor-arg> <constructor-arg index="4"> <value>75000</value> </constructor-arg> </bean>
配置:cas\WEB-INF\web.xml,添加验证码JCaptcha <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/captcha.htm</url-pattern> </servlet-mapping>
配置:cas\WEB-INF\login-webflow.xml,修改 <action-state id="submit"> <action bean="authenticationViaFormAction" method="submit" /> <transition on="warn" to="warn" /> <transition on="success" to="sendTicketGrantingTicket" /> <transition on="error" to="viewLoginForm" /> </action-state>
为 <action-state id="submit"> <action bean="authenticationViaFormAction" method="submit" /> <transition on="warn" to="warn" /> <transition on="success" to="captchaValidate" /> <transition on="error" to="viewLoginForm" /> </action-state>
添加流程节点: <action-state id="captchaValidate"> <action bean="captchaValidateAction" /> <transition on="success" to="sendTicketGrantingTicket" /> <transition on="error" to="viewLoginForm" /> </action-state>
其中使用到的几个java文件: CaptchaValidateAction.java package com.ist.cas;
import org.jasig.cas.web.support.WebUtils; import org.springframework.webflow.action.AbstractAction; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; import com.octo.captcha.service.CaptchaServiceException; import com.octo.captcha.service.image.ImageCaptchaService;
public final class CaptchaValidateAction extends AbstractAction { private ImageCaptchaService captchaService; private String captchaValidationParameter = "j_captcha_response";
protected Event doExecute(final RequestContext context) { String captcha_response = context.getRequestParameters().get(captchaValidationParameter); boolean valid = false;
if (captcha_response != null) { String id = WebUtils.getHttpServletRequest(context).getSession().getId(); if (id != null) { try { valid = captchaService.validateResponseForID(id, captcha_response).booleanValue(); } catch (CaptchaServiceException cse) { } } }
if (valid) { return success(); } context.getRequestScope().put("captchaValidatorError", "bad"); return error(); }
public void setCaptchaService(ImageCaptchaService captchaService) { this.captchaService = captchaService; }
public void setCaptchaValidationParameter(String captchaValidationParameter) { this.captchaValidationParameter = captchaValidationParameter; } }
CaptchaImageCreateController.java package com.ist.cas;
import com.octo.captcha.service.image.ImageCaptchaService; import com.sun.image.codec.jpeg.JPEGCodec; import com.sun.image.codec.jpeg.JPEGImageEncoder; import java.io.ByteArrayOutputStream; import javax.servlet.ServletOutputStream; import javax.servlet.http.*; import org.springframework.beans.factory.InitializingBean; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.Controller; public class CaptchaImageCreateController implements Controller, InitializingBean { private ImageCaptchaService jcaptchaService; public CaptchaImageCreateController(){ } public ModelAndView handleRequest(HttpServletRequest request,HttpServletResponse response) throws Exception { byte captchaChallengeAsJpeg[] = null; ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream(); String captchaId = request.getSession().getId(); java.awt.image.BufferedImage challenge=jcaptchaService.getImageChallengeForID(captchaId,request.getLocale()); JPEGImageEncoder jpegEncoder = JPEGCodec.createJPEGEncoder(jpegOutputStream); jpegEncoder.encode(challenge); captchaChallengeAsJpeg = jpegOutputStream.toByteArray();response.setHeader("Cache-Control", "no-store"); response.setHeader("Pragma", "no-cache"); response.setDateHeader("Expires", 0L); response.setContentType("image/jpeg"); ServletOutputStream responseOutputStream = response.getOutputStream(); responseOutputStream.write(captchaChallengeAsJpeg); responseOutputStream.flush(); responseOutputStream.close(); return null; } public void setJcaptchaService(ImageCaptchaService jcaptchaService) { this.jcaptchaService = jcaptchaService; } public void afterPropertiesSet() throws Exception { if(jcaptchaService == null) throw new RuntimeException("Image captcha service wasn`t set!"); else return; } }
JCaptchaEngineEx.java package com.ist.cas;
import java.awt.Color; import java.awt.Font;
import com.octo.captcha.component.image.backgroundgenerator.BackgroundGenerator; import com.octo.captcha.component.image.backgroundgenerator.GradientBackgroundGenerator; import com.octo.captcha.component.image.color.SingleColorGenerator; import com.octo.captcha.component.image.fontgenerator.FontGenerator; import com.octo.captcha.component.image.textpaster.DecoratedRandomTextPaster; import com.octo.captcha.component.image.textpaster.TextPaster; import com.octo.captcha.component.image.textpaster.textdecorator.BaffleTextDecorator; import com.octo.captcha.component.image.textpaster.textdecorator.LineTextDecorator; import com.octo.captcha.component.image.textpaster.textdecorator.TextDecorator; import com.octo.captcha.component.image.wordtoimage.ComposedWordToImage; import com.octo.captcha.component.image.wordtoimage.WordToImage; import com.octo.captcha.component.word.wordgenerator.RandomWordGenerator; import com.octo.captcha.component.word.wordgenerator.WordGenerator; import com.octo.captcha.engine.image.ListImageCaptchaEngine; import com.octo.captcha.image.gimpy.GimpyFactory;
public class JCaptchaEngineEx extends ListImageCaptchaEngine {
protected void buildInitialFactories() { /** * Set Captcha Word Length Limitation which should not over 6 */ Integer minAcceptedWordLength = new Integer(4); Integer maxAcceptedWordLength = new Integer(4); /** * Set up Captcha Image Size: Height and Width */ Integer imageHeight = new Integer(28); Integer imageWidth = new Integer(75); /** * Set Captcha Font Size between 50 and 55 */ final Integer minFontSize = new Integer(22); final Integer maxFontSize = new Integer(22); /** * We just generate digit for captcha source char * Although you can use abcdefg......xyz */ WordGenerator wordGenerator = (new RandomWordGenerator("0123456789abcdefghijklmnopqrstuvwxyz")); /** * cyt and unruledboy proved that backgroup not a factor of Security. * A captcha attacker won't affaid colorful backgroud, so we just use * white color, like google and hotmail. */ Color bgColor = new Color(255, 255, 255); BackgroundGenerator backgroundGenerator = new GradientBackgroundGenerator( imageWidth, imageHeight, bgColor, bgColor); /** * font is not helpful for security but it really increase difficultness for attacker */ FontGenerator _fontGenerator = new FontGenerator() { public Font getFont() { return new Font("Arial", Font.ITALIC, 16); } public int getMinFontSize() { return minFontSize.intValue(); } public int getMaxFontSize() { return maxFontSize.intValue(); } }; /** * Note that our captcha color is Blue */ SingleColorGenerator scg = new SingleColorGenerator(Color.BLACK); /** * decorator is very useful pretend captcha attack. * we use two line text decorators. */ LineTextDecorator line_decorator = new LineTextDecorator(new Integer(2), Color.RED); LineTextDecorator line_decorator2 = new LineTextDecorator(new Integer(3), Color.CYAN); TextDecorator[] textdecorators = new TextDecorator[2];
textdecorators[0] = line_decorator; textdecorators[1] = line_decorator2;
TextPaster _textPaster = new DecoratedRandomTextPaster(minAcceptedWordLength, maxAcceptedWordLength, scg, new TextDecorator[]{new BaffleTextDecorator(new Integer(0), Color.WHITE)});
/** * ok, generate the WordToImage Object for logon service to use. */ WordToImage wordToImage = new ComposedWordToImage( _fontGenerator, backgroundGenerator, _textPaster); addFactory(new GimpyFactory(wordGenerator, wordToImage)); } }
CASPasswordEncoder.java package com.ist.cas;
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jasig.cas.authentication.handler.PasswordEncoder;
public class CASPasswordEncoder implements PasswordEncoder { protected static final Log log = LogFactory.getLog(CASPasswordEncoder.class);
public String encode(String strSource) { MD5 theMD5 = new MD5(); String strPassMD5 = theMD5.getMD5ofStr(strSource); return strPassMD5; } }
页面配置方面,修改cas\WEB-INF\view\jsp\myth\ui\casLoginView.jsp在密码输入框下新增<li> <span class="itemname">验证码:</span> <span class="iteminput"> <input name = "j_captcha_response" type = "text"> <img src = "captcha.htm"> <a href="#" onclick="javascript:window.location.reload();">看不清换一个</a> </span> </li>
另外发现一个问题,验证出错时的提示信息问题,官方没有给出一个提示的写法,而且提示时新增异常也会导致验证直接成功,所以验证码的提示信息在页面独立分开
<form:errors path="*" cssClass="errors" id="status" element="div" /> <c:if test="${not empty captchaValidatorError}"><div id="status" class="errors">验证码输入错误。</div></c:if>
二.密码有效期验证功能
另外tb_user表中pwd_changedate表示密码更新时间,当前日期超过密码更新的有效期,就会提醒密码出错。 cas\WEB-INF\deployerConfigContext.xml,修改 <bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler"> <property name="dataSource" ref="casDataSource" /> <property name="sql" value="select password from tb_user where login_name = ?" /> <property name="passwordEncoder" ref="myPasswordEncoder"/> </bean>
为<bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler"> <property name="dataSource" ref="casDataSource" /> <property name="sql" value="select password from tb_user where login_name = ? and trunc(pwd_changedate) > trunc(SYSDATE) - ${cas.password.validate.date}" /> <property name="passwordEncoder" ref="myPasswordEncoder"/> </bean>
在:cas\WEB-INF\cas.properties,增加密码有效期属性。cas.password.validate.date=90
三.限制同一IP或用户名登录错误次数功能
限制用户登录,官方提供两种解决办法,一种是内存限制,一种是Inspektr。内存限制是最简单,所以这里用内存限制。
首先,在cas\WEB-INF\spring-configuration新建一个xml,throttleInterceptorTrigger.xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www./schema/beans" xmlns:xsi="http://www./2001/XMLSchema-instance" xmlns:p="http://www./schema/p" xsi:schemaLocation="http://www./schema/beans http://www./schema/beans/spring-beans-2.0.xsd"> <bean id="throttleInterceptor" class="org.jasig.cas.web.support.InMemoryThrottledSubmissionByIpAddressAndUsernameHandlerInterceptorAdapter" p:failureRangeInSeconds="${throttleInterceptor.failureRangeInSeconds}" p:failureThreshold="${throttleInterceptor.failureThreshold}" /> <bean id="throttleInterceptorJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean" p:targetObject-ref="throttleInterceptor" p:targetMethod="decrementCounts" /> <bean id="periodicThrottleCleanerTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean" p:jobDetail-ref="throttleInterceptorJobDetail" p:startDelay="0" p:repeatInterval="${periodicThrottleCleanerTrigger.repeatInterval}" /> </beans>
四.客户端加密功能 修改cas\WEB-INF\view\jsp\myth\ui\casLoginView.jsp JS方面引入jquery.md5.js,用于前台加密 <script type="text/javascript" src="js/jquery-1.8.3.min.js"></script> <script type="text/javascript" src="js/jquery.md5.js"></script> <script language="javascript"> var password = document.getElementById("password").value; document.getElementById("password").value = $.md5(password).toUpperCase(); </script>
配置:cas\WEB-INF\cas-servlet.xml,注释原密码加密功能
<bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler"> <property name="dataSource" ref="casDataSource" /> <property name="sql" value="select password from tb_user where login_name = ? and trunc(pwd_changedate) > trunc(SYSDATE) - ${cas.password.validate.date}" /> <!--<property name="passwordEncoder" ref="myPasswordEncoder"/>--> </bean>
五.记录错误日志功能 配置:cas\WEB-INF\cas-servlet.xml,增加一个记录类 <bean id="captchaErrorCountAction" class="com.ist.cas.CaptchaErrorCountAction"> <property name="dataSource" ref="casDataSource" /> <property name="sql" value="insert into tb_log values(10000, '0', '1', ?, ?)" /> </bean>
其中sql为写错误日志的Insert语句。?第一个参数默认配置当前时间,第二个参数默认配置错误描述。
配置:cas\WEB-INF\login-webflow.xml,修改 <action-state id="submit"> <action bean="authenticationViaFormAction" method="submit" /> <transition on="warn" to="warn" /> <transition on="success" to="sendTicketGrantingTicket" /> <transition on="error" to="viewLoginForm" /> </action-state>
<action-state id="captchaValidate"> <action bean="captchaValidateAction" /> <transition on="success" to="sendTicketGrantingTicket" /> <transition on="error" to="viewLoginForm" /> </action-state> 为 <action-state id="submit"> <action bean="authenticationViaFormAction" method="submit" /> <transition on="warn" to="warn" /> <transition on="success" to="captchaValidate" /> <transition on="error" to="errorCount" /> </action-state> <action-state id="captchaValidate"> <action bean="captchaValidateAction" /> <transition on="success" to="sendTicketGrantingTicket" /> <transition on="error" to="errorCount" /> </action-state>
添加流程节点: <action-state id="errorCount"> <action bean="captchaErrorCountAction" /> <transition on="success" to="viewLoginForm" /> </action-state>
在cas\WEB-INF\classes\com\ist\cas\添加captchaErrorCountAction.class package com.ist.cas;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.webflow.action.AbstractAction;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;
import javax.sql.DataSource;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
public final class CaptchaErrorCountAction extends AbstractAction {
protected static final Log log = LogFactory.getLog(CaptchaErrorCountAction.class);
private SimpleJdbcTemplate jdbcTemplate;
private DataSource dataSource;
private String sql;
protected Event doExecute(final RequestContext context) {
int count = 1;
try {
getJdbcTemplate().update(this.sql, new Object[]{new Date(), "登录失败"});
} catch (Exception e) {
log.error(e);
}
return success();
}
public final void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new SimpleJdbcTemplate(dataSource);
this.dataSource = dataSource;
}
protected final SimpleJdbcTemplate getJdbcTemplate() {
return this.jdbcTemplate;
}
protected final DataSource getDataSource() {
return this.dataSource;
}
public void setSql(String sql) {
this.sql = sql;
}
}
|