美文网首页虚拟化技术Ovirt
【Ovirt 笔记】单点登录分析与整理

【Ovirt 笔记】单点登录分析与整理

作者: 58bc06151329 | 来源:发表于2018-06-06 18:39 被阅读8次

    文前说明

    作为码农中的一员,需要不断的学习,我工作之余将一些分析总结和学习笔记写成博客与大家一起交流,也希望采用这种方式记录自己的学习之旅。

    本文仅供学习交流使用,侵权必删。
    不用于商业目的,转载请注明出处。

    分析整理的版本为 Ovirt 4.2.3 版本。

    1. 概念

    1.1 单点登录

    • 单点登录(Single Sign On)简称为 SSO,定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
      • SSO 需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。
      • 间接授权通过令牌实现,SSO 认证中心验证用户的用户名密码通过,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。
    • Ovirt 4.2 中将 engine 的管理员门户与虚拟机门户作为了不同的应用系统。通过 enginesso 这个独立的工程为 engine 提供单点登录的功能。将一些对登录的参数封装、拦截器(解决登录信息传递问题)等,放置到名为 aaa 的 JAR 包中引入。

    1.2 CORS 定义

    • Cross-Origin Resource Sharing(CORS)跨来源资源共享是一份浏览器技术的规范,提供了 Web 服务从不同域传来沙盒脚本的方法,以避开浏览器的同源策略,是 JSONP 模式的现代版。与 JSONP 不同,CORS 除了 GET 要求方法以外也支持其他的 HTTP 要求。
      • CORS 的基本思想是使用自定义的 HTTP 头部允许浏览器和服务器相互了解对方,从而决定请求或响应成功与否。
        • Access-Control-Allow-Origin,指定授权访问的域。
        • Access-Control-Allow-Methods,授权请求的方法(GET,POST,PUT, DELETE,OPTIONS 等)。
    • Ovirt engine 使用了 org.ebaysf.webcors-filter 来支持多域名配置,项目地址:https://github.com/ebay/cors-filter
    • maven 项目中可以在 pom.xml 文件中配置引入。
    <dependency>
       <groupId>org.ebaysf.web</groupId>
       <artifactId>cors-filter</artifactId>
       <version>1.0.1</version>
    </dependency>
    

    2. 分析与整理

    2.1 用户直接登录管理门户

    用户直接登录管理门户流程
    模块 描述
    webadmin 管理门户
    enginesso SSO 认证中心
    SsoLoginFilter 判断 SsoSession 是否存在,ovirt_aaa_engineSessionId 的值是否存在。
    SsoLoginServlet 组装授权参数,client_id=ovirt-engine-core,response_type=code,app_url=<webadmin_url>,scope=ovirt-app-admin ovirt-app-portal ovirt-ext=auth:sequence-priority=~,source_addr=<client_ipaddr>
    OAuthAuthorizeServlet 创建 SsoSession,将上一个步骤传递的参数保存到 session 中,组装访问域栈 AuthStack。
    InteractiveNextAuthServlet 访问域认证,取出栈中数据,依次认证。
    InteractiveNegotiateAuthServlet 外部域认证,从数据库中查询用户是否登录等信息。
    InteractiveAuthServlet 内部域认证,未登录抛出异常,转向登录 SSO 认证中心登录界。根据传递的用户名和密码,进行 SSO 认证中心登录认证,认证通过创建用户资格证书,处理资格证书,从数据库中查询用户信息,设置到 SsoSession 中,创建 Token 和 authCode,建立 Token 与 SsoSession 的映射表,将 Token 和 authCode 设置到 SsoSession 中。
    InteractiveRedirectToModuleServlet 封装转向业务系统的授权请求。code=<authCode>,app_url=<webadmin_url>,state=authenticated。
    SsoPostLoginServlet 处理授权信息。通过 authCode 获取 Token。创建业务系统自身 Session。
    OAuthTokenServlet SsoPostLoginServlet 中执行通过 authCode 获取 Token 的步骤。是一个模拟请求。
    OAuthTokenInfoServlet SsoPostLoginServlet 中执行通过 Token 获取用户信息等(从 SsoSession 中获取)的步骤。是一个模拟请求。

    2.1.1 用户选择管理门户

    <webModule>
           <groupId>org.ovirt.engine.ui</groupId>
           <artifactId>webadmin</artifactId>
           <bundleFileName>webadmin.war</bundleFileName>
           <contextRoot>/ovirt-engine/webadmin</contextRoot>
    </webModule>
    
    • 请求经过 SsoLoginFilter 过滤器过滤。
      • webadmin 工程的 web.xml 文件中配置。
      • 参数 /sso/login? 用于后续重定向 SSO 认证地址的组装。
    <filter>
         <filter-name>SsoLoginFilter</filter-name>
         <filter-class>org.ovirt.engine.core.aaa.filters.SsoLoginFilter</filter-class>
         <init-param>
             <param-name>login-url</param-name>
             <param-value>/sso/login?</param-value>
         </init-param>
    </filter>
    ......
    <filter-mapping>
         <filter-name>SsoLoginFilter</filter-name>
         <url-pattern>/WebAdmin.html</url-pattern>
    </filter-mapping>
    ......
    <welcome-file-list>
         <welcome-file>WebAdmin.html</welcome-file>
    </welcome-file-list>
    
    • 过滤器验证到该请求的 ovirt_aaa_engineSessionId 参数无效,重定向到 SSO 认证中心。
    public static final String HTTP_SESSION_ENGINE_SESSION_ID_KEY = "ovirt_aaa_engineSessionId";
    ......
    if (!FiltersHelper.isAuthenticated(req) || !FiltersHelper.isSessionValid((HttpServletRequest) request)) {
                    String url = String.format("%s%s&app_url=%s&locale=%s",
                            req.getServletContext().getContextPath(),
                            loginUrl,
                            URLEncoder.encode(requestURL.toString(), "UTF-8"),
                            request.getAttribute("locale").toString());
                    log.debug("Redirecting to {}", url);
                    res.sendRedirect(url);
    ......
    public static boolean isAuthenticated(HttpServletRequest request) {
            return (request.getSession(false) != null && request.getSession(false)
                    .getAttribute(SessionConstants.HTTP_SESSION_ENGINE_SESSION_ID_KEY) != null)
                    || request.getAttribute(SessionConstants.HTTP_SESSION_ENGINE_SESSION_ID_KEY) != null;
    }
    
    <servlet>
         <servlet-name>login</servlet-name>
         <servlet-class>org.ovirt.engine.core.aaa.servlet.SsoLoginServlet</servlet-class>
    </servlet>
    <servlet-mapping>
         <servlet-name>login</servlet-name>
         <url-pattern>/sso/login</url-pattern>
    </servlet-mapping>
    
    • SsoLoginServlet 主要是对认证参数进行了封装,记录了客户端 ID,IP 地址,访问范围等。
    URLBuilder urlBuilder = new URLBuilder(FiltersHelper.getEngineSsoUrl(request), "/oauth/authorize")
                    .addParameter("client_id", EngineLocalConfig.getInstance().getProperty("ENGINE_SSO_CLIENT_ID"))
                    .addParameter("response_type", "code")
                    .addParameter("app_url", request.getParameter("app_url"))
                    .addParameter("engine_url", FiltersHelper.getEngineUrl(request))
                    .addParameter("redirect_uri", redirectUri)
                    .addParameter("scope", scope)
                    .addParameter("source_addr", request.getRemoteAddr());
    
     <context-param>
         <param-name>post-action-url</param-name>
         <param-value>/ovirt-engine/webadmin/sso/oauth2-callback</param-value>
    </context-param>
    <context-param>
         <param-name>auth-seq-priority-property-name</param-name>
         <param-value>ENGINE_SSO_AUTH_SEQUENCE_webadmin</param-value>
    </context-param>
    
    [root@rhvm ~]# cat /usr/share/ovirt-engine/services/ovirt-engine/ovirt-engine.conf | grep ENGINE_SSO_AUTH_SEQUENCE_webadmin
    ENGINE_SSO_AUTH_SEQUENCE_webadmin=~
    
    • engine-server-ear 项目工程中定义访问 enginesso 项目工程,配置文件 pom.xml 中定义。
      • 上面的访问地址会将请求提交到该项目工程。
    <webModule>
         <groupId>org.ovirt.engine.core</groupId>
         <artifactId>enginesso</artifactId>
         <bundleFileName>enginesso.war</bundleFileName>
         <contextRoot>/ovirt-engine/sso</contextRoot>
    </webModule>
    
    • enginesso 项目工程的 web.xml 文件中配置了 CORSSupportFilter 过滤器,支持跨域请求。
    <filter>
        <filter-name>CORSSupport</filter-name>
        <filter-class>org.ovirt.engine.core.utils.servlet.CORSSupportFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>CORSSupport</filter-name>
        <url-pattern>/sso/*</url-pattern>
    </filter-mapping>
    
    • enginesso 项目工程的 web.xml 文件中配置了 OAuthAuthorizeServlet,处理认证请求,并且创建 SsoSession
      • 创建访问域(内部、外部)的栈。
    <servlet>
        <servlet-name>OAuthAuthorizeServlet</servlet-name>
        <servlet-class>org.ovirt.engine.core.sso.servlets.OAuthAuthorizeServlet</servlet-class>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>OAuthAuthorizeServlet</servlet-name>
        <url-pattern>/oauth/authorize</url-pattern>
    </servlet-mapping>
    
    protected SsoSession buildSsoSession(HttpServletRequest request)
                throws Exception {
            String clientId = SsoUtils.getRequestParameter(request, SsoConstants.HTTP_PARAM_CLIENT_ID);
            String scope = SsoUtils.getScopeRequestParameter(request, "");
            String state = SsoUtils.getRequestParameter(request, SsoConstants.HTTP_PARAM_STATE, "");
            String appUrl = SsoUtils.getRequestParameter(request, SsoConstants.HTTP_PARAM_APP_URL, "");
            String engineUrl = SsoUtils.getRequestParameter(request, SsoConstants.HTTP_PARAM_ENGINE_URL, "");
            String redirectUri = request.getParameter(SsoConstants.HTTP_PARAM_REDIRECT_URI);
            String sourceAddr = SsoUtils.getRequestParameter(request, SsoConstants.HTTP_PARAM_SOURCE_ADDR, "UNKNOWN");
            validateClientRequest(request, clientId, scope, redirectUri);
    
            // Create the session
            request.getSession(true);
    
            SsoSession ssoSession = SsoUtils.getSsoSession(request);
            ssoSession.setAppUrl(appUrl);
            ssoSession.setClientId(clientId);
            ssoSession.setSourceAddr(sourceAddr);
            ssoSession.setRedirectUri(redirectUri);
            ssoSession.setScope(scope);
            ssoSession.setState(state);
            ssoSession.getHttpSession().setMaxInactiveInterval(-1);
    
            if (StringUtils.isNotEmpty(engineUrl)) {
                ssoSession.setEngineUrl(engineUrl);
            } else {
                ssoSession.setEngineUrl(SsoUtils.getSsoContext(request).getEngineUrl());
            }
    
            return ssoSession;
    }
    
    • 访问域的栈,默认值从配置文件中获取。
    protected Stack<InteractiveAuth> getAuthSeq(SsoSession ssoSession) {
            String scopes = ssoSession.getScope();
            String appAuthSeq = ssoContext.getSsoLocalConfig().getProperty("SSO_AUTH_LOGIN_SEQUENCE");
    
            String authSeq = null;
            if (StringUtils.isEmpty(scopes) || !scopes.contains("ovirt-ext=auth:sequence-priority=")) {
                authSeq = "~";
            } else {
                for (String scope : SsoUtils.scopeAsList(scopes)) {
                    if (scope.startsWith("ovirt-ext=auth:sequence-priority=")) {
                        String[] tokens = scope.trim().split("=", 3);
                        authSeq = tokens[2];
                    }
                }
            }
    
            List<InteractiveAuth> authSeqList = getAuthListForSeq(authSeq);
    
            if (StringUtils.isNotEmpty(authSeq) && authSeq.startsWith("~")) {
                // get unique auth seq
                for (char c : appAuthSeq.toCharArray()) {
                    if (!authSeqList.contains(InteractiveAuth.valueOf("" + c))) {
                        authSeqList.add(InteractiveAuth.valueOf("" + c));
                    }
                }
                // intersect auth seq with sso auth seq settings
                authSeqList.retainAll(getAuthListForSeq(appAuthSeq));
            }
            Collections.reverse(authSeqList);
            Stack<InteractiveAuth> authSeqStack = new Stack<>();
            authSeqStack.addAll(authSeqList);
            return authSeqStack;
    }
    
    ......
    
    ssoSession.setAuthStack(getAuthSeq(ssoSession));
    
    [root@rhvm ~]# cat /usr/share/ovirt-engine/services/ovirt-engine/ovirt-engine.conf | grep SSO_AUTH_LOGIN_SEQUENCE
    SSO_AUTH_LOGIN_SEQUENCE=NI
    
    I {
            @Override
            public String getName() {
                return "Internal";
            }
    
            @Override
            public String getAuthUrl(HttpServletRequest request, HttpServletResponse response) {
                log.debug("Redirecting to Internal Auth Servlet");
                return request.getContextPath() + SsoConstants.INTERACTIVE_LOGIN_URI;
            }
        },
        N {
            @Override
            public String getName() {
                return "Negotiate";
            }
    
            @Override
            public String getAuthUrl(HttpServletRequest request, HttpServletResponse response) {
                log.debug("Redirecting to External Auth Servlet");
                return request.getContextPath() + SsoConstants.INTERACTIVE_LOGIN_NEGOTIATE_URI;
            }
    };
    
    public static final String INTERACTIVE_LOGIN_NEXT_AUTH_URI = "/interactive-login-next-auth";
    ......
    ssoSession.setAuthStack(getAuthSeq(ssoSession));
                if (ssoSession.getAuthStack().isEmpty()) {
                    throw new OAuthException(SsoConstants.ERR_CODE_ACCESS_DENIED,
                            ssoContext.getLocalizationUtils().localize(
                                    SsoConstants.APP_ERROR_NO_VALID_AUTHENTICATION_MECHANISM_FOUND,
                                    (Locale) request.getAttribute(SsoConstants.LOCALE)));
                }
                redirectUrl = request.getContextPath() + SsoConstants.INTERACTIVE_LOGIN_NEXT_AUTH_URI;
    
    • enginesso 项目工程的 web.xml 文件中配置了 InteractiveNextAuthServlet,处理该请求。
    <servlet>
        <servlet-name>InteractiveNextAuthServlet</servlet-name>
        <servlet-class>org.ovirt.engine.core.sso.servlets.InteractiveNextAuthServlet</servlet-class>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>InteractiveNextAuthServlet</servlet-name>
        <url-pattern>/interactive-login-next-auth</url-pattern>
    </servlet-mapping>
    
    <servlet>
            <servlet-name>InteractiveNegotiateAuthServlet</servlet-name>
            <servlet-class>org.ovirt.engine.core.sso.servlets.InteractiveNegotiateAuthServlet</servlet-class>
    </servlet>
    
    <servlet-mapping>
            <servlet-name>InteractiveNegotiateAuthServlet</servlet-name>
            <url-pattern>/interactive-login-negotiate/*</url-pattern>
    </servlet-mapping>
    
    protected void service(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            switch (SsoUtils.getSsoContext(request).getNegotiateAuthUtils().doAuth(request, response).getStatus()) {
                case Authn.AuthResult.NEGOTIATION_UNAUTHORIZED:
                    log.debug("External authentication failed redirecting to url: {}",
                            SsoConstants.INTERACTIVE_LOGIN_NEXT_AUTH_URI);
                    response.sendRedirect(request.getContextPath() + SsoConstants.INTERACTIVE_LOGIN_NEXT_AUTH_URI);
                    break;
                case Authn.AuthResult.SUCCESS:
                    log.debug("External authentication succeeded redirecting to module");
                    response.sendRedirect(request.getContextPath() + SsoConstants.INTERACTIVE_REDIRECT_TO_MODULE_URI);
                    break;
                case Authn.AuthResult.NEGOTIATION_INCOMPLETE:
                    log.debug("External authentication incomplete");
                    break;
            }
    }
    
    response.sendRedirect(authStack.pop().getAuthUrl(request, response));
    
    public static final String INTERACTIVE_LOGIN_URI = "/interactive-login";
    ......
    @Override
    public String getAuthUrl(HttpServletRequest request, HttpServletResponse response) {
         log.debug("Redirecting to Internal Auth Servlet");
         return request.getContextPath() + SsoConstants.INTERACTIVE_LOGIN_URI;
    }
    
    • enginesso 项目工程的 web.xml 文件中配置了 InteractiveAuthServlet,处理该请求。
    <servlet>
       <servlet-name>InteractiveAuthServlet</servlet-name>
       <servlet-class>org.ovirt.engine.core.sso.servlets.InteractiveAuthServlet</servlet-class>
    </servlet>
    
    <servlet-mapping>
       <servlet-name>InteractiveAuthServlet</servlet-name>
       <url-pattern>/interactive-login</url-pattern>
    </servlet-mapping>
    
    if (ssoSession == null) {
       throw new OAuthException(SsoConstants.ERR_CODE_INVALID_GRANT, ssoContext.getLocalizationUtils().localize(SsoConstants.APP_ERROR_SESSION_EXPIRED, (Locale) request.getAttribute(SsoConstants.LOCALE)));
    
    public static final String INTERACTIVE_LOGIN_FORM_URI = "/login.html";
    ......
    redirectUrl = request.getContextPath() + SsoConstants.INTERACTIVE_LOGIN_FORM_URI;
    ......
    if (redirectUrl != null) {
         response.sendRedirect(redirectUrl);
    }
    
    • enginesso 项目工程的 web.xml 文件中配置了 LoginForm,处理该请求,转向单点登录界面,要求登录。
    <servlet>
         <servlet-name>LoginForm</servlet-name>
         <jsp-file>/WEB-INF/login.jsp</jsp-file>
    </servlet>
    
    <servlet-mapping>
         <servlet-name>LoginForm</servlet-name>
         <url-pattern>/login.html</url-pattern>
    </servlet-mapping>
    

    2.1.2 用户登录 SSO 认证中心

    • 用户输入用户名、密码进行登录。
      • 登录界面 jsp 中配置了请求地址。
    <form class="form-horizontal" id="loginForm" method="post" action="${pageContext.request.contextPath}/interactive-login">
    
    • InteractiveAuthServlet 对请求进行处理,请求中包含了用户名、密码等信息。
      • 根据填写的用户名、密码等信息,组装用户的资格证书。
    private Credentials getUserCredentials(HttpServletRequest request) throws Exception {
            String username = SsoUtils.getFormParameter(request, USERNAME);
            String password = SsoUtils.getFormParameter(request, PASSWORD);
            String profile = SsoUtils.getFormParameter(request, PROFILE);
            Credentials credentials;
            // The code is invoked from the login screen as well as when the user changes password.
            // If the login form parameters are not present the code has been invoked from change password flow and
            // we extract the credentials from the credentials saved to sso session.
            if (username == null || password == null || profile == null) {
                credentials = SsoUtils.getSsoSession(request).getTempCredentials();
            } else {
                credentials = new Credentials(username, password, profile, ssoContext.getSsoProfiles().contains(profile));
            }
            return credentials;
    }
    
    • 认证用户
      • 根据传递的用户名查询出用户信息,放置到 SsoSession 的 authRecord 中。
    redirectUrl = authenticateUser(request, response, userCredentials);
    ......
    private String authenticateUser(
                HttpServletRequest request,
                HttpServletResponse response,
                Credentials userCredentials) throws ServletException, IOException, AuthenticationException {
         ......
         AuthenticationUtils.handleCredentials(ssoContext, request, userCredentials);
    }
    
    • 解析处理用户资格证书,判断用户有效性,如果通过验证,则生成 SsoSession
    SsoSession ssoSession = login(ssoContext, request, credentials, null, interactive);
    
    • 认证用户名信息的过程,并且向上下文 SsoContext 中,注册 SsoSession。
      • SsoSession 在前面的 OAuthAuthorizeServlet 中已经创建,这里直接获得。
      • 生成 Token 并设置。
      • 根据有无范围值 ovirt-ext=token:password-access,判断是否加密密码。
    public static SsoSession persistAuthInfoInContextWithToken(
                HttpServletRequest request,
                String password,
                String profileName,
                ExtMap authRecord,
                ExtMap principalRecord) throws Exception {
            String validTo = authRecord.get(Authn.AuthRecord.VALID_TO);
            String authCode = generateAuthorizationToken();
            String accessToken = generateAuthorizationToken();
    
            SsoSession ssoSession = getSsoSession(request, true);
            ssoSession.setAccessToken(accessToken);
            ssoSession.setAuthorizationCode(authCode);
    
            request.setAttribute(SsoConstants.HTTP_REQ_ATTR_ACCESS_TOKEN, accessToken);
    
            ssoSession.setActive(true);
            ssoSession.setAuthRecord(authRecord);
            ssoSession.setAutheticatedCredentials(ssoSession.getTempCredentials());
            getSsoContext(request).registerSsoSession(ssoSession);
    
            ssoSession.setPrincipalRecord(principalRecord);
            ssoSession.setProfile(profileName);
            ssoSession.setStatus(SsoSession.Status.authenticated);
            ssoSession.setTempCredentials(null);
            ssoSession.setUserId(getUserId(principalRecord));
            try {
                ssoSession.setValidTo(validTo == null ?
                        Long.MAX_VALUE : new SimpleDateFormat("yyyyMMddHHmmssZ").parse(validTo).getTime());
            } catch (Exception ex) {
                log.error("Unable to parse Auth Record valid_to value: {}", ex.getMessage());
                log.debug("Exception", ex);
            }
    
            persistUserPassword(request, ssoSession, password);
    
            ssoSession.touch();
            return ssoSession;
    }
    
    • 用户验证通过,转向业务系统(管理门户)。
    public static final String INTERACTIVE_REDIRECT_TO_MODULE_URI = "/interactive-redirect-to-module";
    ......
    return request.getContextPath() + SsoConstants.INTERACTIVE_REDIRECT_TO_MODULE_URI;
    ......
    if (redirectUrl != null) {
         response.sendRedirect(redirectUrl);
    }
    
    • enginesso 项目工程的 web.xml 文件中配置了 InteractiveRedirectToModuleServlet 处理该请求。
    <servlet>
        <servlet-name>InteractiveRedirectToModuleServlet</servlet-name>
        <servlet-class>org.ovirt.engine.core.sso.servlets.InteractiveRedirectToModuleServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>InteractiveRedirectToModuleServlet</servlet-name>
        <url-pattern>/interactive-redirect-to-module</url-pattern>
    </servlet-mapping>
    
    public static void redirectToModule(HttpServletRequest request,
                                            HttpServletResponse response)
                throws IOException {
            log.debug("Entered redirectToModule");
            try {
                SsoSession ssoSession = getSsoSession(request);
                URLBuilder redirectUrl = new URLBuilder(getRedirectUrl(request))
                        .addParameter("code", ssoSession.getAuthorizationCode());
                String appUrl = ssoSession.getAppUrl();
                if (StringUtils.isNotEmpty(appUrl)) {
                    redirectUrl.addParameter("app_url", appUrl);
                }
                String state = ssoSession.getState();
                if (StringUtils.isNotEmpty(state)) {
                    redirectUrl.addParameter("state", state);
                }
                String url = redirectUrl.build();
                response.sendRedirect(url);
                log.debug("Redirecting back to module: {}", url);
            } catch (Exception ex) {
                log.error("Error redirecting back to module: {}", ex.getMessage());
                log.debug("Exception", ex);
                throw new RuntimeException(ex);
            } finally {
                getSsoSession(request).cleanup();
            }
     }
    
    public static String getRedirectUrl(HttpServletRequest request) throws Exception {
            String uri = getSsoSession(request, true).getRedirectUri();
            return StringUtils.isEmpty(uri) ?
                    new URLBuilder(getSsoContext(request).getEngineUrl(), "/oauth2-callback").build()  : uri;
    }
    
    • 再次通过 engine-server-ear 项目工程中定义访问 webadmin 工程。
      • SsoPostLoginServlet 处理请求。
    <servlet>
         <servlet-name>SsoPostLoginServlet</servlet-name>
         <servlet-class>org.ovirt.engine.core.aaa.servlet.SsoPostLoginServlet</servlet-class>
         <init-param>
            <param-name>login-as-admin</param-name>
            <param-value>true</param-value>
         </init-param>
    </servlet>
    <servlet-mapping>
         <servlet-name>SsoPostLoginServlet</servlet-name>
         s<url-pattern>/sso/oauth2-callback</url-pattern>
    </servlet-mapping>
    
    • 这里插入一步,enginesso 项目工程的 web.xml 文件中配置了 SsoContextListener 监听器。服务启动时创建。
    <listener>
         <listener-class>org.ovirt.engine.core.sso.context.SsoContextListener</listener-class>
    </listener>
    
    • 监听器中将所有的 Client 信息进行了注册,放置到 SsoContext 中。转向业务系统时,用于认证。
      • 从数据库 sso_clients 表中读取所有数据。
    ssoContext.setSsoClientRegistry(DBUtils.getAllSsoClientsInfo());
    
    • 继续前面的步骤,SsoPostLoginServlet 执行的过程中,会进行业务系统的授权认证,通过访问 /oauth/token 生成(模拟请求)生成授权码。
      • 授权码通过读取配置文件获取。
      • 将授权码信息拼装为模拟请求的请求头信息。
      • enginesso 项目工程的 web.xml 文件中配置了 OAuthAuthorizeServlet ,处理请求。
        • 获取的授权信息与 SsoContext 中的 Client 信息能对应上,则认证通过。
    Map<String, Object> response = SsoOAuthServiceUtils.getToken("authorization_code", authCode, scope, redirectUri);
    FiltersHelper.isStatusOk(response);
    
    public static Map<String, Object> getToken(String grantType, String code, String scope, String redirectUri) {
            try {
                HttpPost request = createPost("/oauth/token");
                setClientIdSecretBasicAuthHeader(request);
                List<BasicNameValuePair> form = new ArrayList<>(4);
                form.add(new BasicNameValuePair("grant_type", grantType));
                form.add(new BasicNameValuePair("code", code));
                form.add(new BasicNameValuePair("redirect_uri", redirectUri));
                form.add(new BasicNameValuePair("scope", scope));
                request.setEntity(new UrlEncodedFormEntity(form, StandardCharsets.UTF_8));
                return getResponse(request);
            } catch (Exception ex) {
                return buildMapWithError("server_error", ex.getMessage());
            }
    }
    
    private static void setClientIdSecretBasicAuthHeader(HttpUriRequest request) {
            EngineLocalConfig config = EngineLocalConfig.getInstance();
            byte[] encodedBytes = Base64.encodeBase64(String.format("%s:%s",
                    config.getProperty("ENGINE_SSO_CLIENT_ID"),
                    config.getProperty("ENGINE_SSO_CLIENT_SECRET")).getBytes());
            request.setHeader(FiltersHelper.Constants.HEADER_AUTHORIZATION, String.format("Basic %s", new String(encodedBytes)));
    }
    
    <servlet>
            <servlet-name>OAuthTokenServlet</servlet-name>
            <servlet-class>org.ovirt.engine.core.sso.servlets.OAuthTokenServlet</servlet-class>
    </servlet>
    
    <servlet-mapping>
            <servlet-name>OAuthTokenServlet</servlet-name>
            <url-pattern>/oauth/token</url-pattern>
    </servlet-mapping>
    
    • OAuthTokenServlet 生成授权码
    case "authorization_code":
                    issueTokenForAuthCode(request, response, scope);
                    break;
    ......
    protected void issueTokenForAuthCode(
                HttpServletRequest request,
                HttpServletResponse response,
                String scope) throws Exception {
            String[] clientIdAndSecret = SsoUtils.getClientIdClientSecret(request);
            SsoUtils.validateClientRequest(request,
                    clientIdAndSecret[0],
                    clientIdAndSecret[1],
                    scope,
                    null);
            SsoSession ssoSession = handleIssueTokenForAuthCode(request, clientIdAndSecret[0], scope);
            log.debug("Sending json response");
            SsoUtils.sendJsonData(response, buildResponse(ssoSession));
    }
    
    protected Map<String, Object> buildResponse(SsoSession ssoSession) {
            Map<String, Object> payload = new HashMap<>();
            payload.put(SsoConstants.JSON_ACCESS_TOKEN, ssoSession.getAccessToken());
            payload.put(SsoConstants.JSON_SCOPE, StringUtils.isEmpty(ssoSession.getScope()) ? "" : ssoSession.getScope());
            payload.put(SsoConstants.JSON_EXPIRES_IN, ssoSession.getValidTo().toString());
            payload.put(SsoConstants.JSON_TOKEN_TYPE, "bearer");
            return payload;
    }
    
    • 根据授权码获取授权信息。通过访问 /oauth/token-info 生成(模拟请求)。
      • OAuthTokenServlet 一样,会进行业务系统的授权认证。认证通过才进行授权信息处理。
      • enginesso 项目工程的 web.xml 文件中配置了 OAuthTokenInfoServlet ,处理请求。
    <servlet>
            <servlet-name>OAuthTokenInfo</servlet-name>
            <servlet-class>org.ovirt.engine.core.sso.servlets.OAuthTokenInfoServlet</servlet-class>
    </servlet>
    
    <servlet-mapping>
            <servlet-name>OAuthTokenInfo</servlet-name>
            <url-pattern>/oauth/token-info</url-pattern>
    </servlet-mapping>
    
    • OAuthTokenInfoServlet 生成授权信息。
      • SsoSession 中读取用户信息。
    private Map<String, Object> buildResponse(SsoSession ssoSession, String password) {
            Map<String, Object> payload = new HashMap<>();
            payload.put(SsoConstants.JSON_ACTIVE, ssoSession.isActive());
            payload.put(SsoConstants.JSON_TOKEN_TYPE, "bearer");
            payload.put(SsoConstants.JSON_CLIENT_ID, ssoSession.getClientId());
            payload.put(SsoConstants.JSON_USER_ID, ssoSession.getUserIdWithProfile());
            payload.put(SsoConstants.JSON_SCOPE, StringUtils.isEmpty(ssoSession.getScope()) ? "" : ssoSession.getScope());
            payload.put(SsoConstants.JSON_EXPIRES_IN, ssoSession.getValidTo().toString());
    
            Map<String, Object> ovirt = new HashMap<>();
            ovirt.put("version", SsoConstants.OVIRT_SSO_VERSION);
            ovirt.put("principal_id", ssoSession.getPrincipalRecord().<String>get(Authz.PrincipalRecord.ID));
            ovirt.put("email", ssoSession.getPrincipalRecord().<String>get(Authz.PrincipalRecord.EMAIL));
            ovirt.put("namespace", ssoSession.getPrincipalRecord().<String>get(Authz.PrincipalRecord.NAMESPACE));
            ovirt.put("first_name", ssoSession.getPrincipalRecord().<String>get(Authz.PrincipalRecord.FIRST_NAME));
            ovirt.put("last_name", ssoSession.getPrincipalRecord().<String>get(Authz.PrincipalRecord.LAST_NAME));
            ovirt.put("group_ids", ssoSession.getPrincipalRecord().<Collection>get(Authz.PrincipalRecord.GROUPS,
                Collections.<ExtMap>emptyList()));
            if (password != null) {
                ovirt.put("password", password);
            }
            ovirt.put("capability_credentials_change",
                    ssoContext.getSsoProfilesSupportingPasswdChange().contains(ssoSession.getProfile()));
            payload.put("ovirt", ovirt);
            return payload;
    }
    
    • 创建管理门户的用户 Session。
    ActionReturnValue queryRetVal = FiltersHelper.getBackend(ctx).runAction(ActionType.CreateUserSession,
                            new CreateUserSessionParameters(
                                    (String) jsonResponse.get(SessionConstants.SSO_TOKEN_KEY),
                                    (String) jsonResponse.get(SessionConstants.SSO_SCOPE_KEY),
                                    appScope,
                                    profile,
                                    username,
                                    (String) payload.get("principal_id"),
                                    (String) payload.get("email"),
                                    (String) payload.get("first_name"),
                                    (String) payload.get("last_name"),
                                    (String) payload.get("namespace"),
                                    request.getRemoteAddr(),
                                    (Collection<ExtMap>) payload.get("group_ids"),
                                    loginAsAdmin));
    
    httpSession.setAttribute(SessionConstants.HTTP_SESSION_ENGINE_SESSION_ID_KEY,
    queryRetVal.getActionReturnValue());
    httpSession.setAttribute(FiltersHelper.Constants.REQUEST_LOGIN_FILTER_AUTHENTICATION_DONE, true);
    log.debug("Redirecting to '{}'", appUrl);
    response.sendRedirect(appUrl);
    
    • 经过 SsoLoginFilter 过滤器。SSO 验证通过,直接继续执行请求,交由 WebAdminHostPageServlet 处理,完成管理门户的登录操作。
    <servlet-mapping>
         <servlet-name>WebAdminHostPageServlet</servlet-name>
         <url-pattern>/WebAdmin.html</url-pattern>
    </servlet-mapping>
    

    2.2 用户先登录 SSO 认证中心

    用户先登录 SSO 认证中心流程

    2.2.1 用户首先登录 SSO 认证中心

    <servlet>
            <servlet-name>LoginServlet</servlet-name>
            <servlet-class>org.ovirt.engine.core.LoginServlet</servlet-class>
    </servlet>
    <servlet-mapping>
            <servlet-name>LoginServlet</servlet-name>
            <url-pattern>/login</url-pattern>
    </servlet-mapping>
    
    response.sendRedirect(
                        new URLBuilder(FiltersHelper.getEngineSsoUrl(request),
                                WelcomeUtils.OAUTH_AUTHORIZE_URI)
                        .addParameter(WelcomeUtils.HTTP_PARAM_CLIENT_ID,
                                EngineLocalConfig.getInstance().getProperty(WelcomeUtils.ENGINE_SSO_CLIENT_ID))
                        .addParameter(WelcomeUtils.HTTP_PARAM_RESPONSE_TYPE, WelcomeUtils.CODE)
                        .addParameter(WelcomeUtils.HTTP_PARAM_ENGINE_URL, FiltersHelper.getEngineUrl(request))
                        .addParameter(WelcomeUtils.HTTP_PARAM_REDIRECT_URI, WelcomeUtils.getOauth2CallbackUrl(request))
                        .addParameter(WelcomeUtils.HTTP_PARAM_SCOPE, request.getParameter(WelcomeUtils.SCOPE))
                        .addParameter(WelcomeUtils.HTTP_PARAM_LOCALE, request.getAttribute(WelcomeUtils.LOCALE).toString())
                        .addParameter(WelcomeUtils.HTTP_PARAM_SOURCE_ADDR, request.getRemoteAddr())
                        .build());
    
    public static String getRedirectUrl(HttpServletRequest request) throws Exception {
            String uri = getSsoSession(request, true).getRedirectUri();
            return StringUtils.isEmpty(uri) ?
                    new URLBuilder(getSsoContext(request).getEngineUrl(), "/oauth2-callback").build()  : uri;
    }
    
    • 请求被 welcome 项目工程中的 OAuthCallbackServlet 拦截。
     <servlet>
            <servlet-name>OAuthCallbackServlet</servlet-name>
            <servlet-class>org.ovirt.engine.core.OAuthCallbackServlet</servlet-class>
    </servlet>
    <servlet-mapping>
            <servlet-name>OAuthCallbackServlet</servlet-name>
            <url-pattern>/oauth2-callback</url-pattern>
    </servlet-mapping>
    
    • OAuthCallbackServlet 过程中通过访问 /sso/oauth/token 生成(模拟请求)生成授权码。通过访问 /sso/oauth/token-info 生成授权信息。最后转向到首页。
    String engineUri = EngineLocalConfig.getInstance().getProperty(WelcomeUtils.ENGINE_URI) + "/";
    ......
    response.sendRedirect(engineUri);
    

    2.2.2 再选择管理门户

    • 跳转过程与 2.1.1 处理一致,直到 OAuthAuthorizeServlet 执行。认证通过,直接转向 InteractiveRedirectToModuleServlet 处理。
    if (SsoUtils.isUserAuthenticated(request)) {
                log.debug("User is authenticated redirecting to interactive-redirect-to-module");
                redirectUrl = request.getContextPath() + SsoConstants.INTERACTIVE_REDIRECT_TO_MODULE_URI;
    
    • 后续过程与 2.1.2 处理一致,最终登录业务系统。

    3. 总结整理

    用户认证流程
    • 通过 TokenauthCode 请求用户信息。
      • 登录 SSO 认证中心,发放令牌(Token),建立令牌与 SsoSession 的映射(Token 和 authCode 也是一一对应)。
      • 业务系统(engine)的 Session 创建之前,会先根据业务系统获取的遍历 authCode 查找到令牌,再根据令牌,查询到用户信息(这个过程采用了模拟请求方式),获取用户信息后再生成业务系统 Session。
    • 通过 Client IDClient Secret 确保业务系统(engine)与 SSO 认证中心的对接安全。
      • SSO 能够对接的全部业务系统 Client 等信息在 SSO 服务初始化时设置到 SsoContext 上下文中。
      • 业务系统想通过 authCode 查询令牌,必须提供 Client ID 和 Client Secret 与 SsoContext 中进行比对,比对成功才能通过获取 Token ,并且通过 Token 获取 SsoSession 中的用户信息。
    • 在 2.2 的流程中,首先进行 SSO 认证中心登录,然后选择管理门户后能够直接找到 SSO 认证中心对应的 SsoSession。这是因为在 enginesso 的 web.xml 文件中配置了全局的 SsoSessionListener 监听器。在持监听器中对会话 Session 与 SsoSession 建立了一一对应关系。
    <listener>
        <listener-class>org.ovirt.engine.core.sso.context.SsoSessionListener</listener-class>
    </listener>
    
    @Override
    public void sessionCreated(HttpSessionEvent se) {
        se.getSession().setAttribute(SsoConstants.OVIRT_SSO_SESSION, new SsoSession(se.getSession()));
    }
    
    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        TokenCleanupUtility.cleanupExpiredTokens(se.getSession().getServletContext());
    }
    

    相关文章

      网友评论

      • 在右边:请问 你有没有配置虚拟机门户单点登录的具体步骤呀?也是刚接触ovirt,按照他的文档没有做出来。麻烦了。

      本文标题:【Ovirt 笔记】单点登录分析与整理

      本文链接:https://www.haomeiwen.com/subject/ygtpsftx.html