oidc

“我喜欢编写身份验证和授权代码。” 〜从来没有Java开发人员。 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证。

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相同。 即,我的Bootiful Angular和Bootiful React帖子中的“啤酒” API。

使用Java EE构建Java REST API

首先,我在Twitter上询问了我的网络,是否存在诸如start.spring.io之类的Java EE快速入门。 我收到了一些建议,并开始进行一些研究。 David Blevins建议我看一下tomee-jaxrs-starter-project ,所以我从那里开始。 我还研究了Roberto Cortez推荐的TomEE Maven原型 。

我喜欢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兼容性工作,但尚无发行版本。 我希望您也安装了Apache Maven 。

首先,将我们的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查看系统中的优质啤酒列表。 以下是使用HTTPie时的输出。

$ 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

我向您展示了该应用程序可以做什么,但是我还没有谈论它是如何构建的。 它有一些XML配置文件,但是我将跳过其中的大多数。 目录结构如下所示:

$ 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.xml12 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;}@Overridepublic String toString() {return "Beer{" +"id=" + id +", name='" + name + '\'' +'}';}
}

数据库(又名数据源)在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;@Injectpublic StartupBean(BeerService beerService) {this.beerService = beerService;}@PostConstructprivate 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);}@PreDestroyprivate 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;@Injectpublic 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 {@Injectprivate 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应用程序。

  1. 登录到您在developer.okta.com上的开发者帐户。
  2. 导航至应用程序 ,然后单击添加应用程序
  3. 选择Web ,然后单击Next
  4. 为应用程序命名(例如Java EE Secure API ),然后添加以下内容作为登录重定向URI:
    • http://localhost:3000/implicit/callback
    • http://localhost:8080/login/oauth2/code/okta
    • http://localhost:8080/callback?client_name=OidcClient
  5. 单击完成 ,然后编辑项目并启用“隐式(混合)”作为授予类型(允许ID和访问令牌),然后单击保存

使用JWT Verifier保护Java REST API

要从Okta验证JWT,您需要将Okta Java JWT Verifier添加到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>

然后创建一个JwtFilter.java (在src/main/java/com/okta/developer目录中)。 此过滤器查找其中包含访问令牌的authorization标头。 如果存在,它将对其进行验证并打印出用户的sub ,也就是他们的电子邮件地址。 如果不存在或无效,则返回拒绝访问状态。

确保使用您创建的应用中的设置替换{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;@Overridepublic 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();}}@Overridepublic 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 headerString 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);}@Overridepublic 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 {@Overridepublic 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 contentresponse.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 handshakeif (request.getMethod().equals("OPTIONS")) {response.setStatus(HttpServletResponse.SC_ACCEPTED);return;}// pass the request along the filter chainchain.doFilter(request, response);}@Overridepublic void init(FilterConfig config) {}@Overridepublic 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 SecurityWebApplicationInitializerextends AbstractSecurityWebApplicationInitializer {public SecurityWebApplicationInitializer() {super(SecurityConfiguration.class);}
}

在同一目录中创建一个SecurityConfiguration.java类。 此类使用Spring Security 5的oauth2Login()并向Spring Security注册您的Okta应用程序。

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;@Autowiredpublic 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;}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS).and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and().authorizeRequests().anyRequest().authenticated().and().oauth2Login();}@Beanpublic OAuth2AuthorizedClientService authorizedClientService() {return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository());}@Beanpublic ClientRegistrationRepository clientRegistrationRepository() {List<ClientRegistration> registrations = clients.stream().map(this::getRegistration).filter(Objects::nonNull).collect(Collectors.toList());return new InMemoryClientRegistrationRepository(registrations);}@Beanpublic 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

感谢Baeldung提供有关Spring Security 5 OAuth的出色文档 。

因为启用了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 Viewer Chrome插件提供了美观的JSON。

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

用以下代码替换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();

请参阅Spring Security的Servlet API集成文档以获取更多信息。

使用Pac4j锁定Java REST API

我想向您展示的确保Java REST API安全的最后一种技术是使用Pac4j,特别是j2e-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 {@Overridepublic 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 contentresponse.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 handshakeif (request.getMethod().equals("OPTIONS")) {response.setStatus(HttpServletResponse.SC_ACCEPTED);return;}// pass the request along the filter chainchain.doFilter(request, response);}@Overridepublic void init(FilterConfig config) {}@Overridepublic 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();@Overridepublic 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 JWKURL 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 tokenKeyPair 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相关的文件是从j2e-pac4j-cdi-demo复制的。

注意:我试图在TomEE上运行j2e-pac4j-cdi-demo (没有web.xml ),但是它失败并出现错误: Filters cannot be added to context [] as the context has been initialised 。 当使用Payara Maven插件时,它确实起作用。

创建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. */@Injectprivate WebContext webContext;/** The pac4j profile manager. */@Injectprivate 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. */@PostConstructpublic 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的OpenJDK ),它的新名称是Jakarta EE 。 David Blevins是一个很好的朋友,并且积极参与Java EE / Jakarta EE。 有关证明,请参阅他的Twitter传记:Apache TomEE,OpenEJB和Geronimo项目的创始人。 Apache,JCP EC,EE4J PMC,Jakarta EE WG,MicroProfile和Eclipse Board的成员。 首席执行官@Tomitribe 。

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

David:目前的主要重点是创建与Java EE 8兼容的Jakarta EE版本。我们希望在今年年底之前将其发布。 发布之后,我们将开始开发Jakarta EE 9并根据需要进行迭代。

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

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

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

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感兴趣,这里有一些我写的教程将向您展示要点。

  • Spring Boot,OAuth 2.0和Okta入门
  • 使用React和Spring Boot构建一个简单的CRUD应用
  • 使用Angular 7.0和Spring Boot 2.1构建基本的CRUD应用

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

  • Spring Security 5.0和OIDC入门
  • 身份,声明和令牌– OpenID Connect入门,第1部分,共3部分
  • 行动中的OIDC – OpenID Connect入门,第2部分,共3部分
  • 令牌中有什么? – OpenID Connect入门,第3部分,共3部分

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

  • David Blevins –解构REST安全,迭代2018
  • Antonio Goncalves –使用JWT保护JAX-RS端点
  • TomEE:使用Systemd运行

如果您已经做到了这一点,我怀疑您可能会对将来的博客文章感兴趣。 在Twitter上关注我和我的整个团队 , 在Facebook上关注我们,或者查看我们的YouTube频道 。 如有疑问,请在下面发表评论,或将其发布到我们的开发者论坛 。

“我喜欢编写身份验证和授权代码。” 〜从来没有Java开发人员。 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证。

使用Java EE和OIDC构建Java REST API最初于2018年9月12日发布在Okta开发人员博客上。

翻译自: https://www.javacodegeeks.com/2018/10/build-java-rest-api-java-ee-oidc.html

oidc

oidc_使用Java EE和OIDC构建Java REST API相关推荐

  1. 使用Java EE和OIDC构建Java REST API

    "我喜欢编写身份验证和授权代码." 〜从来没有Java开发人员. 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证. Java ...

  2. Java EE 7中的WebSocket客户端API

    在本文中,让我们探索谈论较少的Web Socket Client API,以及如何在Java EE 7容器本身中利用它. Web套接字服务器API规则 JSR 356的服务器端API(Java的Web ...

  3. java ee 6 sdk中文版,Java EE 6 SDK+Eclipse JEE+Android ADT-Fun言

    很多时候,为了生存,需要不断的了解,学习新东西,于是头脑塞满了便便- -|||- -----------------------.. 按照自己的理解: JDK = Java Develope Kit ...

  4. 2018 二级java 9月时间_2018年9月计算机二级考试JAVA章节知识:构建java程序

    出国留学网在这里为考生们整理了"2018年9月计算机二级考试JAVA章节知识",希望有所帮助,想了解更多考试资讯,请关注小编的及时更新哦. 2018年9月计算机二级考试JAVA章节 ...

  5. react api_使用Java EE 8中的React式API加速服务

    react api 服务通常可以通过异步处理进行优化,即使不改变其对外界的行为. 某些服务效率不高的原因是,它们需要等待其他服务提供结果才能继续下去. 让我们看一下如何在不等待外部REST服务的情况下 ...

  6. ibm liberty_使用Eclipse和Open Liberty的Java EE 8上的Java 9

    ibm liberty 几周前,我写了一篇文章,题目是哪个IDE和服务器支持Java EE 8和Java9 ,着眼于Java 9和Java EE 8之间的当前状态.您可以期望事情发展很快,现在我们有了 ...

  7. java ee规范_测试Java EE 8规范

    java ee规范 Java EE 8平台肯定在过去的几个月中一直在发展. 规范已经发布了早期的草案评审,里程碑甚至最终版本. 实际上,随着JSF 2.3的发布,JSR-372才刚刚进入最终版本. 有 ...

  8. java ee架构_与Java EE和Spring的集成架构

    java ee架构 本周在纽约举行的O'Reilly软件体系结构大会将举行 . 我很高兴与Josh Long一起提供有关如何集成Java EE和Spring的教程. 一段时间以来,我们一直在开玩笑. ...

  9. 使用Java EE 8中的反应式API加速服务

    服务通常可以通过异步处理进行优化,即使不改变其对外界的行为. 某些服务效率不高的原因是,它们需要等待其他服务提供结果才能继续下去. 让我们看一下如何在不等待外部REST服务的情况下调用它们,并独立进行 ...

最新文章

  1. 3.5 向量化实现的解释-深度学习-Stanford吴恩达教授
  2. nginx的启动、停止、平滑重启
  3. 解析烧录固件失败_化虚为实,示人本相!FLIR热像仪双型号双版本上手解析
  4. [vue] vue怎么实现强制刷新组件?
  5. docker 安装 centos
  6. 力扣904-水果成篮(C++,总结别人的思路)
  7. 面向对象设计模式之策略模式
  8. ABAP术语-Business Components
  9. Setting up a EDK II build environment on Windows and Linux:搭建Windows和Linux开发环境[2.2]
  10. SPSS软件数据中心化、标准化和归一化
  11. 直角三角形的两个夹角度数计算(java)
  12. 独家放送!搞定大屏可视化,深得老板心的大屏在这里
  13. pythonfor循环100次_Python之for循环
  14. “绿多多”公益+冠军链盟+关羽斩醛:良设板与“空间优造”的无醛与环保之路
  15. 识别网络计算机的方法,电脑未识别网络怎么办 电脑未识别解决方法【图文】...
  16. E450Think pad 开机亮红灯,无法启动修复
  17. js终止程序执行的方法
  18. GNU Radio: 射频子板
  19. 从零开始Tableau | 12.表计算-特定维度
  20. 全网清晰虚拟机安装截图

热门文章

  1. 牛客题霸 [判断二叉树是否对称] C++题解/答案
  2. 【学习笔记】我命由天不由我之随机化庇佑 —— 爬山法 和 模拟退火法
  3. 历史上的今天(history)+ 勇者斗恶龙(dragon)
  4. CodeForces:372(div1)div373(div2)
  5. 欢乐纪中某B组赛【2019.1.18】
  6. P1040,jzoj1167-加分二叉树【树形dp】
  7. 【做题记录】人类智慧
  8. 【bfs】WZK旅游(jzoj 1996)
  9. 10、mysql数据表中数据的查询(2)
  10. Spring Boot 消息队列 RocketMQ 入门