本文主要介紹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>