Jetty,Java和OAuth入门

使用Okta的身份管理平台轻松部署您的应用程序 使用Okta的API在几分钟之内即可对任何应用程序中的用户进行身份验证,管理和保护。 今天尝试Okta。

Jetty是一个小型,高度可扩展的基于Java的Web服务器和servlet引擎。 它支持HTTP / 2,WebSockets和许多其他协议。 它为大型和小型网站和框架(例如Google AppEngine)提供支持。 因为它是一个Eclipse项目,所以其开源项目称为Eclipse Jetty。 它符合标准,是开源的,并且可以商业使用。 当托管Java应用程序时,它是Tomcat的主要替代方法。 就像使用Tomcat一样,您可以同时使用嵌入式和独立的Jetty。

默认情况下,Spring Boot使用嵌入式Web服务器创建应用程序,这意味着该服务器是嵌入在应用程序代码本身中的,因此您不必运行单独的Web服务器即可发布Java Web应用程序。 但是,只需进行一些配置,您还可以将WAR文件发布到单独的Jetty或Tomcat Servlet容器(老式的应用程序服务器样式)。 Spring默认情况下也使用Tomcat,但是您可以轻松地更改它,如您所见。

在本教程中,您将构建一个嵌入了Jetty的简单Web服务。 之后,您将在Spring Boot和Jetty中构建相同的Web服务。 最后,您将使用方法级安全性(以Okta作为OAuth / OIDC提供者)将JWT(JSON Web令牌)身份验证和授权添加到Web服务。

安装项目依赖项

开始之前,您需要先安装一些东西。

Java 11 :该项目使用Java11。如果没有Java 11,则可以安装OpenJDK 您也可以使用Homebrew安装OpenJDK。 SDKMAN是用于安装和管理Java SDK的另一个不错的选择。

HTTPie :这是用于发出HTTP请求的简单命令行实用程序。 您将使用它来测试REST应用程序。 在其网站上查看安装说明

Okta开发人员帐户 :您将Okta用作OAuth / OIDC提供程序,以向应用程序添加JWT身份验证和授权。 如果尚未登录,请访问他们的网站并注册他们的免费开发者帐户之一。

Gradle :这是可选安装。 如果您从仓库中下载了本教程的项目,则可以使用Gradle包装器运行该项目,而无需安装Gradle。 如果您想从头开始构建项目,则需要安装Gradle

使用Java和Jetty构建简单的Web服务

本教程的第一步是使用Java和Gradle构建一个简单的Web服务。 为此,您将使用GradleGretty插件 Gretty使使用Gradle在嵌入式servlet容器上运行Web应用程序变得超级容易,并支持Tomcat和Jetty。

如果您选择从GitHub存储库下载本教程的项目 ,请遵循以下几个步骤,同时我将介绍如何从头开始构建项目。

git clone https://github.com/oktadeveloper/okta-spring-boot-jetty-example.git

首先,打开一个shell并导航到您想要项目驻留的适当目录(或创建一个目录)。 使用Gradle CLI初始化项目。

mkdir jetty
cd jetty
gradle init --type=basic --dsl=groovy --project-name=JettyEmbedded

编辑项目根目录中的build.gradle文件:

plugins {  
    id 'java'  
    id 'war'  
    id 'org.gretty' version '2.3.1'  
}  
  
repositories {  
    jcenter()  
}  
  
dependencies {  
    providedCompile 'javax.servlet:javax.servlet-api:3.1.0'  
}  
  
gretty {  
    contextPath = '/'  
}

我想在这里指出一些事情。 注意plugins块中的org.gretty插件。 此外,注意javax.servlet:javax.servlet-api您添加依赖使用providedCompile 这会将其添加到编译类路径中,但不会将其添加到打包的war文件中,因为这将由servlet容器在部署时提供。 最后,请注意,嵌入式servlet容器的上下文路径已设置为gretty块中的gretty

现在,为Java文件创建根目录( src/main/java是标准Java根文件夹,加上com.okta.jettyembedded包):

mkdir -p src/main/java/com/okta/jettyembedded

创建一个简单的servlet:

src/main/java/com/okta/jettyembedded/Hello.java
package com.okta.jettyembedded;  
  
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 java.io.IOException;  
  
@WebServlet(name = "HelloServlet", urlPatterns = {"hello"}, loadOnStartup = 1)  
public class Hello extends HttpServlet {  
      
    protected void doGet(HttpServletRequest request, HttpServletResponse response)  
        throws ServletException, IOException {  
        response.getWriter().print("Howdy");  
    }  
      
}

现在您可以运行该应用程序:

gradlew apprun

要测试它,请打开另一个shell窗口并使用HTTPie:

http :8080/hello
HTTP/1.1 200 OK
Content-Length: 5
Date: Fri, 06 Sep 2019 20:23:40 GMT
Server: Jetty(9.2.26.v20180806)

Howdy

您已经构建了一个超级简单的Web servlet。 它还没有做很多,但是(希望)可以工作。 注意Hello.java类中的@WebServlet批注。 在这里,您可以配置一些servlet参数,而不是在web.xml文件中。 将此配置移动到代码中,可以更轻松地构建和维护某些Servlet配置。

接下来,您将看到功能更全的Web Servlet。

通过添加和删除改进Java Servlet

现在,您将创建一个Web应用程序以跟踪远足列表。 它将演示如何支持POST和DELETE操作以及简单的GET和一些简单的错误处理。

创建一个新的Java文件:

src/main/java/com/okta/jettyembedded/HikesTodoServlet.java
package com.okta.jettyembedded;  
  
import java.io.IOException;  
import javax.servlet.annotation.WebServlet;  
import javax.servlet.http.HttpServlet;  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
import java.util.ArrayList;  
import java.util.Arrays;
import java.util.List;
  
@WebServlet(name = "HikesTodoServlet", urlPatterns = {"hikes"}, loadOnStartup = 1)  
public class HikesTodoServlet extends HttpServlet {  
  
    // Not synchronized  
   private List<String> hikes = new ArrayList<>(Arrays.asList(
            "Wonderland Trail", "South Maroon Peak", "Tour du Mont Blanc",
            "Teton Crest Trail", "Everest Base Camp via Cho La Pass", "Kesugi Ridge"
    ));
      
    protected void doGet(HttpServletRequest request, HttpServletResponse response)  
        throws IOException {  
        response.getWriter().print(String.join("\n", this.hikes));  
    }  
  
    protected void doPost(HttpServletRequest request, HttpServletResponse response)  
        throws IOException {  
        String hike = request.getParameter("hike");  
        if (hike == null) {  
            response.setStatus(400);  
            response.getWriter().print("Param 'hike' cannot be null.");  
        }  
        else if (this.hikes.contains(hike)) {  
            response.setStatus(400);  
            response.getWriter().print("The hike '"+hike+"' already exists.");  
        }  
        else {  
            this.hikes.add(hike);  
            response.getWriter().print(String.join("\n", this.hikes));  
        }  
    }  
  
    protected void doDelete(HttpServletRequest request, HttpServletResponse response)  
        throws IOException {  
        String hike = request.getParameter("hike");  
        if (hike == null) {  
            response.setStatus(400);  
            response.getWriter().print("Param 'hike' cannot be null.");  
        }  
        else {  
            this.hikes.remove(hike);  
            response.getWriter().print(String.join("\n", this.hikes));  
        }  
    }  
      
}

使用Control-C停止服务器,然后使用gradle apprun重启服务器。

获取远足清单:

http :8080/hikes
HTTP/1.1 200 OK
...

Wonderland Trail
South Maroon Peak
Tour du Mont Blanc
Teton Crest Trail
Everest Base Camp via Cho La Pass
Kesugi Ridge

发布新的加息:

http -f POST :8080/hikes hike="Pear Lake"
HTTP/1.1 200 OK
...

Wonderland Trail
South Maroon Peak
Tour du Mont Blanc
Teton Crest Trail
Everest Base Camp via Cho La Pass
Kesugi Ridge
Pear Lake

删除远足:

http DELETE :8080/hikes hike=="South Maroon Peak"
HTTP/1.1 200 OK
...

Wonderland Trail
Tour du Mont Blanc
Teton Crest Trail
Everest Base Camp via Cho La Pass
Kesugi Ridge
Pear Lake

现在,尝试删除不存在的加息,或发送一个空值:

http DELETE :8080/hikes
HTTP/1.1 400 Bad Request
...

Param 'hike' cannot be null.

要将其部署到实时服务器,您可以按原样部署项目,使用gradle apprun通过嵌入式Jetty服务器运行应用程序。 您还可以通过使用gradle war构建war文件并将war文件(位于build/libs )复制到服务器上,从而将其部署到外部Jetty服务器。

注意:这是REST服务的非常幼稚的实现。 它使用内存中的ArrayList作为不同步的数据源(因此将在实际的Web servlet中遇到线程问题)。 对于超出本教程范围的任何内容,您都需要实现某种数据库后端。 有关如何执行此操作的帮助,请参阅教程末尾列出的示例博客文章。 通常,您还将添加一个PUT端点,并为每个项目分配一个ID用作索引,以便可以更新数据,但这超出了本教程的范围。

到目前为止,一切进展顺利。 在下一部分中,您将使用Spring Boot重新创建相同的Hikes ToDo应用,并使用Okta作为OAuth / OIDC提供者对应用进行JWT令牌认证。

创建一个OIDC应用程序

现在,让我们前往Okta进行一些实地考察,并为OAuth / OpenID Connect(OIDC)进行设置。 它们一起是用于实施安全授权和身份验证的一组开放标准。 在本教程中,Okta将充当身份提供者,而您的Spring Boot应用将成为客户端。

您应该已经在Okta注册了免费的开发者帐户。 浏览至https://developer.okta.com上的开发人员仪表板。 如果这是您第一次登录,则可能需要单击“ 管理员”按钮。

要配置JWT身份验证和授权,您需要创建一个OIDC应用程序。

在顶部菜单中,单击“ 应用程序”按钮。 单击添加应用程序按钮。

选择应用程序类型Web

单击下一步

为应用命名。 我将其命名为“ Spring Boot Jetty”。

登录重定向URI下 ,添加两个新的URI:

  • https://oidcdebugger.com/debug
  • http://localhost:8080/login/oauth2/code/okta

在“ 允许的授予类型”下 ,选中“ 隐式(混合)”

其余的默认值将起作用。

单击完成

使页面保持打开状态或记下Client ID 生成令牌时需要一点时间。

注意:您将使用oidcdebugger.com重定向URI和隐式授予类型创建访问令牌,您可以在命令行中使用HTTPie。 第二个URI是Spring Security在使用OAuth登录功能时用于Okta的默认重定向URI。

使用Jetty创建一个Spring Boot项目

要创建Spring Boot项目,您将使用Spring Initializr 查看其GitHub项目以查看其代码。 Initializr有一个不错的Web表单,用于配置和下载Spring Boot入门项目,但是对于该项目,您将使用其REST API。

从外壳执行以下命令,以下载已配置的启动程序项目的zip文件。

http https://start.spring.io/starter.zip \
  javaVersion==11 \
  dependencies==web \
  language==java \
  type==gradle-project \
  name==SpringBootJetty \
  groupId==com.okta.springbootjetty \
  artifactId==SpringBootJetty \
  packageName==com.okta.springbootjetty -o SpringBootJetty.zip

解压缩下载的文件,然后在您选择的IDE中打开目录。

首先,修改build.gradle文件,以便项目使用Jetty嵌入式容器,而不是默认的Tomcat。 添加spring-boot-starter-jetty依赖关系,并排除spring-boot-starter-tomcat依赖关系。

更改build.gradle文件以匹配以下内容:

plugins {
    id 'org.springframework.boot' version '2.2.0.RELEASE'
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
    id 'java'
}

group = 'com.okta.springbootjetty'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-jetty' 
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

test {
    useJUnitPlatform()
}


configurations {  
   compile.exclude module: "spring-boot-starter-tomcat"  
}

现在添加一个WebController.java文件。

src/main/java/com/okta/springbootjetty/WebController.java
package com.okta.springbootjetty;  
  
import org.springframework.stereotype.Controller;  
import org.springframework.web.bind.annotation.*;  
  
import javax.servlet.http.HttpServletResponse;  
import java.util.ArrayList;  
import java.util.Arrays;
import java.util.List;
  
@Controller  
public class WebController {  
  
   private List<String> hikes = new ArrayList<>(Arrays.asList(
            "Wonderland Trail", "South Maroon Peak", "Tour du Mont Blanc",
            "Teton Crest Trail", "Everest Base Camp via Cho La Pass", "Kesugi Ridge"
    ));
  
    @GetMapping("/")  
    @ResponseBody  
    public String indexGet() {  
        return String.join("\n", this.hikes);  
    }  
  
    @PostMapping("/")  
    @ResponseBody  
    public String indexPost(@RequestParam String hike, HttpServletResponse response) {  
        if (hike == null) {  
            response.setStatus(400);  
            return "Param 'hike' cannot be null.";  
        }  
        else if (this.hikes.contains(hike)) {  
            response.setStatus(400);  
            return "The hike '"+hike+"' already exists.";  
        }  
        else {  
            this.hikes.add(hike);  
            return String.join("\n", this.hikes);  
        }  
    }  
  
    @DeleteMapping("/")  
    @ResponseBody  
    public String indexDelete(@RequestParam String hike, HttpServletResponse response) {  
        if (hike == null) {  
            response.setStatus(400);  
            return "Param 'hike' cannot be null.";  
        }  
        else {  
            this.hikes.remove(hike);  
            return String.join("\n", this.hikes);  
        }  
    }  
  
}

该控制器重新创建在第一个Jetty Hikes ToDo应用程序中发现的相同功能,但现在使用Spring Boot。 您会注意到Spring简化了一些语法。 @ResponseBody注释告诉Spring Boot控制器正在直接返回响应主体(与返回模板名称相反)。 另外,请注意,代码使用依赖注入来获取HttpServletResponse以及请求参数。

运行Spring Boot REST服务(确保您的其他服务已停止,否则您将收到端口冲突错误):

gradle bootRun

在第二个Shell窗口中,获取远足列表(注意,下面没有/hikes路径)。

http :8080

另外,尝试添加和删除新的加息。

发布新的加息:

http -f POST :8080 hike="Pear Lake"

删除远足:

http DELETE :8080 hike=="South Maroon Peak"

部署Spring Boot项目

现在,您有了一个在嵌入式Jetty容器上运行的Spring Boot应用程序。 要将其部署到生产服务器,请使用gradle bootJar构建可执行的jar文件,将该jar文件复制到服务器,然后使用java -jar <your jar file name>.jar运行它。 无需单独的Web服务器,因为此jar包含嵌入式Jetty Web服务器。

注意:对于在同一服务器上具有多个单独应用程序的应用程序服务器,如果要进行更老式的部署,则需要构建war文件。 有关如何执行此操作的Spring文档非常有用。 从本质上讲,你需要做两件事情:1)添加war插件到项目的依赖,以及2)改变码头或Tomcat依赖于providedRuntime所以它不是在打包的战争包括在内。 然后,您构建一个war文件并将其部署到服务器上的servlet Web应用程序路径。

将OAuth / OIDC登录名添加到Spring Boot App

您注册了Okta并创建了OIDC应用程序。 现在是时候配置Spring Boot应用程序以使用OAuth / OIDC进行身份验证和授权了。

首先,将您的Issuer URI添加到src/main/resources/application.properties文件。 您需要用实际的Okta URL替换{yourOktaUrl} 如果您访问https://developer.okta.com并导航至API授权服务器 ,则会看到default授权服务器的Issuer URI。

okta.oauth2.issuer=https://{yourOktaUrl}/oauth2/default

在该文件中时,从您先前创建的“ Spring Boot Jetty”应用程序中添加客户端ID和客户端**。

okta.oauth2.clientId={clientId}
okta.oauth2.clientSecret={clientSecret}

接下来,您需要在dependencies {}块中的build.gradle文件中添加以下依赖。

implementation 'com.okta.spring:okta-spring-boot-starter:1.3.0'

其中包括Okta Spring Boot Starter,这是一个很好的项目,可简化Spring Boot对Okta身份验证和授权的使用。 查看项目页面以获取更多信息

您还需要更新SpringBootJettyApplication类以匹配以下内容:

package com.okta.springbootjetty;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@SpringBootApplication
public class SpringBootJettyApplication extends WebSecurityConfigurerAdapter {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootJettyApplication.class, args);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .anyRequest().permitAll()
                .and()
            .oauth2Login()
                .and()
            .oauth2ResourceServer().jwt();
    }

}

保护您的DELETE和POST端点

configure(HttpSecurity http)方法将具有OAuth 2.0登录名的Spring Boot应用配置为OAuth 2.0资源服务器,并默认允许所有请求。 您将通过@PreAuthorize批注使用方法级别的安全性来保护下面的DELETE和POST端点。

最后,将@PreAuthorize("isAuthenticated")批注添加到WebController类的indexPost()indexDelete()方法中。

package com.okta.springbootjetty;  
  
import org.springframework.security.access.prepost.PreAuthorize;  
import org.springframework.stereotype.Controller;  
import org.springframework.web.bind.annotation.*;  
  
import javax.servlet.http.HttpServletResponse;  
import java.util.ArrayList;  
import java.util.Arrays;
import java.util.List;  
  
@Controller  
public class WebController {  
  
   private List<String> hikes = new ArrayList<>(Arrays.asList(
            "Wonderland Trail", "South Maroon Peak", "Tour du Mont Blanc",
            "Teton Crest Trail", "Everest Base Camp via Cho La Pass", "Kesugi Ridge"
    ));
  
    @GetMapping("/")  
    @ResponseBody  
    public String indexGet() {  
        return String.join("\n", this.hikes);  
    }  
  
    @PreAuthorize("isAuthenticated")  // <- ***ADDED***
    @PostMapping("/")  
    @ResponseBody  
    public String indexPost(@RequestParam String hike, HttpServletResponse response) {  
        if (hike == null) {  
            response.setStatus(400);  
            return "Param 'hike' cannot be null.";  
        }  
        else if (this.hikes.contains(hike)) {  
            response.setStatus(400);  
            return "The hike '"+hike+"' already exists.";  
        }  
        else {  
            this.hikes.add(hike);  
            return String.join("\n", this.hikes);  
        }  
    }  
  
    @PreAuthorize("isAuthenticated")  // <- ***ADDED***
    @DeleteMapping("/")  
    @ResponseBody  
    public String indexDelete(@RequestParam String hike, HttpServletResponse response) {  
        if (hike == null) {  
            response.setStatus(400);  
            return "Param 'hike' cannot be null.";  
        }  
        else {  
            this.hikes.remove(hike);  
            return String.join("\n", this.hikes);  
        }  
    }  
  
}

您现在有了受保护的Web服务。 您可以发出GET请求,但无法发布或删除。 重新启动服务器,并使用以下HTTPie命令对此进行验证。

http :8080
HTTP/1.1 200 OK
...

Wonderland Trail
South Maroon Peak
Tour du Mont Blanc
Teton Crest Trail
Everest Base Camp via Cho La Pass
Kesugi Ridge
http -f POST :8080 hike="Pear Lake"
HTTP/1.1 403 Forbidden
...

{
    "error": "Forbidden",
    "message": "Forbidden",
    "path": "/",
    "status": 403,
    "timestamp": "2019-09-07T16:13:59.474+0000"
}

使用OIDC调试器生成JWT

要访问受保护的端点,您需要生成一个JWT。 为此,您可以使用OIDC调试器 您将需要先前创建的OIDC应用程序中的客户端ID,以及基本Okta URI(与Issuer URI中的基本URI相同)。

打开OIDC调试器

授权URI更新为: https://{yourOktaUri}/oauth2/default/v1/authorize

从OIDC应用程序将客户端ID更新为客户端ID。

在“ 状态”字段中放置一些内容。 就本教程而言,这可以是任何东西。 此值用于帮助防止跨站点伪造请求。

向下滚动并单击发送请求

将令牌复制到剪贴板,并将其存储在用于发出请求的shell窗口中的shell变量中。

TOKEN=eyJraWQiOiJIb05xb01mNE9jREltWnBGRnBINjZGTkFOM0J... 

现在尝试发布新的加息,然后将其删除。

http -f POST :8080 hike="Pear Lake" "Authorization: Bearer $TOKEN"
HTTP/1.1 200 OK
...

Wonderland Trail
South Maroon Peak
Tour du Mont Blanc
Teton Crest Trail
Everest Base Camp via Cho La Pass
Kesugi Ridge
Pear Lake
http DELETE :8080 hike=="South Maroon Peak" "Authorization: Bearer $TOKEN"
HTTP/1.1 200 OK
...

Wonderland Trail
Tour du Mont Blanc
Teton Crest Trail
Everest Base Camp via Cho La Pass
Kesugi Ridge
Pear Lake

您还配置了此应用程序以使用Spring Security的oauth2Login() 这意味着您可以转到http://localhost:8080/login ,单击发行者URL,然后也以这种方式登录。

了解有关Java,Spring Boot和Spring Security的更多信息

就是这样。 在本教程中,您了解了如何制作一个简单的Java servlet服务并使用Jetty运行它。 您还了解了如何在Spring Boot中重新创建相同的服务,如何将其配置为使用Jetty,以及简化Java代码。 最后,您了解了如何使用Okta提供的免费开发者帐户向您的Spring Boot应用程序添加OAuth / OIDC安全性。

您可以在oktadeveloper / okta-spring-boot-jetty-example上的GitHub上找到本教程的代码。

以下是一些相关的博客文章:

如果您对此帖子有任何疑问,请在下面添加评论。 有关更多精彩内容, 在Twitter上关注@oktadev在Facebook上关注我们,或订阅我们的YouTube频道

使用Okta的身份管理平台轻松部署您的应用程序 使用Okta的API在几分钟之内即可对任何应用程序中的用户进行身份验证,管理和保护。 今天尝试Okta。

翻译自: https://www.javacodegeeks.com/2019/12/get-started-with-jetty-java-and-oauth.html