# 系统登录组件

# 登录认证器

登录认证器是一种认证单元,一个认证策略就需要实现一套认证器。典型应用场景如下:AD/LDAP统一认证、双因素认证、CA认证实现、帐号密码认证。

# 默认认证器

平台提供了多元化的认证器,只要任意一个认证器认证通过,即可当作登录成功。系统缺省提供几个验证器:

验证器 说明
com.seeyon.ctp.portal.sso.login.SSOTicketLoginAuthentication 单点登录认证
com.seeyon.ctp.login.auth.QrCodeLoginAuthentication 二维码登录
com.seeyon.v3x.plugin.ca.CALoginAuthentication 使用CA认证
com.seeyon.ctp.login.IdentificationDogLoginAuthentication 使用身份验证狗认证
com.seeyon.apps.ldap.login.LDAPLoginAuthentication 使用LDAP认证
com.seeyon.ctp.login.auth.CASLoginAuthentication CIP集成平台CAS认证
com.seeyon.ctp.login.auth.SMSLoginAuthentication 短信验证码认证
com.seeyon.ctp.login.auth.DefaultLoginAuthentication 使用协同用户系统认证

# 实现原理

每一套认证器均是集成AbstractLoginAuthentication,重写authenticate方法,并按如下注释说明来控制成功还是失败:

/**
     * 认证,策略是:
     *
     * <ol>
     *   <li>系统框架把登录界面发过来的<code>HttpServletRequest</code>完整的传递给认证实现类</li>
     *   <li>认证实现类完成自己的逻辑</li>
     *   <li>返回值约定
     *      <ol type="i">
     *        <li>当验证通过,需要给框架返回[用户名, 密码],框架将直接跳转到首页</li>
     *        <li>当不验证通过,但要终止本次登录请求,直接throw new LoginAuthenticationException(),用户将跳转到登录页</li>
     *        <li>当不验证通过,返回null,框架将调用下一个认证类认证</li>
     *      </ol>
     *   </li>
     * </ol>
     * 
     * @return 认证通过: 返回[用户名, 密码]; 验证不通过返回null,框架将调用下一个认证类认证
     * @throws LoginAuthenticationException 用户将跳转到登录页
     */
    String[] authenticate(HttpServletRequest request, HttpServletResponse response)
            throws LoginAuthenticationException;

# 实现示例

要实现自己的登录认证,必须:

1、继承com.seeyon.ctp.login.AbstractLoginAuthentication,实现自己的登录验证类

package com.seeyon.apps.login;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.seeyon.ctp.common.constants.Constants;
import com.seeyon.ctp.common.constants.LoginConstants;
import com.seeyon.ctp.login.AbstractLoginAuthentication;
import com.seeyon.ctp.login.LoginAuthenticationException;

public class CustomLoginAuthentication extends AbstractLoginAuthentication {
    @Override
    public String[] authenticate(HttpServletRequest request,
            HttpServletResponse response) throws LoginAuthenticationException {
        String username = request.getParameter(LoginConstants.USERNAME);// 用户名
        String password = request.getParameter(LoginConstants.PASSWORD);// 密码
        if (username == null || password == null) {
            return null;
        }
        //登录方式,判断是否移动应用登陆
        String userAgentFrom = request.getParameter(Constants.LOGIN_USERAGENT_FROM);
        boolean fromMobile = Constants.login_useragent_from.mobile.name().equals(userAgentFrom) || LoginUtil.isFromM1(userAgentFrom);

        if (check(username, password)) {
            return new String[] { username, password };
        }

        return null;
    }

    private boolean check(String username, String password) {
        // 登录认证逻辑,用户名和密码正确时返回true即可
        return false;
    }
}

2、将自定义认证器注册到Spring bean中:

<bean class="com.seeyon.apps.login.CustomLoginAuthentication"/>

# 登录认证拦截器

登录认证拦截器用于在认证前、认证后进行拦截、记录登录行为以及做前置、后置动作。

典型应用场景如下:登录认证前做IP访问控制、登录成功后记录到审计日志、计算登录前、后执行耗时。

登录认证拦截器使用Spring的Interceptor拦截器实现。

# 默认登录拦截器

系统缺省通过几个拦截器实现一些功能控制:

验证器 说明
com.seeyon.ctp.login.interceptor.VerifyCodeLoginInterceptor 验证码拦截器,在preHandle对用户输入的验证码进行校验
com.seeyon.ctp.login.interceptor.LockLoginInterceptor 锁定用户拦截器
com.seeyon.ctp.login.interceptor.IpcontrolLoginInterceptor IP控制拦截器
com.seeyon.ctp.login.interceptor.MutilBrowserLoginInterceptor 限制管理员只能使用PC,通过IE登录

# 实现原理

每一个拦截器均继承自AbstractLoginInterceptor,并根据使用场景重写“登录前preHandle”、“登录后afterComplete”、“登录失败afterFailure”的方法:

/**
     * 登录之前的操作

     * 
     * 此时AppContext.getCurrentUser()是null的
     * 
     * @param request
     * @param response
     * @return Error标示本次登录终止,返回到登录页;OK正常往下进行
     */
    public LoginResult preHandle(HttpServletRequest request, HttpServletResponse response);

    /**
     * 在登录验证成功后的操作

     * 此时AppContext.getCurrentUser()有值
     * @param request
     * @param response
     * @return Error标示本次登录终止,返回到登录页;OK正常往下进行
     */
    public LoginResult afterComplete(HttpServletRequest request, HttpServletResponse response);

    /**
     * 在登录验证失败后的操作
     * 
     * @param request
     * @param response
     * @return Error不管怎么样都会跳转到登录页面,最好别返回LoginResult.OK他也没有用
     */
    public LoginResult afterFailure(HttpServletRequest request, HttpServletResponse response);

# 实现示例

可以通过以下步骤实现登录拦截:

1、继承com.seeyon.ctp.login.AbstractLoginInterceptor,实现自己的登录拦截器类

package com.seeyon.apps.login;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.seeyon.ctp.common.constants.LoginConstants;
import com.seeyon.ctp.login.AbstractLoginInterceptor;
import com.seeyon.ctp.login.LoginAuthenticationException;

public class CustomMutilBrowserLoginInterceptor extends com.seeyon.ctp.login.AbstractLoginInterceptor {
    public CustomMutilBrowserLoginInterceptor() {
    }
    //管理员只能通过IE、PC访问
    public LoginResult afterComplete(HttpServletRequest request,HttpServletResponse response) {
        User currentUser = CurrentUser.get();
        if(currentUser.isAdmin()) {
            Boolean A8Allow4Admin = (Boolean)(BrowserFlag.A8Allow4Admin.getFlag(request));

            //管理员只能从IE上登录
            if (Boolean.FALSE.equals(A8Allow4Admin)) {
                return LoginResult.ERROR_IPCONTROLIPAD;
            }

            //管理员不能从M1上登录
            if(currentUser.isFromM1() || Constants.login_useragent_from.mobile.name().equals(currentUser.getUserAgentFrom())){
                return LoginResult.ERROR_ForbiddenAdminLogin;
            }
        }

        return LoginResult.OK;
    }
}

2、注册Spring bean,将实现类在自己的Spring插件配置下注册:

<bean class="com.seeyon.apps.login.CustomMutilBrowserLoginInterceptor"/>

# 执行顺序调整

在大多数场景下,同一套产品只会有一个主要的LoginAuthentication生效,而LoginInterceptor对顺序不敏感,原则上不需要排序。

但如果要对LoginAuthentication和LoginInterceptor进行排序,可以在插件目录定义一个login.xml,如(WEB-INF\cfgHome\plugin\myplugin\login.xml),并按如下配置示例进行属性配置:

<?xml version="1.0" encoding="UTF-8"?>
<login>
	<!-- 将CustomLoginInterceptor放到com.seeyon.ctp.login.interceptor.LockLoginInterceptor之前执行。 -->
    <bean class="com.seeyon.apps.login.CustomLoginInterceptor" before1="com.seeyon.ctp.login.interceptor.LockLoginInterceptor"/>
	<!-- 将CustomLoginAuthentication放到com.seeyon.v3x.plugin.ca.CALoginAuthentication之后执行。 -->
    <bean class="com.seeyon.apps.login.CustomLoginAuthentication" after2="com.seeyon.v3x.plugin.ca.CALoginAuthentication"/>
</login>

一旦定义了login.xml,对应的bean就无需在spring xml中注册。

编撰人:lichaoj、het