CAS自定义登录验证方法
一、CAS登录认证原理
CAS认证流程如下图:
CAS服务器的org.jasig.cas.authentication.AuthenticationManager负责基于提供的凭证信息进行用户认证。与Spring Security很相似,实际的认证委托给了一个或多个实现了org.jasig.cas.authentication.handler.AuthenticationHandler接口的处理类。
最后,一个org.jasig.cas.authentication.principal.CredentialsToPrincipalResolver用来将传递进来的安全实体信息转换成完整的org.jasig.cas.authentication.principal.Principal(类似于Spring Security中UserDetailsService实现所作的那样)。
二、自定义登录认证
CAS内置了一些AuthenticationHandler实现类,如下图所示,在cas-server-support-jdbc包中提供了基于jdbc的用户认证类。
如果需要实现自定义登录,只需要实现org.jasig.cas.authentication.handler.AuthenticationHandler接口即可,当然也可以利用已有的实现,比如创建一个继承自 org.jasig.cas.adaptors.jdbc.AbstractJdbcUsernamePasswordAuthenticationHandler的类,实现方法可以参考org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler类:
package org.jasig.cas.adaptors.jdbc; import org.jasig.cas.authentication.handler.AuthenticationException; import org.jasig.cas.authentication.principal.UsernamePasswordCredentials; import org.springframework.dao.IncorrectResultSizeDataAccessException; import javax.validation.constraints.NotNull; public final class QueryDatabaseAuthenticationHandler extends AbstractJdbcUsernamePasswordAuthenticationHandler { @NotNull private String sql; protected final boolean authenticateUsernamePasswordInternal(final UsernamePasswordCredentials credentials) throws AuthenticationException { final String username = getPrincipalNameTransformer().transform(credentials.getUsername()); final String password = credentials.getPassword(); final String encryptedPassword = this.getPasswordEncoder().encode( password); try { final String dbPassword = getJdbcTemplate().queryForObject( this.sql, String.class, username); return dbPassword.equals(encryptedPassword); } catch (final IncorrectResultSizeDataAccessException e) { // this means the username was not found. return false; } } /** * @param sql The sql to set. */ public void setSql(final String sql) { this.sql = sql; } }
修改authenticateUsernamePasswordInternal方法中的代码为自己的认证逻辑即可。
注意:不同版本的handler实现上稍有差别,请参考对应版本的hanlder,本文以3.4为例。
三、自定义登录错误提示消息
CAS核心类CentralAuthenticationServiceImpl负责进行登录认证、创建TGT、ST、验证票据等逻辑,该类中注册了CAS认证管理器AuthenticationManager,对应bean的配置如下:
<bean id="centralAuthenticationService" class="org.jasig.cas.CentralAuthenticationServiceImpl" p:ticketGrantingTicketExpirationPolicy-ref="grantingTicketExpirationPolicy" p:serviceTicketExpirationPolicy-ref="serviceTicketExpirationPolicy" p:authenticationManager-ref="authenticationManager" p:ticketGrantingTicketUniqueTicketIdGenerator-ref="ticketGrantingTicketUniqueIdGenerator" p:ticketRegistry-ref="ticketRegistry" p:servicesManager-ref="servicesManager" p:persistentIdGenerator-ref="persistentIdGenerator" p:uniqueTicketIdGeneratorsForService-ref="uniqueIdGeneratorsMap" />
CentralAuthenticationServiceImpl中的方法负责调用AuthenticationManager进行认证,并捕获AuthenticationException类型的异常,如创建ST的方法grantServiceTicket代码示例如下:
if (credentials != null) { try { final Authentication authentication = this.authenticationManager .authenticate(credentials); final Authentication originalAuthentication = ticketGrantingTicket.getAuthentication(); if (!(authentication.getPrincipal().equals(originalAuthentication.getPrincipal()) && authentication.getAttributes().equals(originalAuthentication.getAttributes()))) { throw new TicketCreationException(); } } catch (final AuthenticationException e) { throw new TicketCreationException(e); } }
在CAS WEBFLOW流转的过程中,对应的action就会捕获这些TicketCreationException,并在表单中显示该异常信息。
如org.jasig.cas.web.flow.AuthenticationViaFormAction类中的表单验证方法代码如下:
public final String submit(final RequestContext context, final Credentials credentials, final MessageContext messageContext) throws Exception { final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context); final Service service = WebUtils.getService(context); if (StringUtils.hasText(context.getRequestParameters().get("renew")) && ticketGrantingTicketId != null && service != null) { try { final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicketId, service, credentials); WebUtils.putServiceTicketInRequestScope(context, serviceTicketId); putWarnCookieIfRequestParameterPresent(context); return "warn"; } catch (final TicketException e) { if (e.getCause() != null && AuthenticationException.class.isAssignableFrom(e.getCause().getClass())) { populateErrorsInstance(e, messageContext); return "error"; } this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketId); if (logger.isDebugEnabled()) { logger.debug("Attempted to generate a ServiceTicket using renew=true with different credentials", e); } } } try { WebUtils.putTicketGrantingTicketInRequestScope(context, this.centralAuthenticationService.createTicketGrantingTicket(credentials)); putWarnCookieIfRequestParameterPresent(context); return "success"; } catch (final TicketException e) { populateErrorsInstance(e, messageContext); return "error"; } }
因此在自定义的AuthenticationHandler类的验证方法中抛出继承自AuthenticationException的异常,登录页面(默认为WEB-INF/view/jsp/default/ui/casLoginView.jsp)中的Spring Security验证表单将会自动输出该异常对应的错误消息。
CAS AuthenticationException结构如下图,CAS已经内置了一些异常,比如用户名密码错误、未知的用户名错误等。
假设这样一个需求:用户注册时需要验证邮箱才能登录,如果未验证邮箱,则提示用户还未验证邮箱,拒绝登录。
为实现未验证邮箱后提示用户的需求,定义一个继承自AuthenticationException的类:UnRegisterEmailAuthenticationException,代码示例如下:
package test; import org.jasig.cas.authentication.handler.BadUsernameOrPasswordAuthenticationException; public class UnRegisterEmailAuthenticationException extends BadUsernameOrPasswordAuthenticationException { /** Static instance of UnknownUsernameAuthenticationException. */ public static final UnRegisterEmailAuthenticationException ERROR = new UnRegisterEmailAuthenticationException(); /** Unique ID for serializing. */ private static final long serialVersionUID = 3977861752513837361L; /** The code description of this exception. */ private static final String CODE = "error.authentication.credentials.bad.unregister.email"; /** * Default constructor that does not allow the chaining of exceptions and * uses the default code as the error code for this exception. */ public UnRegisterEmailAuthenticationException() { super(CODE); } /** * Constructor that allows for the chaining of exceptions. Defaults to the * default code provided for this exception. * * @param throwable the chained exception. */ public UnRegisterEmailAuthenticationException(final Throwable throwable) { super(CODE, throwable); } /** * Constructor that allows for providing a custom error code for this class. * Error codes are often used to resolve exceptions into messages. Providing * a custom error code allows the use of a different message. * * @param code the custom code to use with this exception. */ public UnRegisterEmailAuthenticationException(final String code) { super(code); } /** * Constructor that allows for chaining of exceptions and a custom error * code. * * @param code the custom error code to use in message resolving. * @param throwable the chained exception. */ public UnRegisterEmailAuthenticationException(final String code, final Throwable throwable) { super(code, throwable); } }
请注意代码中的CODE私有属性,该属性定义了一个本地化资源文件中的键,通过该键获取本地化资源中对应语言的文字,这里只实现中文错误消息提示,修改WEB-INF/classes/messages_zh_CN.properties文件,添加CODE定义的键值对,如下示例:
error.authentication.credentials.bad.unregister.email=\u4f60\u8fd8\u672a\u9a8c\u8bc1\u90ae\u7bb1\uff0c\u8bf7\u5148\u9a8c\u8bc1\u90ae\u7bb1\u540e\u518d\u767b\u5f55
后面的文字是使用native2ascii工具编码转换的中文错误提示。
接下来只需要在自定义的AuthenticationHandler类的验证方法中,验证失败的地方抛出异常即可。
自定义AuthenticationHandler示例代码如下:
package cn.test.web; import javax.validation.constraints.NotNull; import org.jasig.cas.adaptors.jdbc.AbstractJdbcUsernamePasswordAuthenticationHandler; import org.jasig.cas.authentication.handler.AuthenticationException; import org.jasig.cas.authentication.principal.UsernamePasswordCredentials; import org.springframework.dao.IncorrectResultSizeDataAccessException; public class CustomQueryDatabaseAuthenticationHandler extends AbstractJdbcUsernamePasswordAuthenticationHandler { @NotNull private String sql; @Override protected boolean authenticateUsernamePasswordInternal(UsernamePasswordCredentials credentials) throws AuthenticationException { final String username = getPrincipalNameTransformer().transform(credentials.getUsername()); final String password = credentials.getPassword(); final String encryptedPassword = this.getPasswordEncoder().encode(password); try { // 查看邮箱是否已经验证。 Boolean isEmailValid= EmailValidation.Valid(); if(!isEmailValid){ throw new UnRegisterEmailAuthenticationException(); } //其它验证 …… } catch (final IncorrectResultSizeDataAccessException e) { // this means the username was not found. return false; } } public void setSql(final String sql) { this.sql = sql; } }
三、配置使自定义登录认证生效
最后需要修改AuthenticationManager bean的配置(一般为修改WEB-INF/spring-configuration/applicationContext.xml文件),加入自定义的AuthenticationHandler,配置示例如下:
<bean id="authenticationManager" class="org.jasig.cas.authentication.AuthenticationManagerImpl"> <property name="credentialsToPrincipalResolvers"> <list> <bean class="org.jasig.cas.authentication.principal.UsernamePasswordCredentialsToPrincipalResolver"> <property name="attributeRepository" ref="attributeRepository" /> </bean> <bean class="org.jasig.cas.authentication.principal.HttpBasedServiceCredentialsToPrincipalResolver" /> </list> </property> <property name="authenticationHandlers"> <list> <bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler" p:httpClient-ref="httpClient" p:requireSecure="false" /> <bean class="cn.test.web.CustomQueryDatabaseAuthenticationHandler"> <property name="sql" value="select password from t_user where user_name=?" /> <property name="dataSource" ref="dataSource" /> <property name="passwordEncoder" ref="passwordEncoder"></property> </bean> </list> </property> </bean>