JavaRush /Java 博客 /Random-ZH /JAAS - 技术简介(第 1 部分)
Viacheslav
第 3 级

JAAS - 技术简介(第 1 部分)

已在 Random-ZH 群组中发布
访问安全性已经在 J​​ava 中实现了相当长的时间,提供这种安全性的体系结构称为 JAAS - Java 身份验证和授权服务。这篇评论将试图揭开身份验证、授权是什么以及 JAAS 与它们有何关系的谜团。JAAS 如何与 Servlet API 成为朋友,以及他们的关系中存在哪些问题。
JAAS - 技术简介(第 1 部分)- 1

介绍

在这篇评论中,我想讨论 Web 应用程序安全这样的主题。Java 有多种提供安全性的技术: 但我们今天讨论的是另一种技术,它被称为“Java 身份验证和授权服务(JAAS)”。正是她描述了身份验证和授权等重要的事情。让我们更详细地看看这个。
JAAS - 技术简介(第 1 部分)- 2

联合航空航天局

JAAS是 Java SE 的扩展,在Java 身份验证和授权服务 (JAAS) 参考指南中进行了描述。正如该技术的名称所示,JAAS 描述了如何执行身份验证和授权:
  • Authentication ”:翻译自希腊语,“authentikos”的意思是“真实的、真正的”。也就是说,认证是对真实性的测试。无论谁被验证都是他们所说的真实身份。

  • 授权”:翻译自英文,意思是“许可”。即授权是认证成功后进行的访问控制。

也就是说,JAAS 的作用是确定谁正在请求访问资源,并决定他是否可以获得此访问权限。 生活中的一个小比喻:你正在路上开车,检查员拦住了你。请提供文件-认证。您可以携带文件-授权驾驶汽车吗?或者,例如,您想在商店购买酒精饮料。首先,您需要提供护照 - 身份验证。接下来,根据您的年龄,决定您是否有资格购买酒精饮料。这就是授权。在 Web 应用程序中,以用户身份登录(输入用户名和密码)就是身份验证。而决定你可以打开哪些页面是由授权决定的。这就是“Java 身份验证和授权服务 (JAAS)” 为我们提供帮助的地方。在考虑 JAAS 时,了解 JAAS 描述的几个关键概念非常重要:主题、主体、凭证。 主体是认证的主体。也就是说,它是权利的持有者或持有者。在文档中,主题被定义为执行某些操作的请求的来源。必须以某种方式描述主题或来源,为此目的使用“主体”,在俄语中有时也称为“主体”。也就是说,每个Principal都是一个Subject从某个角度的代表。为了说得更清楚,我们举个例子:某个人是一个Subject。以下人员可以担任校长:
  • 他的驾驶执照代表一个人作为道路使用者
  • 他的护照,代表一个人作为其国家的公民
  • 他的外国护照,作为国际关系参与者的代表
  • 他在图书馆的借书卡,代表一个人作为图书馆的读者
另外,Subject还有一组“Credential”,英文意思是“身份”。这就是主体如何确认他是他的方式。例如,用户的密码可以是Credential。或者任何能让用户确认他确实是他的物体。现在让我们看看 JAAS 如何在 Web 应用程序中使用。
JAAS - 技术简介(第 1 部分)- 3

Web应用程序

因此,我们需要一个网络应用程序。Gradle自动项目构建系统将帮助我们创建它。由于使用 Gradle,我们可以通过执行小命令,以我们需要的格式组装 Java 项目,自动创建必要的目录结构等等。您可以在简短概述:“ Gradle 简介”或官方文档“ Gradle 入门”中阅读有关 Gradle 的更多信息。我们需要对项目进行初始化(Initialization),为此Gradle有一个特殊的插件:“ Gradle Init Plugin ”(Init是Initialization的缩写,方便记忆)。要使用此插件,请在命令行上运行命令:
gradle init --type java-application
成功完成后,我们将拥有一个Java项目。现在让我们打开项目的构建脚本进行编辑。构建脚本是一个名为 的文件build.gradle,它描述了应用程序构建的细微差别。因此得名构建脚本。我们可以说这是一个项目构建脚本。Gradle 是一个多功能工具,其基本功能可以通过插件进行扩展。因此,首先让我们关注“插件”块:
plugins {
    id 'java'
    id 'application'
}
默认情况下,Gradle 按照我们指定的“ --type java-application”设置了一组核心插件,即那些包含在 Gradle 本身发行版中的插件。如果您转到gradle.org网站上的“文档”(即文档)部分,那么在“参考”部分的主题列表左侧,我们会看到“核心插件”部分,即 部分包含这些非常基本的插件的描述。让我们准确选择我们需要的插件,而不是 Gradle 为我们生成的插件。根据文档,“ Gradle Java Plugin ”提供了Java代码的基本操作,例如编译源代码。另外,根据文档,“ Gradle 应用程序插件”为我们提供了使用“可执行 JVM 应用程序”的工具,即 可以作为独立应用程序启动的 java 应用程序(例如,控制台应用程序或具有自己的 UI 的应用程序)。事实证明,我们不需要“应用程序”插件,因为...... 我们不需要独立的应用程序,我们需要一个网络应用程序。我们把它删除吧。以及“mainClassName”设置,只有这个插件知道。此外,在提供应用程序插件文档链接的同一“打包和分发”部分中,还有一个 Gradle War 插件的链接。 Gradle War Plugin,如文档中所述,提供对创建 war 格式的 Java Web 应用程序的支持。WAR 格式意味着将创建 WAR 存档而不是 JAR 存档。看来这就是我们所需要的。另外,正如文档所述,“War 插件扩展了 Java 插件”。即我们可以将java插件替换为war插件。因此,我们的插件块最终将如下所示:
plugins {
    id 'war'
}
另外,在“Gradle War Plugin”的文档中,据说该插件使用了额外的“项目布局”。布局从英语翻译为位置。也就是说,war 插件默认期望存在用于其任务的特定位置的文件。它将使用以下目录来存储 Web 应用程序文件:src/main/webapp 该插件的行为描述如下:
JAAS - 技术简介(第 1 部分)- 4
也就是说,在构建 Web 应用程序的 WAR 存档时,插件将考虑此位置的文件。此外,Gradle War Plugin 文档表示该目录将是“存档的根目录”。我们已经可以在其中创建一个 WEB-INF 目录并在其中添加 web.xml 文件。这是什么类型的文件? web.xml- 这是一个“部署描述符”或“部署描述符”。该文件描述了如何配置我们的 Web 应用程序以使其工作。该文件指定我们的应用程序将处理哪些请求、安全设置等等。从本质上讲,它有点类似于 JAR 文件中的清单文件(请参阅“使用清单文件:基础知识”)。Manifest 文件告诉我们如何使用 Java 应用程序(即 JAR 存档),而 web.xml 告诉我们如何使用 Java Web 应用程序(即 WAR 存档)。“部署描述符”的概念本身并不是出现的,而是在文档“ Servlet API 规范”中描述的任何Java Web应用程序都依赖于这个“Servlet API”。重要的是要理解这是一个API——也就是说,它是一些交互契约的描述。Web应用程序不是独立的应用程序。它们运行在Web服务器上,它提供与用户的网络通信。也就是说,Web服务器是Web应用程序的一种“容器”。这是合乎逻辑的,因为我们要编写Web应用程序的逻辑,即用户将看到哪些页面以及如何查看他们应该对用户的操作做出反应。我们不想编写如何将消息发送给用户、如何传输信息字节以及其他低级且质量要求很高的事情的代码。此外,事实证明,Web 应用程序都是不同的,但数据传输是相同的。也就是说,一百万个程序员必须一遍又一遍地为同一目的编写代码。因此,Web 服务器负责一些用户交互和数据交换,Web 应用程序和开发人员负责生成该数据。为了连接这两个部分,即 Web 服务器和 Web 应用程序,您需要为它们的交互签订合同,即 他们将遵循什么规则来做到这一点?为了以某种方式描述契约,即 Web 应用程序和 Web 服务器之间的交互应该是什么样子,Servlet API 被发明了。有趣的是,即使您使用像 Spring 这样的框架,仍然有一个 Servlet API 在后台运行。也就是说,您使用 Spring,Spring 会为您使用 Servlet API。事实证明,我们的Web应用程序项目必须依赖于Servlet API。在这种情况下,Servlet API 将成为依赖项。众所周知,Gradle 还允许您以声明方式描述项目依赖关系。插件描述了如何管理依赖关系。例如,Java Gradle Plugin 引入了“testImplementation”依赖管理方法,该方法表示这样的依赖仅在测试时需要。但是Gradle War Plugin添加了一个依赖管理方法“providedCompile”,它表示这样的依赖将不会包含在我们的Web应用程序的WAR存档中。为什么我们不将 Servlet API 包含在我们的 WAR 存档中?因为Servlet API将由Web服务器本身提供给我们的Web应用程序。如果 Web 服务器提供 Servlet API,则该服务器称为 Servlet 容器。因此,Web服务器有责任为我们提供Servlet API,而我们有责任仅在代码编译时提供ServletAPI。这就是为什么providedCompile。因此,依赖块将如下所示:
dependencies {
    providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
    testImplementation 'junit:junit:4.12'
}
那么,让我们回到 web.xml 文件。默认情况下,Gradle 不会创建任何部署描述符,因此我们需要自己创建。让我们创建一个目录src/main/webapp/WEB-INF,并在其中创建一个名为 的 XML 文件web.xml。现在让我们打开“Java Servlet 规范”本身和“第 14 章部署描述符”一章。如“14.3 部署描述符”中所述,部署描述符的 XML 文档由模式http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd描述。XML 模式描述了文档可以包含哪些元素以及它们应该以什么顺序出现。哪些是强制性的,哪些不是。一般来说,它描述了文档的结构,并允许您检查 XML 文档的组成是否正确。现在让我们使用“ 14.5示例”一章中的示例,但必须为3.1版本指定方案,即
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd
我们的空的web.xml将如下所示:
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
         http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <display-name>JAAS Example</display-name>
</web-app>
现在让我们描述将使用 JAAS 保护的 servlet。之前,Gradle 为我们生成了 App 类。让我们把它变成一个servlet。正如《 CHAPTER 2 The Servlet Interface 》规范中所述,“对于大多数目的,开发人员将扩展 HttpServlet 来实现他们的 servlet ”,也就是说,要使一个类成为 servlet,您需要从以下位置继承该类HttpServlet
public class App extends HttpServlet {
	public String getGreeting() {
        return "Secret!";
    }
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.getWriter().print(getGreeting());
    }
}
正如我们所说,Servlet API 是服务器和我们的 Web 应用程序之间的契约。这个契约允许我们描述当用户联系服务器时,服务器将以对象的形式生成来自用户的请求HttpServletRequest并将其传递给servlet。它还将为 servlet 提供一个对象,HttpServletResponse以便 servlet 可以为用户编写对其的响应。一旦 servlet 完成运行,服务器就能够向用户提供基于它的响应HttpServletResponse。即servlet不直接与用户通信,而只与服务器通信。为了让服务器知道我们有一个 servlet 以及需要使用它来处理哪些请求,我们需要在部署描述符中告诉服务器这一点:
<servlet>
	<servlet-name>app</servlet-name>
	<servlet-class>jaas.App</servlet-class>
</servlet>
<servlet-mapping>
	<servlet-name>app</servlet-name>
	<url-pattern>/secret</url-pattern>
</servlet-mapping>
在这种情况下,所有请求都/secret不会通过 name 发送到我们的一个 servlet app,该 servlet 对应于 class jaas.App。正如我们之前所说,Web 应用程序只能部署在 Web 服务器上。Web 服务器可以单独安装(独立)。但出于本次审查的目的,另一种选择是合适的 - 在嵌入式服务器上运行。这意味着服务器将以编程方式创建和启动(插件将为我们执行此操作),同时我们的 Web 应用程序将部署在其上。Gradle 构建系统允许您使用“ Gradle Gretty Plugin ”插件来实现以下目的:
plugins {
    id 'war'
    id 'org.gretty' version '2.2.0'
}
此外,Gretty 插件有很好的文档。首先,Gretty 插件允许您在不同的 Web 服务器之间切换。文档对此进行了更详细的描述:“在 servlet 容器之间切换”。让我们切换到 Tomcat,因为... 它是最流行的使用之一,并且也有很好的文档和许多示例和分析的问题:
gretty {
    // Переключаемся с дефолтного Jetty на Tomcat
    servletContainer = 'tomcat8'
    // Укажем Context Path, он же Context Root
    contextPath = '/jaas'
}
现在我们可以运行“gradle appRun”,然后我们的 Web 应用程序将在 http://localhost:8080/jaas/secret 上可用
JAAS - 技术简介(第 1 部分)- 5
检查 Tomcat 是否选择了 servlet 容器(请参阅#1)并检查我们的 Web 应用程序在哪个地址可用(请参阅#2)非常重要。
JAAS - 技术简介(第 1 部分)- 6

验证

身份验证设置通常由两部分组成:服务器端的设置和在此服务器上运行的 Web 应用程序端的设置。如果没有其他原因,Web 应用程序的安全设置只能与 Web 服务器的安全设置交互。我们转向 Tomcat 并没有白费,因为…… Tomcat 具有描述良好的架构(请参阅“ Apache Tomcat 8 架构”)。从这个架构的描述可以清楚地看出,Tomcat作为一个Web服务器,将Web应用程序表示为一定的上下文,称为“ Tomcat上下文”。此上下文允许每个 Web 应用程序拥有自己的设置,与其他 Web 应用程序隔离。此外,Web 应用程序可以影响此上下文的设置。灵活方便。为了更深入地了解,我们建议阅读文章“了解 Tomcat 上下文容器”和 Tomcat 文档部分“上下文容器”。如上所述,我们的 Web 应用程序可以使用/META-INF/context.xml. 我们可以影响的非常重要的设置之一是安全领域。 安全领域是一种“安全区域”。指定特定安全设置的区域。因此,在使用安全领域时,我们应用为此领域定义的安全设置。安全领域由容器管理,即 Web 服务器,而不是我们的 Web 应用程序。我们只能告诉服务器我们的应用程序需要扩展哪些安全范围。Tomcat 文档中的“领域组件”部分将领域描述为有关用户及其用于执行身份验证的角色的数据集合。Tomcat 提供了一组不同的 Security Realm 实现,其中之一是“ Jaas Realm ”。了解了一些术语后,我们来描述文件中的 Tomcat 上下文/META-INF/context.xml
<?xml version="1.0" encoding="UTF-8"?>
<Context>
    <Realm className="org.apache.catalina.realm.JAASRealm"
           appName="JaasLogin"
           userClassNames="jaas.login.UserPrincipal"
           roleClassNames="jaas.login.RolePrincipal"
           configFile="jaas.config" />
</Context>
appName- 应用名称。Tomcat 将尝试将此名称与configFile. configFile- 这是“登录配置文件”。JAAS 文档中可以看到这样的示例:“附录 B:登录配置示例”。此外,重要的是,该文件将首先在资源中搜索。因此,我们的网络应用程序可以自己提供这个文件。属性userClassNamesroleClassNames包含代表用户主体的类的指示。JAAS 将“用户”和“角色”的概念分离为两个不同的概念java.security.Principal。我们来描述一下上面的类。让我们为用户主体创建最简单的实现:
public class UserPrincipal implements Principal {
    private String name;
    public UserPrincipal(String name) {
        this.name = name;
    }
    @Override
    public String getName() {
        return name;
    }
}
我们将为 重复完全相同的实现RolePrincipal。从界面中可以看到,Principal的主要作用是存储并返回一些代表Principal的名称(或ID)。现在,我们有一个安全领域,我们有主要的类。configFile仍然需要从“ ”属性(又称为 )填充文件login configuration file。它的描述可以在Tomcat文档中找到:“ The Realm Component ”。
JAAS - 技术简介(第 1 部分)- 7
也就是说,我们可以将 JAAS 登录配置设置放置在 Web 应用程序的资源中,并且借助 Tomcat Context,我们将能够使用它。该文件必须可作为 ClassLoader 的资源使用,因此其路径应如下所示:\src\main\resources\jaas.config 让我们设置该文件的内容:
JaasLogin {
    jaas.login.JaasLoginModule required debug=true;
};
值得注意的是,context.xml这里和 in 中使用了相同的名称。这将安全领域映射到登录模块。因此,Tomcat Context 告诉我们哪些类代表主体,以及要使用哪个 LoginModule。我们所要做的就是实现这个LoginModule。 LoginModule也许是 JAAS 中最有趣的东西之一。官方文档将帮助我们开发LoginModule:《Java Authentication and Authorization Service (JAAS): LoginModule Developer's Guide》。让我们来实现登录模块。让我们创建一个实现该接口的类LoginModule
public class JaasLoginModule implements LoginModule {
}
首先我们描述一下初始化方法LoginModule
private CallbackHandler handler;
private Subject subject;
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, <String, ?> sharedState, Map<String, ?> options) {
	handler = callbackHandler;
	this.subject = subject;
}
此方法将保存Subject,我们将进一步验证并填充有关主体的信息。我们还将保存给CallbackHandler我们的以供将来使用。在帮助下,CallbackHandler我们可以稍后请求有关身份验证主题的各种信息。CallbackHandler您可以在文档的相应部分阅读更多相关信息:“ JAAS 参考指南:CallbackHandler ”。login接下来,执行认证方法Subject。这是身份验证的第一阶段:
@Override
public boolean login() throws LoginException {
	// Добавляем колбэки
	Callback[] callbacks = new Callback[2];
	callbacks[0] = new NameCallback("login");
	callbacks[1] = new PasswordCallback("password", true);
	// При помощи колбэков получаем через CallbackHandler логин и пароль
	try {
		handler.handle(callbacks);
		String name = ((NameCallback) callbacks[0]).getName();
		String password = String.valueOf(((PasswordCallback) callbacks[1]).getPassword());
		// Далее выполняем валидацию.
		// Тут просто для примера проверяем определённые значения
		if (name != null && name.equals("user123") && password != null && password.equals("pass123")) {
			// Сохраняем информацию, которая будет использована в методе commit
			// Не "пачкаем" Subject, т.к. не факт, что commit выполнится
			// Для примера проставим группы вручную, "хардcodeно".
			login = name;
			userGroups = new ArrayList<String>();
			userGroups.add("admin");
			return true;
		} else {
			throw new LoginException("Authentication failed");
		}
	} catch (IOException | UnsupportedCallbackException e) {
		throw new LoginException(e.getMessage());
	}
}
重要的是login我们不应该改变Subject. 这种改变应该只发生在确认方法中commit。接下来我们要描述一下确认认证成功的方法:
@Override
public boolean commit() throws LoginException {
	userPrincipal = new UserPrincipal(login);
	subject.getPrincipals().add(userPrincipal);
	if (userGroups != null && userGroups.size() > 0) {
		for (String groupName : userGroups) {
			rolePrincipal = new RolePrincipal(groupName);
			subject.getPrincipals().add(rolePrincipal);
		}
	}
	return true;
}
login将方法和 分开似乎很奇怪commit。但重点是登录模块可以组合。为了成功进行身份验证,可能需要多个登录模块才能成功工作。并且只有当所有必要的模块都工作时,才保存更改。这是身份验证的第二阶段。让我们以abort和方法结束logout
@Override
public boolean abort() throws LoginException {
	return false;
}
@Override
public boolean logout() throws LoginException {
	subject.getPrincipals().remove(userPrincipal);
	subject.getPrincipals().remove(rolePrincipal);
	return true;
}
abort当第一阶段身份验证失败时调用 该方法。logout当系统注销时调用该方法。实现Login Module并配置它之后Security Realm,现在我们需要表明web.xml我们想要使用特定的一个Login Config
<login-config>
  <auth-method>BASIC</auth-method>
  <realm-name>JaasLogin</realm-name>
</login-config>
我们指定了安全领域的名称并指定了身份验证方法 - BASIC。这是“ 13.6 身份验证”部分中 Servlet API 中描述的身份验证类型之一。剩余 n
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION