CAS自定義登入驗證方法

沒頭腦的土豆發表於2013-10-14

一、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負責進行登入認證、建立TGTST、驗證票據等邏輯,該類中註冊了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>

 

相關文章