zoukankan      html  css  js  c++  java
  • Tomcat 容器的安全认证和鉴权

    大量的 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 认证和鉴权类图

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

    • 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 管道时,就首先进行认证和鉴权,方法调用如下:

    认证和鉴权序列图

    整个过程的核心代码就在 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 对象中

    在单机环境下,没有问题,在集群环境下,Tomcat 支持 Session 的复制,那单点登录相关的信息也会同步复制吗?后续会继续分析 Tomcat 集群的原理和实现。

    5. 小结

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

    至此,除了集群的实现,Tomcat 的核心原理已经分析完毕,接下来将会模拟实现一个简单的 Tomcat,欢迎关注。

  • 相关阅读:
    ZOJ 1002 Fire Net
    Uva 12889 One-Two-Three
    URAL 1881 Long problem statement
    URAL 1880 Psych Up's Eigenvalues
    URAL 1877 Bicycle Codes
    URAL 1876 Centipede's Morning
    URAL 1873. GOV Chronicles
    Uva 839 Not so Mobile
    Uva 679 Dropping Balls
    An ac a day,keep wa away
  • 原文地址:https://www.cnblogs.com/chuonye/p/10877757.html
Copyright © 2011-2022 走看看