http://blog.163.com/tangweibo_good/blog/static/77492409201341031915634/
最近项目要求增加验证码、密码有效期、限制用户登录之类的功能,于是花了三天去看CAS源码和耶鲁的官网User-manual。
一.增加验证码功能
配置: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>
<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\login-webflow.xml,修改<servlet-mapping>
<servlet-name>cas</servlet-name>
<url-pattern>/captcha.htm</url-pattern>
</servlet-mapping>
为<act
<action-state id="submit"> ion bean="authenticationViaFormAction" method="submit" /> <transition on="warn" to="warn" /> <transition on="success" to="sendTicketGrantingTicket" /> <transition on="error" to="viewLoginForm" /> </act ion-state>
添加流程节点:<act
ion-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>
<act
ion-state id="captchaValidate">
<action bean="captchaValidateAction" />
<transition on="success" to="sendTicketGrantingTicket" />
<transition on="error" to="viewLoginForm" />
</action-state>
CaptchaImageCreateController.javapackage com.ist.cas;
import org.jasig.cas.web.support.WebUtils;
import org.springframework.webflow.act ion.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;
}
}
JCaptchaEngineEx.javapackage 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;
}
}
CASPasswordEncoder.javapackage 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));
}
}
页面配置方面,修改cas\WEB-INF\view\jsp\myth\ui\casLoginView.jsp在密码输入框下新增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;
}
}另外发现一个问题,验证出错时的提示信息问题,官方没有给出一个提示的写法,而且提示时新增异常也会导致验证直接成功,所以验证码的提示信息在页面独立分开<li> <span class="itemname">验证码:</span> <span class="iteminput"> <input name = "j_captcha_response" type = "text"> <img src = "captcha.htm">
<a href="#" on
click="javas cript: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>
在 :cas\WEB-INF\cas.properties,增加密码有效期属性。<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.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.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/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,用于前台加密
配置:cas\WEB-INF\cas-servlet.xml,注释原密码加密功能<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>
<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>
为<act
<action-state id="submit"> ion bean="authenticationViaFormAction" method="submit" /> <transition on="warn" to="warn" /> <transition on="success" to="sendTicketGrantingTicket" /> <transition on="error" to="viewLoginForm" /> </act ion-state> <action-state id="captchaValidate"> <action bean="captchaValidateAction" /> <transition on="success" to="sendTicketGrantingTicket" /> <transition on="error" to="viewLoginForm" /> </action-state>
添加流程节点:<act
ion-state id="submit"> <act ion bean="authenticationViaFormAction" method="submit" /> <transition on="warn" to="warn" /> <transition on="success" to="captchaValidate" /> <transition on="error" to="errorCount" /> </act ion-state> <action-state id="captchaValidate"> <action bean="captchaValidateAction" /> <transition on="success" to="sendTicketGrantingTicket" /> <transition on="error" to="errorCount" /> </action-state>
<act
ion-state id="errorCount"> <act ion bean="captchaErrorCountAction" /> <transition on="success" to="viewLoginForm" /> </act ion-state>
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; } }