加入收藏 | 设为首页 | 会员中心 | 我要投稿 济源站长网 (https://www.0391zz.cn/)- 数据工具、数据仓库、行业智能、CDN、运营!
当前位置: 首页 > 站长资讯 > 外闻 > 正文

Tomcat 容器的安全认证和鉴权

发布时间:2019-05-21 10:06:25 所属栏目:外闻 来源:顿悟源码
导读:大量的 Web 应用都有安全相关的需求,正因如此,Servlet 规范建议容器要有满足这些需求的机制和基础设施,所以容器要对以下安全特性予以支持: 身份验证:验证授权用户的用户名和密码 资源访问控制:限制某些资源只允许部分用户访问 数据完整性:能够证明
副标题[/!--empirenews.page--]

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

Tomcat 容器的安全认证和鉴权

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

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

1. 授权

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

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

  1. <security-constraint> <!-- 安全约束 --> 
  2.   <web-resource-collection> <!-- 限制访问的资源集合 --> 
  3.     <web-resource-name>HTML Manager commands</web-resource-name> 
  4.     <url-pattern>/html/*</url-pattern> 
  5.   </web-resource-collection> 
  6.   <auth-constraint><!-- 授权可访问此资源集合的角色 --> 
  7.      <role-name>manager-gui</role-name> 
  8.   </auth-constraint> 
  9. </security-constraint> 
  10.  
  11. <login-config><!-- 配置验证方法 --> 
  12.   <auth-method>BASIC</auth-method> 
  13.   <realm-name>Tomcat Manager Application</realm-name> 
  14. </login-config> 
  15.  
  16. <security-role><!-- 定义一个安全角色 --> 
  17.   <description> 
  18.     The role that is required to access the HTML Manager pages 
  19.   </description> 
  20.   <role-name>manager-gui</role-name> 
  21. </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 管道时,就首先进行认证和鉴权,方法调用如下:

Tomcat 容器的安全认证和鉴权

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

  1. public void invoke(Request request, Response response) throws IOException, ServletException { 
  2.   LoginConfig config = this.context.getLoginConfig(); 
  3.   // 0. Session 对象中是否缓存着一个已经进行身份验证的 Principal 
  4.   if (cache) { 
  5.     Principal principal = request.getUserPrincipal(); 
  6.     if (principal == null) { 
  7.       Session session = request.getSessionInternal(false); 
  8.       if (session != null) { 
  9.         principal = session.getPrincipal(); 
  10.         if (principal != null) { 
  11.           request.setAuthType(session.getAuthType()); 
  12.           request.setUserPrincipal(principal); 
  13.         } 
  14.       } 
  15.     } 
  16.   } 
  17.   // 对于基于表单登录,可能位于安全域之外的特殊情况进行处理 
  18.   String contextPath = this.context.getPath(); 
  19.   String requestURI = request.getDecodedRequestURI(); 
  20.   if (requestURI.startsWith(contextPath) && requestURI.endsWith(Constants.FORM_ACTION)) { 
  21.           return; 
  22.       } 
  23.   } 
  24.   // 获取安全域对象,默认配置是 LockOutRealm 
  25.   Realm realm = this.context.getRealm(); 
  26.   // 根据请求 URI 尝试获取配置的安全约束 
  27.   SecurityConstraint [] constraints = realm.findSecurityConstraints(request, this.context); 
  28.   
  29.   if ((constraints == null) /* && (!Constants.FORM_METHOD.equals(config.getAuthMethod())) */ ) { 
  30.     // 为 null 表示访问的资源没有安全约束,直接访问下一个阀门 
  31.     getNext().invoke(request, response); 
  32.     return; 
  33.   } 
  34.   // 确保受约束的资源不会被 Web 代理或浏览器缓存,因为缓存可能会造成安全漏洞 
  35.   if (disableProxyCaching &&  
  36.       !"POST".equalsIgnoreCase(request.getMethod())) { 
  37.       if (securePagesWithPragma) { 
  38.           response.setHeader("Pragma", "No-cache"); 
  39.           response.setHeader("Cache-Control", "no-cache"); 
  40.       } else { 
  41.           response.setHeader("Cache-Control", "private"); 
  42.       } 
  43.       response.setHeader("Expires", DATE_ONE); 
  44.   } 
  45.   int i; 
  46.   // 1. 检查用户数据的传输安全约束 
  47.   if (!realm.hasUserDataPermission(request, response, constraints)) { 
  48.     // 验证失败 
  49.     // Authenticator已经设置了适当的HTTP状态代码,因此我们不必做任何特殊的事情 
  50.     return; 
  51.   } 
  52.   // 2. 检查是否包含授权约束,也就是角色验证 
  53.   boolean authRequired = true; 
  54.   for(i=0; i < constraints.length && authRequired; i++) { 
  55.     if(!constraints[i].getAuthConstraint()) { 
  56.       authRequired = false; 
  57.     } else if(!constraints[i].getAllRoles()) { 
  58.       String [] roles = constraints[i].findAuthRoles(); 
  59.       if(roles == null || roles.length == 0) { 
  60.         authRequired = false; 
  61.       } 
  62.     } 
  63.   } 
  64.   // 3. 验证用户名和密码 
  65.   if(authRequired) { 
  66.     // authenticate 是一个抽象方法,由不同的验证方法实现 
  67.     if (!authenticate(request, response, config)) { 
  68.       return; 
  69.     }  
  70.   } 
  71.   // 4. 验证用户是否包含授权的角色 
  72.   if (!realm.hasResourcePermission(request, response,constraints,this.context)) { 
  73.     return; 
  74.   } 
  75.   // 5. 已满足任何和所有指定的约束 
  76.   getNext().invoke(request, response); 

(编辑:济源站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

热点阅读