• 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=u4f60u8fd8u672au9a8cu8bc1u90aeu7bb1uff0cu8bf7u5148u9a8cu8bc1u90aeu7bb1u540eu518du767bu5f55

    后面的文字是使用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>
  • 相关阅读:
    35.使用拦截器实现权限验证
    34.TokenInterceptor防止表单重复提交
    33.使用默认的execAndWait拦截器
    32.使用来MethodFilterInterceptor灵活拦截
    31.自定义拦截器
    sql 学习笔记 档
    AVR文章7课时:动态数字化控制
    MapReduce 规划 系列的12 使用Hadoop Streaming技术集成newLISP文字
    左右TS分析流
    Java达到MySQL数据库备份(两)
  • 原文地址:https://www.cnblogs.com/gao241/p/3367805.html
Copyright © 2020-2023  润新知