65.9K
CodeProject 正在变化。 阅读更多。
Home

Tomcat 过滤器 LDAP 认证。

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2017年9月21日

CPOL

12分钟阅读

viewsIcon

18435

downloadIcon

466

本文旨在从头开始解释构建身份验证 Servlet 过滤器的过程,包括配置初始化、登录页面的 URL 路径过滤以及在独立的 LDAP Windows Active Domain 中动态获取用户进行身份验证的过程。

引言

本文旨在保护您的 Tomcat 或 JBoss 网页,通过 Active Domain Controller 授予访问权限,而无需配置 LDAP 的管理员凭据。

它基本上是关于为尚未准备好用户授权(或拥有可以禁用的遗留授权系统)的开源 Web 应用程序进行用户身份验证,这些应用程序将托管在 Apache Tomcat 或 JBoss Web 应用程序服务器上,同时还验证其他几个已有的应用程序,所有这些都针对 LDAP 服务。

背景

有大量信息描述了如何在 Linux 操作系统上配置 LDAP 身份验证,还可以找到关于配置 Apache 将用户映射到 LDAP 以及安装 OpenLDAP 服务器的优秀文档,但所有这些解决方案,到目前为止,都需要 LDAP 端的管理员权限,包括在 Web 服务器配置中,这并不总是可用或可取的,而且花费大量时间来理解和部署。

很难找到一个简单有效的指南来创建一些东西来为 Tomcat/JBoss 的 URL 路径添加安全性,同时还包括针对 LDAP 的验证。很明显,需要编写一个使用 Servlet API 的 AuthorizationFilter 映射,包括使用一些 javax.naming 类来构建一个 DirContext,通过 LdapCtxFactory.getLdapCtxInstance(),在没有异常的情况下,用户名/密码组合将允许经过身份验证的用户访问给定的 URL 路径。

在弄清楚解决方案并使代码正常运行后,决定分享编写授权过滤器的经验,并在其中添加一些逻辑,特别是针对 Active Domain 目录服务的验证,包括一个登录页面。希望本文能指导开发者使用 Java Servlet 编写过滤器映射,并为您提供开发和部署任务的捷径。

解决方案

代码被编写为一个 Servlet 过滤器,其中除了登录页面外,任何页面都被禁止访问,因为用户应该有一个公共入口点才能进行身份验证。显然,开发者可以配置任意数量的过滤器,甚至可以扩展到其他过滤器或显示身份验证过程错误的页面。

web.xml 中添加 filter-mapping 后,过滤器中的初始化代码也会读取 web.xml 中的参数,因此,拥有一个非常灵活的产品,我们可以利用给定的用户进行固定的系统验证(始终是系统中相同的用户)或定义动态授权,包括一个 Web 登录页面,因此用户可以根据在该登录 Web 页面中输入的凭据进行身份验证并获得访问权限。

使用代码

我们将从简短的介绍过滤器的作用开始,以及如何配置映射来命中正确的 Java 代码,因为这是我们项目的第一个步骤。

基本上,过滤器是在正常的 HTTP 周期中的请求-响应操作中进行的一次绕行,它可以指向某个预定义类或定制类中的代码,该代码在用户每次请求给定 URL 路径上的资源页面或定义过滤器的 URL 映射下的任何路径时执行。所以,Web 应用程序服务器不会简单地沿着路径向下走并返回页面,而是执行这个类,该类然后返回成功或取消结果。对于同一个路径有许多类型的过滤器,因此,您将它们一个接一个地链接起来,取决于您的 <url-pattern>/*</url-pattern> 定义,所以如果您的过滤器成功授予访问权限,您将简单地亮绿灯继续过滤器链,如果出现问题,则进程将被终止并返回 HTTP 错误。您可以在以下 页面 上找到关于 Apache Tomcat 过滤器的丰富信息。

现在我们已经清楚了这种绕行的概念,我们将继续介绍 web.xml 的配置以及如何指向正确的源代码来实现过滤器,然后实现相应的身份验证例程以返回过滤器的成功或拒绝结果。之后,我们将修改给定的代码以包含一个登录页面,允许用户动态进行身份验证,这正是我们最初的兴趣所在。

配置

这里的诀窍是使用一些辅助工具,避免手动添加库和从头开始配置(如 Eclipse 和 Maven - 尽管如果您是这样的专家级程序员,您可以手动添加所有内容,但这超出了本文的范围)。所以,我们将直接开始一个动态 Web 项目,然后将其转换为 Maven 项目,或者使用 maven-archetype-webapp 或您喜欢的任何其他原型从头开始创建一个 Maven 项目(后者可能在某些部分略有不同)。

在您的 Eclipse 中,您会看到一个与以下类似的结构,其中 src 包含授权过滤器 Java 代码的类,而 WebContent 托管您的页面和配置,这些页面和配置最终将部署在您的 webapp 文件夹下,在我们的项目中显示在 Deployed Resources 下。强烈建议您也添加一个 Apache Tomcat 服务器进行本地测试,以便您可以轻松调试并了解其工作原理。

web.xml 文件是魔法开始的地方,已经将 servlet-api jar 添加到您的 pom.xml 中,只需要配置即将命名为 AuthorizeFilter 的授权过滤器,由于 '/*' 的 url-pattern 定义,它将保护您的应用程序根文件夹和子文件夹,最后,filter-mapping 应该知道哪个类包含过滤器的实现,我们将在 org/web/servlets/filters/Authorize.java 文件中创建该代码。

<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
	<display-name>MyAuthorizeProject</display-name>

		<filter>
			<filter-name>AuthorizeFilter</filter-name>
			<filter-class>org.web.servlets.filters.Authorize</filter-class>
			<!-- init-param are going to be here -->
		</filter>

		<filter-mapping>
		<filter-name>AuthorizeFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
</web-app>

过滤器源代码

现在我们将直接进入已命名的 org/web/servlets/filters/Authorize.java 文件中的编码部分。首先,我们需要一个实现 javax.servlet.Filter 的类,这将迫使我们定义和实现以下方法:init()doFilter()destroy()。初始化方法将帮助我们从上面标记的 web.xml init 参数部分读取参数,因为 init() 方法在 Apache Tomcat 服务器启动时调用,因此在 Web 应用程序运行期间保留该配置。destroy() 方法显然是在初始化期间或 Web 应用程序生命周期期间释放任何请求或保留的资源时调用的。最后,doFilter() 方法在应用程序服务器命中 URL 路径上的查询时被调用,在本例中,是每次!这看起来很明显,但不要将我们的应用程序根文件夹与您的 Web 服务器的主 RootDirectory 混淆,它们是不同的事物,尽管您可以稍微处理一下(配置 httpd.conf 或简单地添加一个使用 http-equiv url 刷新到我们给定的 Web 应用程序根文件夹示例的 index 页面)。所以,这是 Java 源代码中过滤器示例的骨架。

package org.web.servlets.filters;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

public class Authorize implements Filter {

	boolean authenticationGranted = false;
	
	public void init(FilterConfig filterConfiguration) throws ServletException {
		
		if (filterConfiguration.getInitParameter("grantedAccess").toLowerCase().equals("true"))
			authenticationGranted = true;
		
		System.out.println("Authorize filter started having grant '" + 
            authenticationGranted ? "always" : "never" + "'.");
	}

	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, 
        FilterChain servletFilterChain) throws IOException, ServletException {
	
		System.out.println("Performing authorization...");
		HttpServletResponse response = (HttpServletResponse) servletResponse;
		HttpServletRequest request = (HttpServletRequest) servletRequest;
		HttpSession session = request.getSession();

		if (authenticationGranted) {
			System.out.println("Authorization granted.");
			servletFilterChain.doFilter(servletRequest, servletResponse);
		} else {
			System.out.println("Authorization denied!");
			response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
		}
	}

	public void destroy() {
		System.out.println("Authorize filter destroyed.");
	}
}

此 Java 代码假定您已在过滤器的相应配置初始化部分定义了一个 grantedAccess 参数,正如我们将在下一个 XML 代码中看到的。它被读取为一个字符串,不区分大小写,并转换为布尔值,以便始终授予或从不授予访问权限,到目前为止,我们只有一个概念,以后可以演变为动态身份验证。

<init-param>
	<param-name>grantedAccess</param-name>
	<param-value>true</param-value>
</init-param>

现在您可以开始玩代码了,只需更改 grantedAccess 参数的值,然后查看您的过滤器代码如何根据您的初始配置授予或拒绝访问。一旦代码正常运行,您就可以进入用户身份验证,但这不会太难,只是 LDAP 服务器和用户名/密码凭据的额外参数以及 LDAP 身份验证的正确例程。

LDAP 用户身份验证

现在我们已经知道如何配置、编写和运行 Java 过滤器,我们将添加真正的代码,以便使用标准的 Java 类与 Active Domain 进行身份验证,在这种情况下,我们将使用 com.sun.jndi.ldap.LdapCtxFactory.getLdapCtxInstance() 来检查用户名和密码组合与 LDAP 服务器的匹配程度,尽管这将来可能会改变,甚至演变为其他工厂类;这里重要的部分是哪里添加授权代码以及向过滤器链返回什么结果。以下代码将直接进入 Java 过滤器实现中的 doFilter() 方法,以便代码可以决定用户是否被 LDAP 识别,从而分别授予或拒绝对所请求资源的访问。开发人员务必在 Build Java Path 中的项目属性中双重检查 JDK 是否已设置,因为 com.sun.jndi.ldap.LdapCtxFactory 仅在 jdk 中可以找到。

String ldapContext = String.format("ldap://%s", ldapHostname);
		
Hashtable<String, String> ldapUserProperties = new Hashtable<String, String>();
ldapUserProperties.put(Context.SECURITY_PRINCIPAL, ldapUsername);
ldapUserProperties.put(Context.SECURITY_CREDENTIALS, ldapPassword);

try {
	DirContext directoryContext = LdapCtxFactory.getLdapCtxInstance(ldapContext, ldapUserProperties);
	authenticationGranted = true;
} catch (NamingException e) {
	System.out.println("Authentication failed!");
}

显然,这段代码需要 ldapServerldapUsernameldapPassword 变量作为 init-param 定义,并由过滤器 Java 代码中的 init() 方法读取,同时也在类中定义。还需要一些导入,但目前已省略,仅展示解决方案的重要部分。

此时,如果一切顺利,我们就拥有一个完整的过滤器项目示例,它从 web.xml 获取用户名和密码组合,并根据配置的用户授予或拒绝访问。到目前为止,我们拥有的与周围的所有示例几乎相同,但我们学会了如何从头开始编写过滤器 - 是吧?

无论如何,我们不仅让代码正常工作,我们还知道如何读取配置变量并初始化我们的类,因此我们将进一步教授我们的过滤器,如何决定是否授予对 登录页面 的访问权限,从该登录页面获取 用户名密码 组合,然后动态地对我们的 LDAP 服务进行身份验证... 之后,我们将如虎添翼!

添加登录页面

在本节中,我们将了解如何捕获用户正在导航到的 URL,因为我们需要允许未经授权的用户始终访问我们的登录页面,在该页面上我们将要求输入用户名和密码组合,并对 LDAP 进行身份验证,在此过程之后,我们将允许访问系统或仅返回授权错误;另请注意,我们应该记住用户最初导航的 URL,因为如果我们的用户碰巧有一个完整的路径要访问,我们将不得不重定向到登录页面,并在成功身份验证后(在我们的项目中也是授权),我们将不得不将用户重定向回原始 URL。

以下代码位于我们的 Authorize Java 类中,并决定是否重定向到登录页面;根据使用的方法(GET 或 POST),它将在 POST 时如果已授权则重定向回原始 URL。有几个额外的变量只是为了在我们的会话中拥有一个已验证的用户以及我们将使用的登录 HTML 页面。在登录页面内,我们应该包含一个表单,并且还必须注意表单内的参数名称,因为它们必须与我们从 POST 请求中提取的名称相同。我们将把最后一段代码重构为一个名为 performLdapAuthentication() 的方法,以便代码清晰。

// perform authentication only if user is not previously authorized
String authorizedUsername = (String) session.getAttribute("authorizedUsername");
if (authorizedUsername != null) {
	System.out.println("User '" + authorizedUsername + "' was previously authorized!");
	authenticationGranted = true;
} else {
	// loginUrlPath will be normally defined as "/login"
	if (request.getServletPath().equals(loginUrlPath)) {
		if (request.getMethod().equals("GET")) {
			// login URI is always allowed
			authenticationGranted = true;
		} else if (request.getMethod().equals("POST")) {
			String username = request.getParameter("_login_username");
			String password = request.getParameter("_login_password");

			// authorize user with credentials provided from login page
			if (performLdapAuthentication(username, password)) {
				session.setAttribute("authorizedUsername", ldapUsername);
				authenticationGranted = true;

				System.out.println("User '" + authorizedUsername + "' is now authorized!");
			} else {
				redirectUri = String.format("%s%s", request.getContextPath(), loginUrlPath);
				session.setAttribute("_login_authentication_error",
                                     "Authentication error, username or password invalid!");
			}
		}
	} else {
		// redirect to login URI if configured
		if (loginUrlPath != null) {
			redirectUri = String.format("%s%s", request.getContextPath(), loginUrlPath);
			// keep original URL in session to redirect after valid authentication
			session.setAttribute("_login_original_request_url", request.getRequestURL());
		} else {
			// authorize user with credentials provided from initial parameters
			if (performLdapAuthentication(ldapUsername, ldapPassword)) {
				authorizedUsername = ldapUsername;
				session.setAttribute("authorizedUsername", ldapUsername);
				authenticationGranted = true;

				System.out.println("User '" + authorizedUsername + "' is now authorized!");
			}
		}
	}
}

if (authenticationGranted) {
	servletFilterChain.doFilter(servletRequest, servletResponse);
} else {
	if (redirectUri == null) {
		response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
	} else {
		response.sendRedirect(redirectUri);
	}
}

为了这一切正常工作,我们还必须定义登录的 URL 资源,该资源将向用户显示一个 HTML 页面来输入用户名和密码,但也会决定是否重定向回原始 URL。这是在一个实现 HttpServlet 的新类中创建的,该类定义了两个方法:doGet()doPost(),分别用于 GET 和 POST 操作。因此,一旦用户访问登录 URL,Authorize 过滤器就允许访问登录资源,然后通过 doGet() 方法的 action 导航到我们的 login.html 页面,该登录页面将请求用户名和密码组合,并将 POST 回我们的 Servlet,该 Servlet 将再次命中我们的 Authorize Java 类,将在 LDAP 上执行身份验证,并在结果后 - 无论是授予还是拒绝 - Authorize 代码将允许过滤器继续其链或仅返回 HTTP 错误。在后一种情况下,我们将再次导航到登录 URL 并显示错误,在前一种情况下,链下的导航将我们带回 Login 类,但在 doPost() 方法中,它将重定向回保存的原始 URL。

这是实现 GET 和 POST 两个方法以及登录资源所需定义的 login.java 类,并为此使用注解。非常重要的是,您必须在 login-url-path 的配置中设置相同的路径,否则重定向到登录将不起作用,实际上 web.xml 中的参数不是用来定义登录路径的,而是用来激活它的,被设置为字符串而不是布尔值只是为了解释这一点。

package org.web.servlets;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@WebServlet("/login")
public class Login extends HttpServlet {

	private static final long serialVersionUID = 1L;

	@Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        request.getRequestDispatcher("/WEB-INF/login.jsp").forward(request, response);
    }
	
	@Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
        throws ServletException, IOException {
		HttpSession session = request.getSession();
		
		// restore original URL after successful authorization
		String originalRequestUrl = session.getAttribute("_login_original_request_url").toString();
		if (originalRequestUrl != null && originalRequestUrl.length() > 0) {
			session.removeAttribute("_login_original_request_url");
			response.sendRedirect(originalRequestUrl);
		}
    }
}

这就是关于 Servlet 过滤器、登录资源以及如何处理登录页面的全部内容,一旦您理解了所有这些,您将能够创建任何其他过滤器,包括身份验证和授权,它们在本项目中是捆绑在一起的,尽管很容易扩展以提供用户映射以授权到不同的路径和操作。

关注点

理解过滤器的工作原理并不是一项困难的任务,学习曲线非常快速和直接,尽管认识到它在 Web 应用程序服务器上的强大功能很重要。

在 Linux 上运行 Tomcat 或 JBoss 与 Windows 上的 Active Domain Directory 进行互操作不是一件简单的工作,389 端口转发被证明是足够的,尽管可用的信息通常包含大量查询 ldap 的参数,而我们的项目应保持简单,因此找出只认证用户的最小努力需要一些时间。

最后一点,我们的项目将不得不包含相同的身份验证机制,但来自 Windows .NET 和 C# - 尽管这可能是在未来另一个写作的主题。

历史

该产品的第一个版本提供系统范围的登录页面进行身份验证。

计划未来将 LDAP 参数映射并授权到 Web 服务器。

© . All rights reserved.