您好,欢迎来到品趣旅游知识分享网。
搜索
您的当前位置:首页使用Java EE和OIDC构建Java REST API

使用Java EE和OIDC构建Java REST API

来源:品趣旅游知识分享网

“我喜欢编写身份验证和授权代码。” 〜从来没有Java开发人员。 厌倦了一次又一次地建立相同的登录屏幕?

Java EE允许您使用JAX-RS和JPA快速轻松地构建Java REST API。 Java EE是保护伞标准规范,它描述了许多Java技术,包括EJB,JPA,JAX-RS和许多其他技术。 它最初旨在允许Java应用程序服务器之间的可移植性,并在2000年代初期蓬勃发展。 那时,应用服务器非常流行,并且由许多知名公司(例如IBM,BEA和Sun)提供。 JBoss是一家新兴公司,它破坏了现状,并表明可以将Java EE应用程序服务器开发为一个开源项目,并免费提供它。 JBoss在2006年被RedHat收购。

在2000年代初期,Java开发人员使用servlet和EJB来开发其服务器应用程序。 Hibernate和Spring分别于2002年和2004年问世。 两种技术都对各地的Java开发人员产生了巨大的影响,这表明他们有可能在没有EJB的情况下编写分布式,健壮的应用程序。 Hibernate的POJO模型最终被用作JPA标准,并且对EJB的影响也很大。

快进到2018年,Java EE肯定不像以前那样! 现在,它主要是POJO和注释,并且使用起来更简单。

为什么要使用Java EE而不是Spring Boot构建Java REST API?

Spring Boot是Java生态系统中我最喜欢的技术之一。 它极大地减少了Spring应用程序中必需的配置,并使得仅用几行代码即可生成REST API。 但是,最近有一些不使用Spring Boot的开发人员提出了很多API安全性问题。 其中一些甚至没有使用Spring!

基于这个原因,我认为构建一个Java REST API(使用Java EE)很有趣,该API与我过去开发的Spring Boot REST API相同。 即,我的和帖子中的“啤酒” API。

使用Java EE构建Java REST API

首先,我是否存在诸如start.spring.io之类的Java EE快速入门。 我收到了一些建议,并开始进行一些研究。 建议我看一下 ,所以我从那里开始。 我还研究了推荐的 。

我喜欢jaxrs-starter项目,因为它展示了如何使用JAX-RS创建REST API。 TomEE Maven原型也很有用,特别是因为它展示了如何使用JPA,H2和JSF。 我将两者结合起来,创建了自己的最小启动器,可用于在TomEE上实现安全的Java EE API。 您不必在这些示例中使用TomEE,但我尚未在其他实现上对其进行测试。

如果您在其他应用服务器上使用了这些示例,请告诉我,我将更新此博客文章。

在这些示例中,我将使用Java 8和Java EE 7.0以及TomEE 7.1.0。 TomEE 7.x是EE 7兼容版本; 有一个TomEE 8.x分支用于EE8兼容性工作,但尚无发行版本。 我希望您也安装了 。

首先,将我们的Java EE REST API存储库克隆到您的硬盘驱动器,然后运行它:

git clone https://github.com/oktadeveloper/okta-java-ee-rest-api-example.git javaee-rest-api
cd javaee-rest-api
mvn package tomee:run

导航到http:// localhost:8080并添加新啤酒。

单击添加 ,您应该看到成功消息。

单击查看存在的啤酒查看啤酒的完整列表。


您还可以在http://localhost:8080/good-beers查看系统中的优质啤酒列表。 以下是使用时的输出。

$ http :8080/good-beers
HTTP/1.1 200
Content-Type: application/json
Date: Wed, 29 Aug 2018 21:58:23 GMT
Server: Apache TomEE
Transfer-Encoding: chunked
[
    {
        "id": 101,
        "name": "Kentucky Brunch Brand Stout"
    },
    {
        "id": 102,
        "name": "Marshmallow Handjee"
    },
    {
        "id": 103,
        "name": "Barrel-Aged Abraxas"
    },
    {
        "id": 104,
        "name": "Heady Topper"
    },
    {
        "id": 108,
        "name": "White Rascal"
    }
]

使用Java EE构建REST API

$ tree .
.
├── LICENSE
├── README.md
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── okta
    │   │           └── developer
    │   │               ├── Beer.java
    │   │               ├── BeerBean.java
    │   │               ├── BeerResource.java
    │   │               ├── BeerService.java
    │   │               └── StartupBean.java
    │   ├── resources
    │   │   └── META-INF
    │   │       └── persistence.xml
    │   └── webapp
    │       ├── WEB-INF
    │       │   ├── beans.xml
    │       │   └── faces-config.xml
    │       ├── beer.xhtml
    │       ├── index.jsp
    │       └── result.xhtml
    └── test
        └── resources
            └── arquillian.xml

12 directories, 16 files

最重要的XML文件是pom.xml ,它定义了依赖关系,并允许您运行TomEE Maven插件。 它非常简短,可爱,只有一个依赖项和一个插件。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.okta.developer</groupId>
    <artifactId>java-ee-rest-api</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>Java EE Webapp with JAX-RS API</name>
    <url>http://developer.okta.com</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>
        <failOnMissingWebXml>false</failOnMissingWebXml>
        <javaee-api.version>7.0</javaee-api.version>
        <tomee.version>7.1.0</tomee.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>${javaee-api.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.tomee.maven</groupId>
                <artifactId>tomee-maven-plugin</artifactId>
                <version>${tomee.version}</version>
                <configuration>
                    <context>ROOT</context>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

主要实体是Beer.java

package com.okta.developer;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Beer {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String name;

    public Beer() {}

    public Beer(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String beerName) {
        this.name = beerName;
    }

    @Override
    public String toString() {
        return "Beer{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

数据库(aka,数据源)在src/main/resources/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
    <persistence-unit name="beer-pu" transaction-type="JTA">
        <jta-data-source>beerDatabase</jta-data-source>
        <class>com.okta.developer.Beer</class>
        <properties>
            <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema(ForeignKeys=true)"/>
        </properties>
    </persistence-unit>
</persistence>

BeerService.java类使用JPA的EntityManager处理该实体的读取并将其保存到数据库中。

package com.okta.developer;

import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.persistence.criteria.CriteriaQuery;
import java.util.List;

@Stateless
public class BeerService {

    @PersistenceContext(unitName = "beer-pu")
    private EntityManager entityManager;

    public void addBeer(Beer beer) {
        entityManager.persist(beer);
    }

    public List<Beer> getAllBeers() {
        CriteriaQuery<Beer> cq = entityManager.getCriteriaBuilder().createQuery(Beer.class);
        cq.select(cq.from(Beer.class));
        return entityManager.createQuery(cq).getResultList();
    }

    public void clear() {
        Query removeAll = entityManager.createQuery("delete from Beer");
        removeAll.executeUpdate();
    }
}

有一个StartupBean.java ,用于在启动时填充数据库,并在关闭时清除数据库。

package com.okta.developer;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.inject.Inject;
import java.util.stream.Stream;

@Singleton
@Startup
public class StartupBean {
    private final BeerService beerService;

    @Inject
    public StartupBean(BeerService beerService) {
        this.beerService = beerService;
    }

    @PostConstruct
    private void startup() {
        // Top beers from https://www.beeradvocate.com/lists/top/
        Stream.of("Kentucky Brunch Brand Stout", "Marshmallow Handjee", 
                "Barrel-Aged Abraxas", "Heady Topper",
                "Budweiser", "Coors Light", "PBR").forEach(name ->
                beerService.addBeer(new Beer(name))
        );
        beerService.getAllBeers().forEach(System.out::println);
    }

    @PreDestroy
    private void shutdown() {
        beerService.clear();
    }
}

这三个类构成了应用程序的基础,此外还有一个BeerResource.java类,它使用JAX-RS公开/good-beers端点。

package com.okta.developer;

import javax.ejb.Lock;
import javax.ejb.Singleton;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import java.util.List;
import java.util.stream.Collectors;

import static javax.ejb.LockType.READ;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;

@Lock(READ)
@Singleton
@Path("/good-beers")
public class BeerResource {
    private final BeerService beerService;

    @Inject
    public BeerResource(BeerService beerService) {
        this.beerService = beerService;
    }

    @GET
    @Produces({APPLICATION_JSON})
    public List<Beer> getGoodBeers() {
        return beerService.getAllBeers().stream()
                .filter(this::isGreat)
                .collect(Collectors.toList());
    }

    private boolean isGreat(Beer beer) {
        return !beer.getName().equals("Budweiser") &&
                !beer.getName().equals("Coors Light") &&
                !beer.getName().equals("PBR");
    }
}

最后,有一个BeerBean.java类用作JSF的托管bean。

package com.okta.developer;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.List;

@Named
@RequestScoped
public class BeerBean {

    @Inject
    private BeerService beerService;
    private List<Beer> beersAvailable;
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Beer> getBeersAvailable() {
        return beersAvailable;
    }

    public void setBeersAvailable(List<Beer> beersAvailable) {
        this.beersAvailable = beersAvailable;
    }

    public String fetchBeers() {
        beersAvailable = beerService.getAllBeers();
        return "success";
    }

    public String add() {
        Beer beer = new Beer();
        beer.setName(name);
        beerService.addBeer(beer);
        return "success";
    }
}

您现在拥有了使用Java EE构建的REST API! 但是,这并不安全。 在以下各节中,我将向您展示如何使用Okta的Java JWT验证程序,Spring Security和Pac4j对其进行保护。

使用Okta将OIDC安全性添加到Java REST API

您将需要在Okta中创建OIDC应用程序,以验证将要实施的安全配置。 要使此操作毫不费力,您可以使用Okta的OIDC API。 在Okta,我们的目标是使比您以往更加轻松,安全和可扩展。 Okta是一项云服务,允许开发人员创建,编辑和安全地存储用户帐户和用户帐户数据,并将它们与一个或多个应用程序连接。 我们的API使您能够:

  • 和用户
  • 存储有关您的用户的数据
  • 执行基于密码的
  • 通过保护您的应用程序
  • 以及更多! 查看我们的

你卖了吗 ! 完成后,请完成以下步骤以创建OIDC应用程序。

使用JWT Verifier保护Java REST API

要从Okta验证JWT,您需要将添加到pom.xml

<properties>
    ...
    <okta-jwt.version>0.3.0</okta-jwt.version>
</properties>

<dependencies>
    ...
    <dependency>
        <groupId>com.okta.jwt</groupId>
        <artifactId>okta-jwt-verifier</artifactId>
        <version>${okta-jwt.version}</version>
    </dependency>
</dependencies>

确保使用您创建的应用中的设置替换{yourOktaDomain}{clientId}

package com.okta.developer;

import com.nimbusds.oauth2.sdk.ParseException;
import com.okta.jwt.JoseException;
import com.okta.jwt.Jwt;
import com.okta.jwt.JwtHelper;
import com.okta.jwt.JwtVerifier;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter(filterName = "jwtFilter", urlPatterns = "/*")
public class JwtFilter implements Filter {
    private JwtVerifier jwtVerifier;

    @Override
    public void init(FilterConfig filterConfig) {
        try {
            jwtVerifier = new JwtHelper()
                    .setIssuerUrl("https://{yourOktaDomain}/oauth2/default")
                    .setClientId("{yourClientId}")
                    .build();
        } catch (IOException | ParseException e) {
            System.err.print("Configuring JWT Verifier failed!");
            e.printStackTrace();
        }
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        System.out.println("In JwtFilter, path: " + request.getRequestURI());

        // Get access token from authorization header
        String authHeader = request.getHeader("authorization");
        if (authHeader == null) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Access denied.");
            return;
        } else {
            String accessToken = authHeader.substring(authHeader.indexOf("Bearer ") + 7);
            try {
                Jwt jwt = jwtVerifier.decodeAccessToken(accessToken);
                System.out.println("Hello, " + jwt.getClaims().get("sub"));
            } catch (JoseException e) {
                e.printStackTrace();
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Access denied.");
                return;
            }
        }

        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
    }
}

为确保此过滤器正常工作,请重新启动您的应用并运行:

mvn package tomee:run

如果在浏览器中导航到http://localhost:8080/good-beers ,则会看到拒绝访问错误。

为了证明它可以与有效的JWT一起使用,您可以克隆我的Bootiful React项目,并运行其UI:

git clone -b okta https://github.com/oktadeveloper/spring-boot-react-example.git bootiful-react
cd bootiful-react/client
npm install

编辑此项目的client/src/App.tsx文件,并更改issuerclientId以匹配您的应用程序。

const config = {
  issuer: 'https://{yourOktaDomain}/oauth2/default',
  redirectUri: window.location.origin + '/implicit/callback',
  clientId: '{yourClientId}'
};

然后启动它:

npm start

然后,您应该能够使用创建帐户所用的凭据登录http://localhost:3000 。 但是,由于CORS错误(在浏览器的开发人员控制台中),您将无法从API加载任何啤酒。

Failed to load http://localhost:8080/good-beers: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access.

提示:如果看到401并且没有CORS错误,则可能意味着您的客户ID不匹配。

要解决此CORS错误,请在JwtFilter.java类旁边添加一个CorsFilter.java 。 下面的过滤器将允许OPTIONS请求,并向后发送访问控制标头,以允许任何起源,GET方法和任何标头。 我建议您在生产中使这些设置更加具体。

package com.okta.developer;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter(filterName = "corsFilter")
public class CorsFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        System.out.println("In CorsFilter, method: " + request.getMethod());

        // Authorize (allow) all domains to consume the content
        response.addHeader("Access-Control-Allow-Origin", "http://localhost:3000");
        response.addHeader("Access-Control-Allow-Methods", "GET");
        response.addHeader("Access-Control-Allow-Headers", "*");

        // For HTTP OPTIONS verb/method reply with ACCEPTED status code -- per CORS handshake
        if (request.getMethod().equals("OPTIONS")) {
            response.setStatus(HttpServletResponse.SC_ACCEPTED);
            return;
        }

        // pass the request along the filter chain
        chain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig config) {
    }

    @Override
    public void destroy() {
    }
}

您添加的两个过滤器都使用@WebFilter进行注册。 这是一个方便的注释,但不提供任何过滤器排序功能。 要解决此丢失的功能,请修改JwtFilter ,使其@WebFilter中没有urlPattern

@WebFilter(filterName = "jwtFilter")

然后创建一个src/main/webapp/WEB-INF/web.xml文件,并使用以下XML进行填充。 这些过滤器映射可确保CorsFilter处理CorsFilter

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1"
         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">

    <filter-mapping>
        <filter-name>corsFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

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

重新启动Java API,现在一切正常!

在控制台中,您应该看到类似于我的消息:

In CorsFilter, method: OPTIONS
In CorsFilter, method: GET
In JwtFilter, path: /good-beers
Hello, demo@okta.com

通过Okta的JWT验证程序使用过滤器是实现资源服务器的一种简单方法(采用OAuth 2.0命名法)。 但是,它不向您提供有关该用户的任何信息。 JwtVerifier接口的确有一个decodeIdToken(String idToken, String nonce)方法,但是您必须从客户端传递ID令牌才能使用它。

在接下来的两节中,我将向您展示如何使用Spring Security和Pac4j来实现类似的安全性。 作为奖励,我将向您展示如何提示用户登录(当他们尝试直接访问API时)并获取用户的信息。

通过Spring Security保护Java REST API

Spring Security是我在Javaland中最喜欢的框架之一。 在显示如何使用Spring Security时,此博客上的大多数示例都使用Spring Boot。 我将使用最新版本– 5.1.0.RC2 –因此本教程将保持最新状态。

还原更改以添加JWT Verifier,或直接删除web.xml继续。

修改您的pom.xml使其具有Spring Security所需的依赖关系。 您还需要添加Spring的快照存储库以获取候选版本。

<properties>
    ...
    <spring-security.version>5.1.0.RC2</spring-security.version>
    <spring.version>5.1.0.RC3</spring.version>
    <jackson.version>2.9.6</jackson.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-framework-bom</artifactId>
            <version>${spring.version}</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-bom</artifactId>
            <version>${spring-security.version}</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    ...
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-resource-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-jose</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>${jackson.version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>${jackson.version}</version>
    </dependency>
</dependencies>

<pluginRepositories>
    <pluginRepository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/libs-snapshot</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </pluginRepository>
</pluginRepositories>
<repositories>
    <repository>
        <id>spring-snapshots</id>
        <name>Spring Snapshot</name>
        <url>https://repo.spring.io/libs-snapshot</url>
    </repository>
</repositories>

src/main/java/com/okta/developer创建一个SecurityWebApplicationInitializer.java类:

package com.okta.developer;

import org.springframework.security.web.context.*;

public class SecurityWebApplicationInitializer
   extends AbstractSecurityWebApplicationInitializer {

   public SecurityWebApplicationInitializer() {
       super(SecurityConfiguration.class);
   }
}
package com.okta.developer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

@Configuration
@EnableWebSecurity
@PropertySource("classpath:application.properties")
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    private final String clientSecret;
    private final String clientId;
    private final String issuerUri;

    @Autowired
    public SecurityConfiguration(@Value("${okta.issuer-uri}") String issuerUri,
            @Value("${okta.client-id}") String clientId,
            @Value("${okta.client-secret}") String clientSecret) {
        this.issuerUri = issuerUri;
        this.clientId = clientId;
        this.clientSecret = clientSecret;
    }

@Override
   protected void configure(HttpSecurity http) throws Exception {
      http
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
                .and()
           .csrf()
               .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
               .and()
           .authorizeRequests()
               .anyRequest().authenticated()
               .and()
           .oauth2Login();
   }

   @Bean
   public OAuth2AuthorizedClientService authorizedClientService() {
       return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository());
   }

   @Bean
   public ClientRegistrationRepository clientRegistrationRepository() {
       List<ClientRegistration> registrations = clients.stream()
               .map(this::getRegistration)
               .filter(Objects::nonNull)
               .collect(Collectors.toList());

       return new InMemoryClientRegistrationRepository(registrations);
   }

   @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        ClientRegistration okta = getRegistration();
        return new InMemoryClientRegistrationRepository(okta);
    }

    ClientRegistrations.fromOidcIssuerLocation(Objects.requireNonNull(issuerUri))
            .registrationId("okta")
            .clientId(clientId)
            .clientSecret(clientSecret)
            .build();
}

创建src/main/resources/application.properties并用Okta OIDC应用设置进行填充。

okta.client-id={clientId}
okta.client-secret={clientSecret}
okta.issuer-uri=https://{yourOktaDomain}/oauth2/default

感谢提供的 。

因为启用了CSRF,所以必须在任何<h:form>标记内添加以下隐藏字段以保护CSRF。 我将以下内容添加到src/main/webapp/beer.xhtmlresult.xhtml

<input type="hidden" value="${_csrf.token}" name="${_csrf.parameterName}"/>

重新启动您的API( mvn clean package tomee:run )并导航到http://localhost:8080/good-beers 。 您应该重定向到Okta进行登录。

输入有效的凭证,您应该在浏览器中看到JSON。 提供了美观的JSON。

要求用户登录以查看您的API数据很方便,但是最好将其作为React UI示例的资源服务器。 OAuth 2.0资源服务器支持是新增功能,因此我将向您展示如何使用它。

用以下代码替换SecurityConfiguration.javaconfigure()方法,该代码启用CORS并设置资源服务器。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
            .and()
        .csrf()
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .and()
        .cors()
            .and()
        .authorizeRequests()
            .anyRequest().authenticated()
            .and()
        .oauth2Login()
            .and()
        .oauth2ResourceServer()
            .jwt();
}

@Bean
JwtDecoder jwtDecoder() {
    return JwtDecoders.fromOidcIssuerLocation(this.issuerUri);
}

@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowCredentials(true);
    configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
    configuration.setAllowedMethods(Collections.singletonList("GET"));
    configuration.setAllowedHeaders(Collections.singletonList("*"));
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

进行这些更改之后,重新启动您的API并确认您的React UI可以与之对话。 很漂亮吧?

Spring Security的用户信息

Spring Security与Servlet API集成在一起,因此您可以使用以下方法来获取当前用户的信息。

  • HttpServletRequest.getRemoteUser()
  • HttpServletRequest.getUserPrincipal()

拥有Principal ,您可以获取有关用户的详细信息,包括其角色(又名,权限)。

OAuth2Authentication authentication = (OAuth2Authentication) principal;
Map<String, Object> user = (Map<String, Object>) authentication.getUserAuthentication().getDetails();

请参阅以获取更多信息。

使用Pac4j锁定Java REST API

我想向您展示的确保Java REST API安全的最后一种技术是使用Pac4j,特别是 。

恢复您的更改以添加Spring Security。

git reset --hard HEAD

编辑pom.xml以添加完成本节所需的Pac4j库。

<properties>
    ...
    <pac4j-j2e.version>4.0.0</pac4j-j2e.version>
    <pac4j.version>3.0.0</pac4j.version>
</properties>

<dependencies>
    ...
    <dependency>
        <groupId>org.pac4j</groupId>
        <artifactId>j2e-pac4j</artifactId>
        <version>${pac4j-j2e.version}</version>
    </dependency>
    <dependency>
        <groupId>org.pac4j</groupId>
        <artifactId>pac4j-oidc</artifactId>
        <version>${pac4j.version}</version>
    </dependency>
    <dependency>
        <groupId>org.pac4j</groupId>
        <artifactId>pac4j-http</artifactId>
        <version>${pac4j.version}</version>
    </dependency>
    <dependency>
        <groupId>org.pac4j</groupId>
        <artifactId>pac4j-jwt</artifactId>
        <version>${pac4j.version}</version>
    </dependency>
</dependencies>

就像创建JWT Verifier一样,创建一个src/main/java/com/okta/developer/CorsFilter.java

package com.okta.developer;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter(filterName = "corsFilter")
public class CorsFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        System.out.println("In CorsFilter, method: " + request.getMethod());

        // Authorize (allow) all domains to consume the content
        response.addHeader("Access-Control-Allow-Origin", "http://localhost:3000");
        response.addHeader("Access-Control-Allow-Methods", "GET");
        response.addHeader("Access-Control-Allow-Headers", "*");

        // For HTTP OPTIONS verb/method reply with ACCEPTED status code -- per CORS handshake
        if (request.getMethod().equals("OPTIONS")) {
            response.setStatus(HttpServletResponse.SC_ACCEPTED);
            return;
        }

        // pass the request along the filter chain
        chain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig config) {
    }

    @Override
    public void destroy() {
    }
}

在同一程序包中创建一个SecurityConfigFactory.java 。 将客户端ID,密钥和域占位符替换为与OIDC应用程序匹配的占位符。

package com.okta.developer;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.pac4j.core.client.Clients;
import org.pac4j.core.client.direct.AnonymousClient;
import org.pac4j.core.config.Config;
import org.pac4j.core.config.ConfigFactory;
import org.pac4j.core.credentials.TokenCredentials;
import org.pac4j.core.profile.CommonProfile;
import org.pac4j.http.client.direct.HeaderClient;
import org.pac4j.jwt.config.signature.RSASignatureConfiguration;
import org.pac4j.jwt.credentials.authenticator.JwtAuthenticator;
import org.pac4j.jwt.util.JWKHelper;
import org.pac4j.oidc.client.OidcClient;
import org.pac4j.oidc.config.OidcConfiguration;
import org.pac4j.oidc.profile.OidcProfile;

import java.io.IOException;
import java.net.URL;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class SecurityConfigFactory implements ConfigFactory {
    private final JwtAuthenticator jwtAuthenticator = new JwtAuthenticator();
    private final ObjectMapper mapper = new ObjectMapper();

    @Override
    public Config build(final Object... parameters) {
        System.out.print("Building Security configuration...\n");

        final OidcConfiguration oidcConfiguration = new OidcConfiguration();
        oidcConfiguration.setClientId("{yourClientId}");
        oidcConfiguration.setSecret("{yourClientSecret}");
        oidcConfiguration.setDiscoveryURI("https://{yourOktaDomain}/oauth2/default/.well-known/openid-configuration");
        oidcConfiguration.setUseNonce(true);
        final OidcClient<OidcProfile, OidcConfiguration> oidcClient = new OidcClient<>(oidcConfiguration);
        oidcClient.setAuthorizationGenerator((ctx, profile) -> {
            profile.addRole("ROLE_USER");
            return profile;
        });

        HeaderClient headerClient = new HeaderClient("Authorization", "Bearer ", (credentials, ctx) -> {
            String token = ((TokenCredentials) credentials).getToken();
            if (token != null) {
                try {
                    // Get JWK
                    URL keysUrl = new URL("https://{yourOktaDomain}/oauth2/default/v1/keys");
                    Map map = mapper.readValue(keysUrl, Map.class);
                    List keys = (ArrayList) map.get("keys");
                    String json = mapper.writeValueAsString(keys.get(0));

                    // Build key pair and validate token
                    KeyPair rsaKeyPair = JWKHelper.buildRSAKeyPairFromJwk(json);
                    jwtAuthenticator.addSignatureConfiguration(new RSASignatureConfiguration(rsaKeyPair));
                    CommonProfile profile = jwtAuthenticator.validateToken(token);
                    credentials.setUserProfile(profile);
                    System.out.println("Hello, " + profile.getId());
                } catch (IOException e) {
                    System.err.println("Failed to validate Bearer token: " + e.getMessage());
                    e.printStackTrace();
                }
            }
        });

        final Clients clients = new Clients("http://localhost:8080/callback",
                oidcClient, headerClient, new AnonymousClient());
        return new Config(clients);
    }
}

如果oidcClient的代码中的oidcClient尝试直接访问您的API,将使用户登录Okta。 headerClient设置了资源服务器,该资源服务器根据用户的访问令牌对用户进行授权。

创建src/main/webapp/WEB-INF/web.xml来映射CorsFilter以及Pac4j的CallbackFilterSecurityFilter 。 您可以看到SecurityFilter通过其configFactory init-param链接到SecurityConfigFactory类。

<?xml version="1.0" encoding="UTF-8"?>
<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>javaee-pac4j-demo</display-name>

    <absolute-ordering/>

    <filter-mapping>
        <filter-name>corsFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter>
        <filter-name>callbackFilter</filter-name>
        <filter-class>org.pac4j.j2e.filter.CallbackFilter</filter-class>
        <init-param>
            <param-name>defaultUrl</param-name>
            <param-value>/</param-value>
        </init-param>
        <init-param>
            <param-name>renewSession</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <param-name>multiProfile</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>callbackFilter</filter-name>
        <url-pattern>/callback</url-pattern>
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>

    <filter>
        <filter-name>OidcFilter</filter-name>
        <filter-class>org.pac4j.j2e.filter.SecurityFilter</filter-class>
        <init-param>
            <param-name>configFactory</param-name>
            <param-value>com.okta.developer.SecurityConfigFactory</param-value>
        </init-param>
        <init-param>
            <param-name>clients</param-name>
            <param-value>oidcClient,headerClient</param-value>
        </init-param>
        <init-param>
            <param-name>authorizers</param-name>
            <param-value>securityHeaders</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>OidcFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
    </filter-mapping>
</web-app>

为了更好地可视化用户信息,您需要创建更多文件。 这些与JSF相关的文件是从复制的。

注意:我试图在 (没有web.xml ),但是失败并出现错误: Filters cannot be added to context [] as the context has been initialised ,因此无法将Filters cannot be added to context [] as the context has been initialised 。 当使用时,它确实起作用。

创建src/main/java/com/okta/developer/ProfileView.java ,这是一个JSF托管的bean,用于收集用户的信息。

package com.okta.developer;

import org.pac4j.core.context.WebContext;
import org.pac4j.core.profile.ProfileManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.List;

/**
 * Managed bean which exposes the pac4j profile manager.
 *
 * JSF views such as facelets can reference this to view the contents of profiles.
 *
 * @author Phillip Ross
 */
@Named
@RequestScoped
public class ProfileView {

    /** The static logger instance. */
    private static final Logger logger = LoggerFactory.getLogger(ProfileView.class);

    /** The pac4j web context. */
    @Inject
    private WebContext webContext;

    /** The pac4j profile manager. */
    @Inject
    private ProfileManager profileManager;

    /** Simple no-args constructor. */
    public ProfileView() {
    }

    /**
     * Gets the first profile (if it exists) contained in the profile manager.
     *
     * @return a list of pac4j profiles
     */
    public Object getProfile() {
        return profileManager.get(true).orElse(null); // It's fine to return a null reference if there is no value present.
    }

    /**
     * Gets the profiles contained in the profile manager.
     *
     * @return a list of pac4j profiles
     */
    public List getProfiles() {
        return profileManager.getAll(true);
    }

    /** Simply prints some debugging information post-construction. */
    @PostConstruct
    public void init() {
        logger.debug("webContext is null? {}", (webContext == null));
        logger.debug("profileManager is null? {}", (profileManager == null));
    }
}

src/main/webapp/oidc/index.xhtml为JSF模板。

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:h="http://java.sun.com/jsf/html"
                xmlns:f="http://java.sun.com/jsf/core"
                xmlns:ui="http://java.sun.com/jsf/facelets"
                template="/WEB-INF/template.xhtml">
    <ui:define name="title">Pac4J Java EE Demo - Protected Area</ui:define>
    <ui:define name="content">
        <div class="ui-g">
            <div class="ui-g-12">
                <div class="ui-container">
                    <h1>Protected Area</h1>
                    <p><h:link value="Back" outcome="/index"/></p>
                </div>
                <ui:include src="/WEB-INF/facelets/includes/pac4j-profiles-list.xhtml"/>
            </div>
        </div>
    </ui:define>
</ui:composition>

创建pac4j-profiles-list.xhtml文件,该文件包含在WEB-INF/facelets/includes

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:h="http://java.sun.com/jsf/html"
                xmlns:f="http://java.sun.com/jsf/core"
                xmlns:ui="http://java.sun.com/jsf/facelets">
    <div class="ui-container">
        <p>Found  <h:outputText value="#{profileView.profiles.size()}"/> profiles.</p>
        <h:panelGroup layout="block" rendered="#{profileView.profiles.size() > 0}">
            <p>First profile:  <h:outputText value="#{profileView.profile}"/></p>
        </h:panelGroup>
    </div>

    <h:panelGroup layout="block" rendered="#{not empty profileView.profile}">
        <h2>Profile Details</h2>
        <p><h:outputText value="Id: #{profileView.profile.id}"/></p>
        <p><h:outputText value="Type Id: #{profileView.profile.typedId}"/></p>
        <p><h:outputText value="Remembered: #{profileView.profile.remembered}"/></p>
        <h3>Attributes (<h:outputText value="#{profileView.profile.attributes.size()}"/>)</h3>
        <h:panelGroup layout="block" rendered="#{profileView.profile.attributes.size() > 0}">
            <ul>
                <ui:repeat value="#{profileView.profile.attributes.keySet().toArray()}" var="attributeName">
                    <li><h:outputText value="#{attributeName}"/>: <h:outputText value="#{profileView.profile.attributes.get(attributeName)}"/> </li>
                </ui:repeat>
            </ul>
        </h:panelGroup>
        <h3>Roles (<h:outputText value="#{profileView.profile.roles.size()}"/>)</h3>
        <h:panelGroup layout="block" rendered="#{profileView.profile.roles.size() > 0}">
            <ul>
                <ui:repeat value="#{profileView.profile.roles.toArray()}" var="role">
                    <li><h:outputText value="#{role}"/></li>
                </ui:repeat>
            </ul>
        </h:panelGroup>
        <h3>Permissions (<h:outputText value="#{profileView.profile.permissions.size()}"/>)</h3>
        <h:panelGroup layout="block" rendered="#{profileView.profile.permissions.size() > 0}">
            <ul>
                <ui:repeat value="#{profileView.profile.permissions.toArray()}" var="permission">
                    <li><h:outputText value="#{permission}"/></li>
                </ui:repeat>
            </ul>
        </h:panelGroup>
    </h:panelGroup>
</ui:composition>

oidc/index.xhtml模板使用WEB-INF/template.xhtml ,因此您也需要创建它。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">

    <h:head>
        <f:facet name="first">
            <meta http-equiv="X-UA-Compatible" content="IE=edge" />
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
            <meta name="apple-mobile-web-app-capable" content="yes" />
        </f:facet>
        <title><ui:insert name="title">Pac4J Java EE Demo</ui:insert></title>
        <ui:insert name="head"/>
    </h:head>

    <h:body styleClass="main-body">
        <div class="layout-wrapper">
            <div class="layout-main">
                <ui:insert name="content"/>
            </div>
        </div>
    </h:body>
</html>

添加这些文件后,重建项目并重新启动TomEE。

mvn clean package tomee:run

导航到http://localhost:8080/oidc/index.jsf ,您将被重定向到Okta进行登录。 如果您初次尝试无法解决问题,请重新启动浏览器并使用隐身窗口。 您应该看到用户的个人资料信息。

http://localhost:3000尝试您的React客户端; 它也应该工作!

如果您想知道为什么不堆叠图像,那是因为我将React应用程序的BeerList.tsx的啤酒清单的JSX更改为内联。

<h2>Beer List</h2>
{beers.map((beer: Beer) =>
  <span key={beer.id} style={{float: 'left', marginRight: '10px', marginLeft: '10px'}}>
    {beer.name}<br/>
    <GiphyImage name={beer.name}/>
  </span>
)}

雅加达EE呢?

您可能已经听说Java EE已经成为开源的(类似于Java SE的 ),其新名称为 。 David Blevins是一个很好的朋友,并且积极参与Java EE / Jakarta EE。 有关证明,请参阅他的传记:Apache TomEE,OpenEJB和Geronimo项目的创始人。 Apache,JCP EC,EE4J PMC,Jakarta EE WG,MicroProfile和Eclipse Board的成员。 首席执行官 。

我问戴维何时会发布可用的Jakarta EE。

Jakarta EE有一个来决定平台的方向。

了解有关安全REST API,Java EE,Jakarta EE和OIDC的更多信息

我希望您喜欢这个游览,向您展示了如何使用JWT和OIDC构建和保护Java EE REST API。 如果您想查看每个完成部分的源代码,我将它们放在分支中。 您可以使用以下命令克隆不同的实现:

git clone -b jwt-verifier https://github.com/oktadeveloper/okta-java-ee-rest-api-example.git
git clone -b spring-security https://github.com/oktadeveloper/okta-java-ee-rest-api-example.git
git clone -b pac4j https://github.com/oktadeveloper/okta-java-ee-rest-api-example.git

如前所述,我们在此博客上获得的大多数Java教程都展示了如何使用Spring Boot。 如果您有兴趣学习Spring Boot,这里有一些我写的教程将向您展示要点。

如果您是OIDC的新手,建议您查看以下文章:

有关Java REST API和TomEE的更多信息,我建议以下来源:

如果您到目前为止已经做到了,我怀疑您可能对以后的博客文章感兴趣。 在Twitter上和我的 , 上关注我们,或者查看 。 如有疑问,请在下面发表评论,或将其发布到我们的 。

“我喜欢编写身份验证和授权代码。” 〜从来没有Java开发人员。 厌倦了一次又一次地建立相同的登录屏幕?

最初于2018年9月12日发布在Okta开发人员博客上。

翻译自:

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- pqdy.cn 版权所有 赣ICP备2024042791号-6

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务