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負責進行登錄認證、創建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>

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM