tomcat中AuthenticatorBase简单的安全认证

likesyour 2020-06-07

有些web应用程序的内容是有限制的,只允许有权限的用户在提供正确的用户名和密码的情况下才允许访问。Servlet通过配置部署文件web.xml来对安全性提供技术支持。

    一个servlet通过一个叫authenticator的阀门(valve)来支持安全性限制。当容器启动的时候,authenticator被添加到容器的流水线上(一整条流水线:StandardEngineValve->StandardHost->ErroReportValve->StandardContextValve->AuthenciatorValve->StandWrapperValve)。

    authenticator阀门会在包装器阀门之前被调用。authenticator用于对用户进行验证,如果用户输入了正确的用户名和密码,authenticator阀门调用下一个用于处理请求servlet的阀门。如果验证失败,authenticator不唤醒下一个阀门直接返回。由于验证失败,用户并不能看到请求的servlet。

    在用户验证的时候authenticator阀门调用的是上下文域(realm)内的authenticate方法,将用户名和密码传递给它。该容器域可以访问合法的用户名密码。

域 Realm

    域是用于进行用户验证的一个组件,它可以告诉你一个用户名密码对是否是合法的。一个域跟一个上下文容器相联系,一个容器可以只有一个域。可以使用容器的setRealm方法来建立它们之间的联系。

    一个域是如何验证一个用户的合法性的?一个域拥有所有的合法用户的密码或者是可以访问它们。至于它们存放在哪里则取决于域的实现。在Tomcat的默认实现里,合法用户被存储在tomcat-users.xml文件里。但是可以使用域的其它实现来访问其它的源,如关系数据库。

在Catalina中,一个域用接口org.apache.catalina.Realm表示。该接口最重要的方法是四个authenticate方法:

第一个方法是最常用的方法,Realm接口还有一个getRole方法,签名如下:

    public boolean hasRole(Principal principal, String role);

另外,域还有getContainer和setContainer方法用于建立域与容器的联系。

    一个域的基本上实现是抽象类org.apache.catalina.realm.RealmBase。org.apache.catalina.realm包中还提供了其它一些类继承了RealmBase如:JDBCRealm, JNDIRealm, MemoryRealm,和 UserDatabaseRealm。默认情况下使用的域是MemoryRealm。

GenericPrincipal

    一个principal使用java.security.Principal接口来表示,Tomcat中该接口的实现为org.apache.catalina.realm.GenericPrincipal接口。一个GenericPrincipal必须跟一个域相关联,这个是通过构造函数实现的:

    GenericPrincipal必须拥有一个用户名和一个密码,此外还可选择性的传递一列角色。可以使用hasRole方法来检查一个principal是否有一个特定的角色,传递的参数为角色的字符串表示形式。这里是Tomcat4中的hasRole方法:

LoginConfig类

        一个login configuration包括一个域名,用org.apache.catalina.deploy.LoginConfig类表示。LoginConfig的实例封装了域名和验证要用的方法。可以使用LoginConfig实例的getRealmName方法来获得域名,可以使用getAuthName方法来验证用户。一个验证(authentication)的名字必须是下面的之一:BASIC, DIGEST, FORM, o或者CLIENT-CERT。如果用到的是基于表单(form)的验证,该LoginConfig对象还包括登录或者错误页面像对应的URL。

Tomcat一个部署启动的时候,先读取web.xml。如果web.xml包括一个login-confgi元素,Tomcat创建一LoginConfig对象并相应的设置它的属性。验证阀门调用LoginConfig的getRealmName方法并将域名发送给浏览器显示登录表单。如果getRealmName名字返回值为null,则发送给浏览器服务器的名字和端口名

Authenticator类

        org.apache.catalina.Authenticator接口用来表示一个验证器。该方接口并没有方法,只是一个组件的标志器,这样就能使用instanceof来检查一个组件是否为验证器。

        Catalina提供了Authenticator接口的基本实现:org.apache.catalina.authenticator.AuthenticatorBase类。除了实现Authenticator接口外,AuthenticatorBase还继承了org.apache.catalina.valves.ValveBase类。这就是说AuthenticatorBase也是一个阀门。可以在org.apache.catalina.authenticator包中找到该接口的几个类:BasicAuthenticator用于基本验证, FormAuthenticator用于基于表单的验证, DigestAuthentication用于摘要(digest)验证, SSLAuthenticator用于SSL验证。NonLoginAuthenticator用于Tomcat没有指定验证元素的时候。NonLoginAuthenticator类表示只是检查安全限制的验证器,但是不进行用户验证。

        org.apache.catalina.authenticator包中类的UML结构图

    tomcat中AuthenticatorBase简单的安全认证

一个验证器的主要工作是验证用户。因此,AuthenticatorBase类的invoke方法调用了抽象方法authenticate,该方法的具体实现由子类完成。在BasicAuthenticator中,它authenticate使用基本验证器来验证用户。

大量的 Web 应用都有安全相关的需求,正因如此,Servlet 规范建议容器要有满足这些需求的机制和基础设施,所以容器要对以下安全特性予以支持:

  • 身份验证:验证授权用户的用户名和密码
  • 资源访问控制:限制某些资源只允许部分用户访问
  • 数据完整性:能够证明数据在传输过程中未被第三方修改
  • 机密性或数据隐私:传输加密(SSL),确保信息只能被信任用户访问

本文就以上问题,对 Tomcat 容器提供的认证和鉴权的设计与实现,以及内部单点登录的原理进行分析。首发于微信公众号顿悟源码.

1. 授权

容器和 Web 应用采用的是基于角色的权限访问控制方式,其中容器需要实现认证和鉴权的功能,而 Web 应用则要实现授权的功能。

在 Servlet 规范中描述了两种授权方式:声明式安全和编程式安全。声明式安全就是在部署描述符中声明角色、资源访问权限和认证方式。以下代码片段摘自 Tomcat 自带的 Manager 应用的 web.xml:

<security-constraint> <!-- 安全约束 -->
  <web-resource-collection> <!-- 限制访问的资源集合 -->
    <web-resource-name>HTML Manager commands</web-resource-name>
    <url-pattern>/html/*</url-pattern>
  </web-resource-collection>
  <auth-constraint><!-- 授权可访问此资源集合的角色 -->
     <role-name>manager-gui</role-name>
  </auth-constraint>
</security-constraint>

<login-config><!-- 配置验证方法 -->
  <auth-method>BASIC</auth-method>
  <realm-name>Tomcat Manager Application</realm-name>
</login-config>

<security-role><!-- 定义一个安全角色 -->
  <description>
    The role that is required to access the HTML Manager pages
  </description>
  <role-name>manager-gui</role-name>
</security-role>

这些安全相关的配置,都会在应用部署时,初始化和设置到 StandardContext 对象中。更多详细的内容可查看规范对部署描述文件的解释,接下来看 Tomcat 怎么设计和实现认证及鉴权。

2. 认证和鉴权的设计

Servlet 规范虽然描述了 Web 应用声明安全约束的机制,但没有定义容器与关联用户和角色信息之间的接口。因此,Tomcat 定义了一个 Realm 接口,用于适配身份验证的各种信息源。整体设计的类图如下:

tomcat中AuthenticatorBase简单的安全认证

上图中,包含了各个类的核心方法,关键类或接口的作用如下:

  • Realm - 译为,域有泛指某种范围的意思,在这个范围内存储着用户名、密码、角色和权限,并且提供身份和权限验证的功能,典型的这个范围可以是某个配置文件或数据库
  • CombinedRealm - 内部包含一个或多个 Realm,按配置顺序执行身份验证,任一 Realm 验证成功,则表示成功验证
  • LockOutRealm - 提供用户锁定机制,防止在一定时间段有过多身份验证失败的尝试
  • Authenticator - 不同身份验证方法的接口,主要有 BASIC、DIGEST、FORM、SSL 这几种标准实现
  • Principal - 对认证主体的抽象,它包含用户身份和权限信息
  • SingleSignOn - 用于支持容器内多应用的单点登录功能

2.1 初始化

Realm 是容器的一个可嵌套组件,可以嵌套在 Engine、Host 和 Context 中,并且子容器可以覆盖父容器配置的 Realm。默认的 server.xml 在 Engine 中配置了一个 LockOutRealm 组合域,内部包含一个 UserDatabaseRealm,它从配置的全局资源 conf/tomcat-users.xml 中提取用户信息。

web.xml 中声明的安全约束会初始化成对应的 SecurityConstraint、SecurityCollection 和 LoginConfig 对象,并关联到一个 StandardContext 对象。

在上图可以看到,AuthenticatorBase 还实现了 Valve 接口,StandardContext 对象在配置的过程中,如果发现声明了标准的验证方法,那么就会把它加入到自己的 Pipeline 中。

3. 一次请求认证和鉴权过程

Context 在 Tomcat 内部就代表着一个 Web 应用,假设配置使用 BASIC 验证方法,那么 Context 内部的 Pipeline 就有 BasicAuthenticator 和 StandardContextValve 两个阀门,当请求进入 Context 管道时,就首先进行认证和鉴权,方法调用如下:

tomcat中AuthenticatorBase简单的安全认证

整个过程的核心代码就在 AuthenticatorBase 的 invoke 方法中:

public void invoke(Request request, Response response) throws IOException, ServletException {
  LoginConfig config = this.context.getLoginConfig();
  // 0. Session 对象中是否缓存着一个已经进行身份验证的 Principal
  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);
        }
      }
    }
  }
  // 对于基于表单登录,可能位于安全域之外的特殊情况进行处理
  String contextPath = this.context.getPath();
  String requestURI = request.getDecodedRequestURI();
  if (requestURI.startsWith(contextPath) && requestURI.endsWith(Constants.FORM_ACTION)) {
          return;
      }
  }
  // 获取安全域对象,默认配置是 LockOutRealm
  Realm realm = this.context.getRealm();
  // 根据请求 URI 尝试获取配置的安全约束
  SecurityConstraint [] constraints = realm.findSecurityConstraints(request, this.context);
 
  if ((constraints == null) /* && (!Constants.FORM_METHOD.equals(config.getAuthMethod())) */ ) {
    // 为 null 表示访问的资源没有安全约束,直接访问下一个阀门
    getNext().invoke(request, response);
    return;
  }
  // 确保受约束的资源不会被 Web 代理或浏览器缓存,因为缓存可能会造成安全漏洞
  if (disableProxyCaching && 
      !"POST".equalsIgnoreCase(request.getMethod())) {
      if (securePagesWithPragma) {
          response.setHeader("Pragma", "No-cache");
          response.setHeader("Cache-Control", "no-cache");
      } else {
          response.setHeader("Cache-Control", "private");
      }
      response.setHeader("Expires", DATE_ONE);
  }
  int i;
  // 1. 检查用户数据的传输安全约束
  if (!realm.hasUserDataPermission(request, response, constraints)) {
    // 验证失败
    // Authenticator已经设置了适当的HTTP状态代码,因此我们不必做任何特殊的事情
    return;
  }
  // 2. 检查是否包含授权约束,也就是角色验证
  boolean authRequired = true;
  for(i=0; i < constraints.length && authRequired; i++) {
    if(!constraints[i].getAuthConstraint()) {
      authRequired = false;
    } else if(!constraints[i].getAllRoles()) {
      String [] roles = constraints[i].findAuthRoles();
      if(roles == null || roles.length == 0) {
        authRequired = false;
      }
    }
  }
  // 3. 验证用户名和密码
  if(authRequired) {
    // authenticate 是一个抽象方法,由不同的验证方法实现
    if (!authenticate(request, response, config)) {
      return;
    } 
  }
  // 4. 验证用户是否包含授权的角色
  if (!realm.hasResourcePermission(request, response,constraints,this.context)) {
    return;
  }
  // 5. 已满足任何和所有指定的约束
  getNext().invoke(request, response);
}

另外,AuthenticatorBase 还有一个比较重要的 register() 方法,它会把认证后生成的 Principal 对象设置到当前 Session 中,如果配置了SingleSignOn 单点登录的阀门,同时把用户身份、权限信息关联到 SSO 中。

4. 单点登录

Tomcat 支持通过一次验证就能访问部署在同一个虚拟主机上的所有 Web 应用,可通过以下配置实现:

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

Tomcat 的单点登录是利用 Cookie 实现的:

  • 当任一 Web 应用身份验证成功后,都会把用户身份信息缓存到 SSO 中,并生成一个名为 JSESSIONIDSSO 的 Cookie
  • 当用户再次访问这个主机时,会通过 Cookie 拿出存储的用户 token,获取用户 Principal 并关联到 Request 对象中

5. 小结

本文介绍的是 Tomcat 内部实现的登录认证和权限,而应用程序通常都是通过 Filter 或者自定义的拦截器(如 Spring 的 Interceptor)实现登录,或者使用第三方安全框架,比如 Shiro,但是原理都差不多。


原文链接:https://blog.csdn.net/u012233580/article/details/79316212

https://www.cnblogs.com/chuonye/p/10877757.html

相关推荐

Mr丶Yang / 0评论 2016-11-04