<form:form id="loginForm" method="post" commandName="${commandName}" htmlEscape="true"> <form:errors path="*" element="em" cssClass="" /> <c:if test="${not empty sessionScope.openIdLocalId}"> 用户名<input id="username" name="username" type="text" value="${sessionScope.openIdLocalId}" /> </c:if> <c:if test="${empty sessionScope.openIdLocalId}"> <!-- 这里您不要直接使用 input,而使用 springframework 的 form:input 能够在登录出错时,还能保证显示之前登录时输入的用户名 --> <form:input id="username" path="username" htmlEscape="true" /> </c:if> 密码:<input id="password" name="password" type="password" /> 验证码:<input id="validateCode" name="validateCode" type="text" /><img src="/captcha.jpg" /> <input id="rememberMe" name="rememberMe" type="checkbox" value="true" /><label for="rememberMe">记住用户名</label> <input id="login-Btn" type="button" value="登录" /> <input name="lt" type="hidden" value="${loginTicket}" /> <input name="execution" type="hidden" value="${flowExecutionKey}" /> <input name="_eventId" type="hidden" value="submit" /> </form:form> <!-- 需要 jquery.cookie 和 jquery.md5 插件 --> <script type="text/javascript"> $(document).ready(function(){ var loginUsername = $.cookie("loginUsername"); if(loginUsername){ $("#username").val(loginUsername); $("#rememberMe").attr("checked", true); } $("#login-Btn").click(function(){ /* 为了密码传输安全,表单提交之前,进行 md5 加密 */ $("#password").val($.md5($("#password").val())); if($("#rememberMe").is(":checked") == true){ $.cookie("loginUsername", $("#username").val(), {expires: 365}); }else{ $.cookie("loginUsername", null, {expires: -1}); } $("#loginForm").submit(); }); }); </script>
此例子中的验证实现是使用的 Google 的 Kaptcha
Google Kaptcha 配置(在 WEB-INF/spring-configuration/applicationContext.xml 添加)
<bean id="captchaConfig" class="com.google.code.kaptcha.util.Config"> ... ... </bean> <bean id="captchaProducer" class="com.google.code.kaptcha.impl.DefaultKaptcha" p:config-ref="captchaConfig" />
Servlet 配置
1)修改 WEB-INF/cas-servlet.xml 中的
<bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction" p:centralAuthenticationService-ref="centralAuthenticationService" p:warnCookieGenerator-ref="warnCookieGenerator"/>
为 : AuthenticationCaptchaViaFormAction.java(用户验证 Action)
<bean id="authenticationViaFormAction" class="com.buession.cas.web.flow.AuthenticationCaptchaViaFormAction" p:captchaConfig-ref="captchaConfig" p:centralAuthenticationService-ref="centralAuthenticationService" p:warnCookieGenerator-ref="warnCookieGenerator" />
2)在 WEB-INF/cas-servlet.xml 添加
CaptchaController.java(验证码控制器)
ValidateCaptchaController.java(验证码异步验证控制器)
<bean id="handlerMappingC" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="mappings"> <props> <prop key="/captcha.jpg">captchaController</prop><!-- 验证码 Controller --> <prop key="/validateCaptcha">validateCaptchaController</prop><!-- 验证码异步验证 Controller,不是必须 --> ... ... </props> </property> </bean> <bean id="captchaController" class="com.buession.cas.web.controller.CaptchaController" p:config-ref="captchaConfig" /> <bean id="validateCaptchaController" class="com.buession.cas.web.controller.ValidateCaptchaController" p:config-ref="captchaConfig" />
3)记得在 web.xml 添加 Servlet 和 URL 之间映射
数据库配置(在添加 WEB-INF/spring-configuration/applicationContext.xml 添加)
<bean id="masterJdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" p:dataSource-ref="masterDataSource" /> <bean id="slaveJdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" p:dataSource-ref="slaveDataSource" /> <bean id="masterDataSource" class="org.apache.commons.dbcp.BasicDataSource" ... ... destroy-method="close" /> <bean id="slaveDataSource" class="org.apache.commons.dbcp.BasicDataSource" ... ... destroy-method="close" />
为什么要配置 master 数据源和 slave 数据源?
用户登录时,查询用户信息是只读,我们就从从库中查询,主库是可以用于向数据库中写入用户登录日志,或者密码错误次数(当然这里可以根据实际需求调整,比如用户密码输入错误次数,也是可以记录到 Memcache、Redis 等缓存服务器中的)
修改用户凭证 credentials(WEB-INF/login-webflow.xml)
修改
<var name="credentials" class="org.jasig.cas.authentication.principal.UsernamePasswordCredentials" />
为:RememberMeUsernamePasswordCaptchaCredentials.java(带有 ”RememberMe“ 和 ”验证码“的用户凭证)
<var name="credentials" class="com.buession.cas.authentication.principal.RememberMeUsernamePasswordCaptchaCredentials" />
修改 AuthenticationViaFormAction(WEB-INF/login-webflow.xml)
修改
<view-state id="viewLoginForm" view="casLoginView" model="credentials"> <binder> <binding property="username" /> <binding property="password" /> </binder> ... ... </view-state>
为(binding 的 property 属性值一定要与 Credentials 的属性名和登录表单控件的 name 属性的值保持一致)
<view-state id="viewLoginForm" view="casLoginView" model="credentials"> <binder> <binding property="username" /> <binding property="password" /> <binding property="validateCode" /> </binder> ... ... </view-state>
添加数据库 authentication handlers(在 WEB-INF/deployerConfigContext.xml 中添加)
<bean id="authenticationManager" class="org.jasig.cas.authentication.AuthenticationManagerImpl"> ... ... <property name="authenticationHandlers"> <list> <bean class="com.domain.authentication.handler.DatabaseAuthenticationHandler"> <property name="jdbcTemplate" ref="slaveJdbcTemplate" /> <!--(1)--><property name="sql" value="SELECT `password`, `salt`, `algo` FROM `member` WHERE `username` = ? LIMIT 1" /> <!--(2)--><property name="sql" value="SELECT `password`, `salt`, `algo` FROM `member` WHERE `username` = ? OR `email` = ? OR `mobile` = ? LIMIT 1" /> <!--(3)--><property name="sql" value="SELECT `password`, `salt`, `algo` FROM `member` WHERE `username` = ? OR `email` = ? OR `mobile` = ?" /> <!-- (1)适用于单一条件登录;(2)适用于多条件,且这些条件中不会出现相同值的情况;(3)适用于多条件,但这些条件中可能会存在相同值的情况,例如:存在 username 为:13800138000,和 mobile 也为:13800138000 的不同两条用户数据 --> <property name="passwordEncoder" ref="passwordEncoder" /> </bean> </list> </property> </bean>
class DatabaseAuthenticationHandler extends com.buession.cas.authentication.handler.support.DatabaseQueryAuthenticationHandler { @Override protected boolean authenticateUsernamePasswordInternal(UsernamePasswordCredentials credentials) throws AuthenticationException { PasswordEncoder passwordEncoder = (PasswordEncoder) getPasswordEncoder(); String username = getPrincipalNameTransformer().transform(credentials.getUsername()); String password = credentials.getPassword(); (1)、(2) try { final Map<String, Object> r = jdbcTemplate.queryForMap(sql, username, username, username); if (valid(username, password, r, passwordEncoder) == true) { /** * 对旧用户数据更换加密方式,且重新生成密码 */ if (Mcrypt.MD5.equals(r.get("algo")) == true) { modifyEncoder((String) r.get("username"), credentials.getPassword(), (String) r.get("salt")); } return ture; } } catch (IncorrectResultSizeDataAccessException e) { } (3) try { List<Map<String, Object>> result = jdbcTemplate.queryForList(sql, username, username, username); if (result != null && result.size() > 0) { for (Map<String, Object> r : result) { if (valid(username, password, r, passwordEncoder) == true) { /** * 对旧用户数据更换加密方式,且重新生成密码 */ if (Mcrypt.MD5.equals(r.get("algo")) == true) { modifyEncoder((String) r.get("username"), credentials.getPassword(), (String) r.get("salt")); } return true; } } } } catch (IncorrectResultSizeDataAccessException e) { } return false; } private boolean valid(String username, String password, final Map<String, Object> object, PasswordEncoder passwordEncoder) { String salt = (String) object.get("salt"); String algo = (String) object.get("algo"); passwordEncoder.setAlgo(algo); /** * 如果加密方式为 "MD5",则为老的加密方式 * 旧系统中,密码为明文传输,在后端 MD5 加密的, * 现在密码在前端进行了 MD5 加密传输,所以无需双重加密,而直接 encode(password+salt) */ if (Mcrypt.MD5.equals(algo) == true) { password += salt; } else { passwordEncoder.setSalt(salt); } final String encodedPassword = passwordEncoder.encode(password); return encodedPassword != null && encodedPassword.equalsIgnoreCase((String) object.get("password")); } private void modifyEncoder(final String username, final String password, final String salt) { PasswordEncoder passwordEncoder = (PasswordEncoder) getPasswordEncoder(); passwordEncoder.setAlgo(Mcrypt.SHA512); passwordEncoder.setSalt(salt); String sql = "UPDATE `member` SET `algo` = ?, `password` = ? WHERE `username` = ?"; jdbcTemplate.update(sql, Mcrypt.SHA512, passwordEncoder.encode(password), username); } }
密码加密配置(在添加 WEB-INF/deployerConfigContext.xml 添加)
<bean id="passwordEncoder" class="com.buession.cas.authentication.handler.PasswordEncoder" p:characterEncoding="UTF-8" />
PasswordEncoder.java
添加验证码验证消息(WEB-INF/classes/messages_xxx.properties)
required.captcha=验证码不能为空 INVALID_CAPTCHA=验证码错误