使用Quarkus构建Java REST API

Build a Java REST API With Quarkus

Java REST API

Quarkus被设计为容器优先的框架,针对高速,低内存使用和出色的可扩展性进行了优化。 容器优先策略通过将运行时环境和应用程序代码绑定在一起,从而避免了单片服务器系统可能伴随的配置问题和无数更新。

最初,创建Quarkus是为了支持Graal / SubstrateVM的本机代码,但它也可以与JVM和OpenJDK HotSpot一起使用。 Quarkus支持许多行业标准的库,例如Hibernate,Kubernetes,RESTEasy和Eclipse MicroProfile。

创建Quarkus是为了在微服务和无服务器环境以及反应式编程模型中使用。 它使用JAX-RS作为REST端点,使用JPA保留数据模型,并使用cdI进行依赖项注入。

通过这篇文章,您将学习如何使用Java和Quarkus使用JAX-RS创建REST API,以及如何使用OAuth 2.0和Okta保护它。

本教程是Quarkus网站上" Quarkus-Using JWT RBAC"教程的修改和更新版本。 主要区别在于,本教程将使用Okta作为OAuth提供程序和OIDC调试器来生成用于临时测试的令牌(而不是自己滚动整个过程)。

让我们开始吧!

安装Quarkus教程前提条件

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

Java 11 :该项目使用Java11。OpenJDK11也将正常工作。 有关说明,请参见OpenJDK网站。 也可以使用Homebrew安装OpenJDK。 另外,SDKMAN是用于安装和管理Java版本的另一个不错的选择。

提示一下,如果您运行mvn -v,您将看到Maven版本以及运行Maven的Java版本。

在我的计算机(Mac)上,我能够使用以下命令将运行Maven的外壳设置为Java 11:export JAVA_HOME=$(/usr/libexec/java_home -v 11)

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

Okta开发人员帐户:您将使用Okta作为OAuth / OIDC提供程序,以向应用程序添加JWT身份验证和授权。 转到我们的开发人员网站并注册一个免费的开发人员帐户。

创建一个Java Quarkus项目

打开一个终端,然后cd到项目的相应父目录。 下面的命令使用quarkus-maven-plugin创建启动程序,并将其放置在oauthdemo子目录中。

1
2
3
4
5
6
mvn io.quarkus:quarkus-maven-plugin:0.23.1:create \
    -DprojectGroupId=com.okta.quarkus \
    -DprojectArtifactId=oauthdemo \
    -DclassName="com.okta.quarkus.jwt.TokenSecuredResource" \
    -Dpath="/secured" \
    -Dextensions="resteasy-jsonb, jwt"

如果此时运行项目,则会出现错误,因为您需要首先定义一些应用程序属性。

配置Quarkus应用程序属性

打开src/main/resources/application.properties文件,然后将以下内容复制并粘贴到其中。

1
2
3
4
mp.jwt.verify.publickey.location=https://{yourOktaDomain}/oauth2/default/v1/keys
mp.jwt.verify.issuer=https://{yourOktaDomain}/oauth2/default
quarkus.smallrye-jwt.auth-mechanism=MP-JWT
quarkus.smallrye-jwt.enabled=true

您需要在两个地方填写Okta开发人员URI。

要找到您的开发人员URI,请打开Okta开发人员仪表板,然后导航至 API > 授权服务器。 查看default身份验证服务器的行,您将在其中看到颁发者URI

该域是您需要填充的Okta URI(代替{yourOktaDomain})。

测试默认的Quarkus端点

导航到项目目录:cd oauthdemo

运行项目:

1
./mvnw compile quarkus:dev

您应该看到如下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< com.okta.quarkus:oauthdemo >---------------------
[INFO] Building oauthdemo 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
...
Listening for transport dt_socket at address: 5005
2019-09-30 10:39:02,186 INFO  [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation
2019-09-30 10:39:02,889 INFO  [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 703ms
2019-09-30 10:39:03,266 INFO  [io.quarkus] (main) Quarkus 0.23.1 started in 1.195s. Listening on: http://0.0.0.0:8080
2019-09-30 10:39:03,268 INFO  [io.quarkus] (main) Profile dev activated. Live Coding activated.
2019-09-30 10:39:03,268 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy, resteasy-jsonb, security, smallrye-jwt]

如果出现错误,请首先检查正在使用哪个版本的Java Maven。

1
./mvnw -v

现在,在另一个终端窗口中,使用HTTPie测试生成的端点:

1
2
3
4
5
6
7
8
$ http :8080/secured

HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 5
Content-Type: text/plain;charset=UTF-8

hello

真可爱! 但是我们可以做得更好。

src/main/java/com/okta/quarkus/jwt/TokenSecuredResource.java文件替换为以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.okta.quarkus.jwt;  

import java.security.Principal;  

import javax.annotation.security.PermitAll;  
import javax.enterprise.context.RequestScoped;  
import javax.inject.Inject;  
import javax.ws.rs.GET;  
import javax.ws.rs.Path;  
import javax.ws.rs.Produces;  
import javax.ws.rs.core.Context;  
import javax.ws.rs.core.MediaType;  
import javax.ws.rs.core.SecurityContext;  

import org.eclipse.microprofile.jwt.JsonWebToken;  

/**  
 * Version 1 of the TokenSecuredResource
 */

@Path("/secured")  
@RequestScoped  
public class TokenSecuredResource {  

    @Inject  
    JsonWebToken jwt;  

    @GET()  
    @Path("/permit-all")  
    @PermitAll  
    @Produces(MediaType.TEXT_PLAIN)  
    public String hello(@Context SecurityContext ctx) {  
        Principal caller =  ctx.getUserPrincipal();  
        String name = caller == null ?"anonymous" : caller.getName();  
        boolean hasJWT = jwt != null;  
        String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s, hasJWT: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJWT);  
        return helloReply;  
    }  

}

现在,保存文件并测试新的,更新的端点:

1
2
3
4
5
6
$ http :8080/secured/permit-all

HTTP/1.1 200 OK
...

hello + anonymous, isSecure: false, authScheme: null, hasJWT: true

您是否注意到您无需重新启动或重新编译应用程序即可使新端点正常工作? 这是Quarkus最漂亮的功能之一!

接下来,我们将向应用程序添加OAuth 2.0支持。

在Okta中创建OIDC应用程序以测试您的Quarkus服务

转至Okta开发人员仪表板-如果这是您首次登录,则可能需要单击 Admin 按钮。

在顶部菜单中,单击应用程序按钮,然后单击添加应用程序

选择应用程序类型网络,然后单击下一步

为应用命名。 我将其命名为" Quarkus Demo"。

登录重定向URIs 下,添加一个新URI:https://oidcdebugger.com/debug

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

其余的默认值将起作用。

点击完成

让页面保持打开状态或记录客户ID 。 生成令牌时需要一点时间。

更新令牌安全资源

现在更新TokenSecuredResource类以执行以下两项操作:

1)使用cdI依赖项注入从JWT注入groups声明(如果有)。

2)为受OAuth 2.0保护且需要Everyone组访问的/secured路径添加默认端点。

TokenSecuredResource.java更改为以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.okta.quarkus.jwt;  

import java.security.Principal;  
import java.util.Set;  

import javax.annotation.security.PermitAll;  
import javax.annotation.security.RolesAllowed;  
import javax.enterprise.context.RequestScoped;  
import javax.inject.Inject;  
import javax.ws.rs.GET;  
import javax.ws.rs.Path;  
import javax.ws.rs.Produces;  
import javax.ws.rs.core.Context;  
import javax.ws.rs.core.MediaType;  
import javax.ws.rs.core.SecurityContext;  

import org.eclipse.microprofile.jwt.Claim;  
import org.eclipse.microprofile.jwt.JsonWebToken;  

/**  
 * Version 1 of the TokenSecuredResource */

@Path("/secured")  
@RequestScoped  
public class TokenSecuredResource {  

    @Inject  
    JsonWebToken jwt;  

    @Inject  
    @Claim("groups")  
    private Set<String> groups;  

    @GET()  
    @Path("permit-all")  
    @PermitAll  
    @Produces(MediaType.TEXT_PLAIN)  
    public String hello(@Context SecurityContext ctx) {  
        Principal caller =  ctx.getUserPrincipal();  
        String name = caller == null ?"anonymous" : caller.getName();  
        boolean hasJWT = jwt != null;  
        String groupsString = groups != null ? groups.toString() :"";  
        String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s, hasJWT: %s, groups: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJWT, groupsString);  
        return helloReply;  
    }  

    @GET()  
    @Path("/")  
    @RolesAllowed({"Everyone"})  
    @Produces(MediaType.TEXT_PLAIN)  
    public String helloRolesAllowed(@Context SecurityContext ctx) {  
        Principal caller =  ctx.getUserPrincipal();  
        String name = caller == null ?"anonymous" : caller.getName();  
        boolean hasJWT = jwt != null;  
        String groupsString = groups != null ? groups.toString() :"";  
        String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s, hasJWT: %s, groups: %s"", name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJWT, groupsString);  
        return helloReply;  
    }  
}

试用新的默认端点:

1
2
3
4
5
6
$ http :8080/secured

HTTP/1.1 401 Unauthorized
...

Not authorized

生成OAuth 2.0访问令牌以测试Quarkus中的身份验证

打开OpenID Connect调试器。 您将使用此页面生成可用于身份验证的JWT访问令牌。

OIDC Debugger

请按照以下步骤继续:

  • 授权URI 设置为:https://{yourOktaDomain}/oauth2/default/v1/authorize

  • 从上面创建的Okta OIDC应用程序中复制您的客户ID ,并在客户ID 下填写

  • 将范围更改为openid email profile

  • 添加一些内容。 没关系。 不能空白

  • 向下滚动。 点击发送

  • 将生成的JWT访问令牌复制到剪贴板,然后在运行HTTPie命令的终端中,将令牌值保存到shell变量中,如下所示:

  • 1
    TOKEN=eyJraWQiOiJxMm5rZmtwUDRhMlJLV2REU2JfQ...

    使用受保护的Quarkus端点测试JWT

    现在,您已经从OAuth提供者(Okta)获得了有效的JWT,则应该可以使用此JWT对受保护的端点进行身份验证。

    试试看:

    1
    http :8080/secured"Authorization: Bearer $TOKEN"

    您应该看到类似以下内容:

    1
    2
    3
    4
    5
    6
    HTTP/1.1 200 OK
    Connection: keep-alive
    Content-Length: 123
    Content-Type: text/plain;charset=UTF-8

    hello + andrew@gmail.com, isSecure: false, authScheme: MP-JWT, hasJWT: true, groups: [Everyone, Admin]"

    向Quarkus REST端点添加功能

    建立更现实的REST资源的第一步是创建数据模型类。 为此,您可以使用一个名为Lombok的帮助程序项目(更多信息请参见其网站)。

    要使用Lombok,请将以下依赖项添加到您的pom.xml中。

    1
    2
    3
    4
    5
    6
     <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.8</version>
        <scope>provided</scope>
    </dependency>

    注意::如果您使用的是IDE(而非命令行)来构建项目,请为其安装Lombok插件。 例如,请参阅Lombok IntelliJ插件。

    您将只使用Lombok的一小部分@Data注释,以节省在数据模型类中编写样板代码的时间(如果需要,请查看注释文档)。 它生成getter,setter,equals()hashCode()方法。

    继续创建一个Java类:src/main/java/com/okta/quarkus/jwt/Kayak.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package com.okta.quarkus.jwt;  

    import lombok.Data;  

    import java.util.Objects;  

    @Data  
    public class Kayak {  

        private String make;
        private String model;
        private Integer length;  

        public Kayak() {  
        }  

        public Kayak(String make, String model, Integer length) {  
            this.make = make;  
            this.model = model;  
            this.length = length;  
        }  

    }

    您可能现在已经猜到了,新的REST端点将要管理皮划艇列表。

    该代码是纯JAX-RS,并不特定于Quarkus或Kubernetes。 JAX-RS是用于Restful Web Services的Java API,这是用于配置REST服务的基于注释的规范。 因为这只是一个规范,所以实际的实现由Quarkus堆栈提供。

    现在,您需要创建REST端点资源:src/main/java/com/okta/quarkus/jwt/KayakResource.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    package com.okta.quarkus.jwt;  

    import java.util.Collections;  
    import java.util.LinkedHashMap;  
    import java.util.Set;  

    import javax.annotation.security.RolesAllowed;  
    import javax.ws.rs.Consumes;  
    import javax.ws.rs.DELETE;  
    import javax.ws.rs.GET;  
    import javax.ws.rs.POST;  
    import javax.ws.rs.Path;  
    import javax.ws.rs.Produces;  
    import javax.ws.rs.core.MediaType;  

    @Path("/kayaks")  
    @Produces(MediaType.APPLICATION_JSON)  
    @Consumes(MediaType.APPLICATION_JSON)  
    public class KayakResource {  

        private Set<Kayak> kayaks = Collections.newSetFromMap(Collections.synchronizedMap(new LinkedHashMap<>()));  

        public KayakResource() {  
            kayaks.add(new Kayak("NDK","Romany", 17));  
            kayaks.add(new Kayak("NDK","Surf", 16));  
            kayaks.add(new Kayak("P&H","Scorpio HV", 15));  
        }  

        @GET  
        public Set<Kayak> list() {  
            return kayaks;  
        }  

        @RolesAllowed({"Everyone"})  
        @POST  
        public Set<Kayak> add(Kayak kayak) {  
            kayaks.add(kayak);  
            return kayaks;  
        }  

        @RolesAllowed({"Everyone"})  
        @DELETE  
        public Set<Kayak> delete(Kayak kayak) {  
            kayaks.remove(kayak);  
            return kayaks;  
        }  
    }

    我在这里不做过多介绍,但是我想指出一些事情。 首先,注意@Produces@Consumes批注。 Quarkus文档指出,将它们包括进来非常重要,因为它们被用来优化最终版本。 该端点使用由常量MediaType.APPLICATION_JSON指定的JSON。

    另外,请注意,GET端点是公共的,但是POST和DELETE端点需要Everyone组的成员身份。 不要将Everyone与匿名混淆。 Everyone是一个通用的默认组,分配给在Okta OIDC应用程序上进行身份验证的任何人。 在这种情况下,这意味着用户已经过身份验证,但不一定是任何其他组的一部分,例如Admin

    测试您的Quarkus端点并添加新的皮划艇

    由于您已向pom.xml添加了新的依赖关系,因此需要重新启动运行服务器的Maven进程。

    然后,在没有令牌的情况下测试POST端点,以验证其是否受到保护。

    1
    2
    3
    4
    5
    6
    $ http POST :8080/kayaks make="P&H" model="Cetus HV" length=18

    HTTP/1.1 401 Unauthorized
    ...

    Not authorized

    现在,再次尝试使用令牌。 您会看到一个新的皮划艇已添加到列表中!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    $ http POST :8080/kayaks make="P&H" model="Cetus HV" length=18"Authorization: Bearer $TOKEN"

    HTTP/1.1 200 OK
    Connection: keep-alive
    ...

    [
        {
           "length": 17,
           "make":"NDK",
           "model":"Romany"
        },
        {
           "length": 16,
           "make":"NDK",
           "model":"Surf"
        },
        {
           "length": 15,
           "make":"P&H",
           "model":"Scorpio HV"
        },
        {
           "length": 18,
           "make":"P&H",
           "model":"Cetus HV"
        }
    ]

    您可以使用以下命令删除新添加的皮划艇:

    1
    http DELETE :8080/kayaks make="P&H" model="Cetus HV" length=18"Authorization: Bearer $TOKEN"

    您可能会注意到此服务中没有PUT(更新)。 在更完善的服务中,每个记录将具有与之关联的某种类型的唯一ID。 这将允许客户端应用程序指定用于更新和删除的特定记录(而不是使用记录属性本身和equals()方法)。

    同样,很显然,该资源非常幼稚,将数据存储在class属性中。 在实际的应用程序中,可以使用JPA批注将数据模型映射到数据库,以方便序列化和反序列化。

    了解有关Java,Quarkus和令牌认证的更多信息

    全做完了! 在本教程中,您使用Quarkus和Java创建了一个简单的REST服务,并使用Okta作为OAuth / OIDC提供程序通过JWT OAuth进行了保护。 您还了解了如何使用cdI依赖项注入来检查JWT声明并检索有关已认证(或未认证)客户端的信息。 最后,您尝试了RBAC(基于角色的身份验证)的一些基础知识。

    提醒一下,本教程的灵感来自Quarkus的文章:使用JWT RBAC。

    Quarkus在其网站上还有大量其他出色的指南。

    您可以在oktadeveloper / okta-quarkus-example中找到本教程的源代码。

    以下是一些相关的博客文章,以了解有关Java和身份验证的更多信息:

  • Java应用程序的简单令牌认证

  • 在15分钟内使用Spring Boot和Spring Security构建一个Web应用程序

  • 创建一个安全的Spring REST API

  • 使用Spring Boot和Vue.js构建一个简单的CRUD应用

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

    如何使用Java和OIDC身份验证开发Quarkus应用最初于2019年9月30日发布在Okta开发者博客上。