编程语言
首页 > 编程语言> > Tomcat单点登录配置及源码分析

Tomcat单点登录配置及源码分析

作者:互联网

我们上网的时候,一定遇到过类似这样的情况,例如使用网易邮箱时进行了登录操作,之后再访问网易的博客系统时,发现自动以之前的ID登录了。这种实现在计算机中称为SSO(Single Sign On),即我们常说的单点登录。这种在关联网站间共享认证信息,避免需要在多个系统中重复输入帐户信息的行为,是SSO要解决的。

对于许多应用,可能会独立部署等情况,所以常会采用cas的形式,来实现SSO。

我们今天要了解的,是作为在同一个Tomcat中部署的应用之间,如何实现SSO,避免重复登录。


预备:

首先,有几点预备知识需要先了解一下。

  1. 在Tomcat架构设计中,不同的Container中包含了Peipline。各个Pipeline中可以添加多种不同形式的Valve。例如我们之前提到的AccessLogValveTomcat的AccessLogValve介绍

  2. Tomcat中session的实现,最常用的是Cookie Session, 通过将名为JSESSIONID的cookie写回浏览器,实现session。我们在前面的文章里也描述过。深入Tomcat源码分析Session

  3. 关于认证的一些内容,可以参考介绍过的Basic认证。你可能不了解的Basic认证

环境:

有了这些准备之后,我们开始进行环境的搭建和实验。

以Tomcat自带的几个应用为例,我们启动Tomcat后,访问这两个应用:docsexamples 我们看到,默认是不需要登录的,都可以直接访问。

此时,在docs应用的web.xml中增加如下配置:

    <security-constraint>    <display-name>Security Constraint</display-name>    <web-resource-collection>    <web-resource-name>Protected Area</web-resource-name>    <url-pattern>/*</url-pattern>    </web-resource-collection>    <auth-constraint>    <role-name>tomcat</role-name>    </auth-constraint>    </security-constraint>    <login-config>    <auth-method>BASIC</auth-method>    <realm-name>SSO Test</realm-name>    </login-config>    <security-role>    <role-name>tomcat</role-name>    </security-role>

此时重启Tomcat,再次请求docs应用,发现需要验证了。

同样,再修改examples应用的web.xml,限制对于其直接访问,在文件中增加如下内容: <url-pattern>/*</url-pattern>。只需要增加这个就可以了,下面是修改内容对应的位置参考。

<web-resource-collection>         <web-resource-name>Protected Area - Allow methods</web-resource-name>         <url-pattern>/jsp/security/protected/*</url-pattern>             <url-pattern>/*</url-pattern>         <http-method>DELETE</http-method>         <http-method>GET</http-method>         <http-method>POST</http-method>         <http-method>PUT</http-method>      </web-resource-collection>

修改之后,examples也需要登录才能访问了。由于同样的认证,我们对两个应用的访问需要重复输入用户名、密码进行认证,此时,SSO的配置就显出了必要性了。

在Tomcat的server.xml中,默认的Host,localhost中,增加以下Valve:

<Valve className="org.apache.catalina.authenticator.SingleSignOn"/>

再次重启Tomcat,这个时候SSO已经生效了,你再重新访问上面两个应用时,只需要对其中一个进行认证即可,是不是很容易?

原理:

在前面分析请求流程的几篇文章中,我们介绍过从CoyoteAdapter进行service处理,再到达各个Pipeline、Valve。(Facade模式与请求处理)

而这些Valve中,对于SSO的Valve SingleSignOn是在认证的ValveAuthenticatorBase之前执行。

在SingleSignOn中,会先进行userPrincipal的判断,不为空就会直接向后执行,为空时,判断请求中是否包含SSO Cookie。

    if (request.getUserPrincipal() != null) {            getNext().invoke(request, response);            return;        }         // Check for the single sign on cookie        Cookie cookie = null;        Cookie cookies[] = request.getCookies();        if (cookies != null) {            for (int i = 0; i < cookies.length; i++) {                if (Constants.SINGLE_SIGN_ON_COOKIE.equals(cookies[i].getName())) {                    cookie = cookies[i];                    break;                }            }        }        if (cookie == null) {            getNext().invoke(request, response);            return;        }

对于第一个就进认证的应用,走的流程基本和配置之前一样,区别就在于SSO配置后,会把认证的信息,添加到Cookie中。并将其存储并和一个ssoId进行关联。

BasicAuthenticator:

对于docs应用,使用的是Basic认证方式

 principal = context.getRealm().authenticate(username, password);     if (principal != null) {       register(request, response, principal,        HttpServletRequest.BASIC_AUTH, username, password);                return (true);        } }

FormAuthenticator

对于examples应用,使用的是Form的认证方式,如果是Form认证的应用不是第一个请求,则在请求到达时,已经进行过认证,后面的请求会直接获取session并关联到ssoId上。 如果是初次请求即访问Form认证的应用,SsoId还没值,流程基本和Basic一样,不同的是从表单中提取用户名和密码信息,再进行register

Principal principal = request.getUserPrincipal();   String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE);   if (principal != null) {    // Associate the session with any existing SSO session     if (ssoId != null) {      associate(ssoId, request.getSessionInternal(true)); // 注意这里,把新获取到的sessionId关联到ssoId中  }  return true; }
这里register会把认证的信息添加, 在ssoId为空时,进行Cookie的创建,
        String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE);        if (ssoId == null) {            ssoId = sessionIdGenerator.generateSessionId();            Cookie cookie = new Cookie(Constants.SINGLE_SIGN_ON_COOKIE, ssoId);            cookie.setMaxAge(-1);            cookie.setPath("/");             // Bugzilla 41217            cookie.setSecure(request.isSecure());             // Bugzilla 34724            String ssoDomain = sso.getCookieDomain();            if(ssoDomain != null) {                cookie.setDomain(ssoDomain);            }             // Configure httpOnly on SSO cookie using same rules as session cookies            if (request.getServletContext().getSessionCookieConfig().isHttpOnly() ||                    request.getContext().getUseHttpOnly()) {                cookie.setHttpOnly(true);            }             response.addCookie(cookie);             // Register this principal with our SSO valve            sso.register(ssoId, principal, authType, username, password);            request.setNote(Constants.REQ_SSOID_NOTE, ssoId);

Cookie不为空时,进行ssoId和session的关联

protected boolean associate(String ssoId, Session session) {        SingleSignOnEntry sso = cache.get(ssoId);        if (sso == null) {            if (containerLog.isDebugEnabled()) {                containerLog.debug(sm.getString("singleSignOn.debug.associateFail",                        ssoId, session));            }            return false;        } else {            }            sso.addSession(this, ssoId, session);            return true;        }    }


我们注意到这行代码sso.addSession(this, ssoId, session) 这里会给session添加一个listener,这个listener会在session过期销毁时,把sso的session也移除掉

应用的SSO

在Pipeline中从SingleSignOn这个Valve开始,一直调用到AuthenticatorBase,再到达其实现类. SingleSignOn这个Valve处理请求时,判断entry是否为空,此时由于前面的应用已经存储过该信息,所以这里不为空,就会据此设置request中的authType和principal

  SingleSignOnEntry entry = cache.get(cookie.getValue());    if (entry != null) {   request.setNote(Constants.REQ_SSOID_NOTE, cookie.getValue());   // Only set security elements if reauthentication is not required    if (!getRequireReauthentication()) {         request.setAuthType(entry.getAuthType());     request.setUserPrincipal(entry.getPrincipal());            }

而后面的Valve中,认证时首先会判断principal是否为空。由于前置的sso已经把这些信息填充过了,所以这里就会走这样的逻辑:

    public void invoke(Request request, Response response)        throws IOException, ServletException {        // Have we got a cached authenticated Principal to record?        if (cache) {            Principal principal = request.getUserPrincipal(); // 这里不为空            if (principal == null) {                Session session = request.getSessionInternal(false);                if (session != null) {                    principal = session.getPrincipal();                    if (principal != null) {                        request.setAuthType(session.getAuthType());                        request.setUserPrincipal(principal);                    }                }            }        }

总结一下:

单点登录的实现,是在第一次进行认证的时候,将认证信息进行存储。后续相同域的请求到达时,会先判断是否存储了单点登录的认证信息,如果已经存储过,就将其添加到新到达的request中,以此进行后续的认证,从而实现SSO.


标签:单点,Tomcat,request,ssoId,SSO,session,认证,源码,principal
来源: https://blog.51cto.com/u_15127648/2776652