EOS Low-Code Platform 8 EOS Low-Code Platform 8
产品简介
学习指南
更新说明
安装与集成
上线指南
初见EOS
低代码开发手册
专业代码开发手册
专题场景实战
公共服务框架
应用运行治理
运维指南
  • 认证原理及集成说明
  • 登录原理
  • 认证原理
  • 伪登录

# 认证原理及集成说明

# 登录原理

源码路径

登录接口位于AFCenter源码下的com.primeton.gocom.afcenter.oauth模块儿下,具体路径为:

com.primeton.gocom.afcenter.oauth.controller.LoginManagerController#login

时序图

login

登陆时,用户输入用户名与密码,后端会校验用户信息,包含用户校验、密码校验、安全策略校验等,校验通过后会去根据用户信息创建用户会话,初始化用户的UserObject对象,UserObject对象包含用户基本信息以及权限信息,后端同时会记录用户的登陆操作日志,然后将登录成功信息返回给用户,并将用户会话绑定UserObject。

# 认证原理

源码路径:

在AFCenter中基础的认证类源码在com.primeton.gocom.afcenter.common模块下,具体路径为:

com.primeton.gocom.afcenter.common.intercepter.UserLoginInterceptor

活动图

auth

默认实现的认证逻辑中,如果是白名单接口,会设置匿名对象进行上下文的初始化。如果不是白名单接口,会从请求中获取所需要的认证凭证信息,如果开启了CAS认证,则根据请求的url判断是否需要校验,需要校验则调用CAS的认证接口进行认证,不需要校验则根据请求参数判断是否需要登出或者是正常的接口调用,正常接口调用就根据凭证和会话信息进行eos上下文初始化,然后执行调用。如果凭证和会话信息无效,并且也不是白名单或者CAS的认证方式,则判断是否是OAuth2认证模式(访问授权码模式),如果不是,则返回401未授权信息,否则调用OAuth2的认证服务进行认证,认证成功后携带凭证信息重定向回配置的回调地址。

自定义认证说明

如果项目上有自定义认证需求,可以修改该拦截器及该拦截器的子类(取决于集成模式,starter集成还是sdk集成),子类分别位于如下位置:

starter集成com.primeton.gocom.afcenter.oauth.intercepter.AFCUserLoginInterceptor
sdk集成com.primeton.gocom.afcenter.sdk.SDKLoginInterceptor

# 伪登录

在某些特殊场景下,我们需要页面访问低开资源,此时用户信息可能通过url或者请求头来进行传递,那么我们就可以在拦截器中进行获取,然后通过凭证信息获取用户信息进行伪登录逻辑,完成功能调用,下面举一个例子来配合AFCenter中的拦截器进行伪登录的模拟行为(这里不考虑提供认证的服务的实现方案,只说明在AFCenter中何时、何地调用认证服务)。

示例:http://[ip:port]/index.html#/module/formDesigner/page/render?resourceType=form&authCode=123456

这个url中携带了authCode来传递用户认证信息,那么我们可以在拦截器中获取该参数,然后调用认证服务提供的接口进行认证,成功后初始化UserObject,将用户信息放入EOS上下文中在进行后续调用,具体demo如下:

注意:

  1. 示例urlhttp://[ip:port]/index.html#/module/formDesigner/page/render?resourceType=form&authCode=123456只是一个示例,不能直接使用,如果需要前端来转发授权码,需要前端也做出响应处理,将授权码转发到后端。
  2. 下方demo不能直接使用,需要根据现场情况进行修改,主要要修改的逻辑在//这里是伪登录逻辑实现的地方,在上述sdk白名单代码之后紧接着去实现就行这行注释之后,根据其余注释内容进行修改处理
  3. authCode不是固定的认证参数,只是举例子,可以换成其他任意的名称,注意前后端交互即可
package com.primeton.gocom.afcenter.common.intercepter;

import com.eos.access.http.IWebInterceptor;
import com.eos.access.http.IWebInterceptorChain;
import com.eos.access.http.OnlineUserManager;
import com.eos.access.http.UserLoginCheckedFilter;
import com.eos.common.cache.CacheFactory;
import com.eos.common.cache.CacheProperty;
import com.eos.common.cache.ICache;
import com.eos.data.datacontext.DataContextManager;
import com.eos.data.datacontext.IMUODataContext;
import com.eos.data.datacontext.IUserObject;
import com.eos.data.datacontext.UserObject;
import com.eos.runtime.core.TraceLoggerFactory;
import com.eos.spring.BeanFactory;
import com.eos.system.logging.Logger;
import com.eos.system.utility.JsonUtil;
import com.primeton.eos.api.springboot.EOS8ApplicationContext;
import com.primeton.eos.springboot.exception.ResponseErrorModel;
import com.primeton.ext.access.http.HttpMapContextFactory;
import com.primeton.ext.access.http.HttpUrlHelper;
import com.primeton.ext.common.muo.MUODataContextHelper;
import com.primeton.gocom.afcenter.api.oauth.ILoginService;
import com.primeton.gocom.afcenter.api.oauth.IOauthClientService;
import com.primeton.gocom.afcenter.api.resource.MenuUsageRecordService;
import com.primeton.gocom.afcenter.common.config.CrosConfig;
import com.primeton.gocom.afcenter.common.constants.CommonConstants;
import com.primeton.gocom.afcenter.common.context.DataPrivilegeHolder;
import com.primeton.gocom.afcenter.common.exception.AFCExceptionCode;
import com.primeton.gocom.afcenter.common.util.CasUtil;
import com.primeton.gocom.afcenter.common.util.CasUtil.ProxyTicketValidator;
import com.primeton.gocom.afcenter.common.util.UserUtil;
import com.primeton.gocom.afcenter.model.org.Employee;
import com.primeton.gocom.afcenter.model.org.User;
import com.primeton.system.exception.impl.ExceptionStatus;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.cors.CorsUtils;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

import static com.primeton.gocom.afcenter.common.constants.CommonConstants.API_PATH_PREFIX;
import static com.primeton.gocom.afcenter.common.constants.CommonConstants.SDK_URL_PREFIX;

/**
 * 登录拦截器
 *
 * @author hanjz
 * @date 2022年04月12日-15:16
 */
public class UserLoginInterceptor implements IWebInterceptor {

    private static final Logger logger = TraceLoggerFactory.getLogger(UserLoginInterceptor.class);

    public static final String AUTHORIZATION = "Authorization";
    private static final String ORIGINAL_URL = "original_url";
    public static final String TENANTID = "afc-tenantId";

    //API接口的认证请求头
    public static final String AFC_TOKEN = "afc-token";
    protected static boolean isSdk = true;
    public static final String ANONYMOUS = "anonymous";
    public static final String GATEWAY_VALIDATE_TOKEN_URL = API_PATH_PREFIX + "/login/validation-token";
    public static final String AFC_SSO_CAS_VALIDATE_URL = API_PATH_PREFIX + "/login/validation-sso-cas";
    
    private static ICache<String, String> casSsoServiceTicketMapCache;

    private static CrosConfig config;

    private static IOauthClientService oauthClientService;

    private static MenuUsageRecordService menuUsageRecordService;
    
    static {
        CacheProperty cacheConfig = new CacheProperty();
        cacheConfig.setCacheName("casSsoServiceTicketMapCache");
        casSsoServiceTicketMapCache = (ICache<String, String>)CacheFactory.getInstance().createCache(cacheConfig);
        BeanFactory instance = BeanFactory.getInstance();
        config = instance.getBean(CrosConfig.class);
        oauthClientService = instance.getBean(IOauthClientService.class);
        menuUsageRecordService = instance.getBean(MenuUsageRecordService.class);
    }

    @Override
    public void doIntercept(HttpServletRequest request, HttpServletResponse response, IWebInterceptorChain chain) throws IOException, ServletException {
        //添加跨域请求配置
        String originHeader = request.getHeader("Origin") != null
                ? request.getHeader("Origin").replace(" ", "")
                : "*";
        //如果配置了可信域,则只能可信域访问,否则默认所有跨域请求都不拦截
        boolean enableAllowedOrigins =
                config.getAllowedOrigins() != null && config.getAllowedOrigins().length > 0;
        if (CorsUtils.isCorsRequest(request) && enableAllowedOrigins &&
                !Arrays.asList(config.getAllowedOrigins()).contains(originHeader)) {
            ServletServerHttpResponse serverHttpResponse = new ServletServerHttpResponse(response);
            serverHttpResponse.setStatusCode(HttpStatus.FORBIDDEN);
            serverHttpResponse.getBody().write("Invalid CORS request".getBytes(StandardCharsets.UTF_8));
            serverHttpResponse.flush();
            return;
        }

        if (CorsUtils.isCorsRequest(request)) {
            response.setHeader("Access-Control-Allow-Origin", originHeader);
            response.setHeader("Access-Control-Allow-Methods", request.getMethod());
            response.setHeader("Access-Control-Allow-Credentials", "true");
            response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
            if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
                response.setHeader("Access-Control-Max-Age", "3600");
                response.setStatus(204);
                return;
            }
        }

        DataContextManager.current().setMapContextFactory(new HttpMapContextFactory(request, response));
        String servletPath = HttpUrlHelper.getRequestUrl(request, response);
        HttpSession session = null;

        //logger.info("请求入口:uniqueId:" + request.getHeader(AUTHORIZATION) + ", url:" + servletPath);
        //未登录的处理
        //排除设置的url
        if (HttpUrlHelper.isIn(servletPath, UserLoginCheckedFilter.getExcludeUrls()) || !HttpUrlHelper.isIn(servletPath, UserLoginCheckedFilter.getIncludeUrls())) {
            session = request.getSession();
            UserObject userObjectAnonymous = new UserObject();
            userObjectAnonymous.setUserId(ANONYMOUS);
            userObjectAnonymous.setUserName(ANONYMOUS);
            userObjectAnonymous.setUserRealName(ANONYMOUS);
            userObjectAnonymous.setUserRemoteIP(ANONYMOUS);
            String tenantId = request.getHeader(TENANTID);
            if (StringUtils.isNotBlank(tenantId)) {
                userObjectAnonymous.put(CommonConstants.TENANT, tenantId);
            }
            if (servletPath.startsWith(SDK_URL_PREFIX)) {
                //SDK接口,每次都会创建会话,单独设置10秒超时时间
                session.setMaxInactiveInterval(10);
            }
            //logger.error("白名单:session" + session);
            initContext(request, userObjectAnonymous, session);
            try {
                chain.doIntercept(request, response);
                return;
            } finally {
                DataPrivilegeHolder.clear();
            }
        }

        //这里是伪登录逻辑实现的地方,在上述sdk白名单代码之后紧接着去实现就行
        String authCode = request.getParameter("authCode");
        //如果没有这个参数既不进行伪登录,按照AFCenter原本逻辑继续执行
        if (StringUtils.isNotBlank(authCode)) {
            //调用认证服务器接口,完成用户信息的获取
            //.......
            //这里是获取到的用户id,当然也能是其他唯一标识员工的属性,这个根据具体项目而定
            String userId = "123";
            Employee emp = getEmployeeByUserId(userId);
            session = request.getSession();
            UserObject userObjectAnonymous = new UserObject();
            userObjectAnonymous.setUserId(emp.getId());
            userObjectAnonymous.setUserName(emp.getName());
            //租户id
            String tenantId = request.getHeader(TENANTID);
            if (StringUtils.isNotBlank(tenantId)) {
                userObjectAnonymous.put(CommonConstants.TENANT, tenantId);
            }
            //上述逻辑根据需要可以变更,后续逻辑可以直接照抄
            //初始化上下文可以直接调用该方法
            initContext(request, userObjectAnonymous, session);
            try {
                //过滤器链调用标准方法
                chain.doIntercept(request, response);
                return;
            } finally {
                DataPrivilegeHolder.clear();
            }
            //这是结尾,后面没有逻辑了,到这里自定义伪登录就结束了
        }
       	//省略开源源码中的其他逻辑..........
    }

}

这是URL携带参数的示例demo,拦截器的源码路径为com.primeton.gocom.afcenter.common.intercepter.UserLoginInterceptor,其中去认证服务器获取参数的方法可以在这个拦截器中定义一个空方法(模板方法),然后分别在oauth模块儿的com.primeton.gocom.afcenter.oauth.intercepter.AFCUserLoginInterceptor类和sdk模块儿的com.primeton.gocom.afcenter.sdk.SDKLoginInterceptor类分别去重写这个方法,这样可以保证不会出现依赖冲突,这里再说明下两个类的区别oauth模块的拦截器是后端starter集成方式使用的,sdk模块的拦截器是后端sdk集成模式使用的。

通过请求头传递参数的格式,跟url唯一的区别就是认证参数从请求头中获取,例如request.getHeader("authCode")。