搜索词>>RestTemplate 耗时0.0030
  • HttpClient的RestTemplate - Java配置示例

    HttpClient的RestTemplate - Java配置示例<h2 style="margin-left:0px; margin-right:0px; text-align:start">HttpClient配置</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#333333"><span style="font-family:"varela round","helvetica neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">在<code>HttpClientConfig</code>课堂上,我们主要配置两件事 -</span></span></span></p> <ol style="margin-left:40px; margin-right:0px"> <li style="list-style-type:decimal"><code><a href="https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/conn/PoolingHttpClientConnectionManager.html" rel="external nofollow" style="box-sizing:border-box; transition:all 0.1s ease-in-out; color:#0366d6; text-decoration:none" target="_blank">PoolingHttpClientConnectionManager</a></code> - 顾名思义,它的连接池管理器。在这里,连接按照每个路线进行汇集。对于已经是管理器具有可用于池中的持续连接的路由的请求将是通过从池租用连接而不是创建全新连接的服务。 <p style="margin-left:0px; margin-right:0px"><code><a href="https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/conn/ConnectionKeepAliveStrategy.html" rel="external nofollow" style="box-sizing:border-box; transition:all 0.1s ease-in-out; color:#0366d6; text-decoration:none" target="_blank">ConnectionKeepAliveStrategy</a></code> 有助于设置时间,这决定了连接在重新使用之前可以保持空闲状态的时间。</p> </li> <li style="list-style-type:decimal">并设置一个<code>idleConnectionMonitor</code>线程,它会定期检查所有连接并释放尚未使用的空闲时间和空闲时间。</li> </ol> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#333333"><span style="font-family:"varela round","helvetica neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">真正使用的http客户端是<code><a href="https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/client/CloseableHttpClient.html" rel="external nofollow" style="box-sizing:border-box; transition:all 0.1s ease-in-out; color:#0366d6; text-decoration:none" target="_blank">CloseableHttpClient</a></code>bean。它将<code>RestTemplate</code>用于获取与API端点的连接。</span></span></span><br />  </p> <pre> <code class="language-java">import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.util.concurrent.TimeUnit; import org.apache.http.HeaderElement; import org.apache.http.HeaderElementIterator; import org.apache.http.HttpResponse; import org.apache.http.client.config.RequestConfig; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.ConnectionKeepAliveStrategy; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.TrustSelfSignedStrategy; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.message.BasicHeaderElementIterator; import org.apache.http.protocol.HTTP; import org.apache.http.protocol.HttpContext; import org.apache.http.ssl.SSLContextBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; /** * - Supports both HTTP and HTTPS * - Uses a connection pool to re-use connections and save overhead of creating connections. * - Has a custom connection keep-alive strategy (to apply a default keep-alive if one isn't specified) * - Starts an idle connection monitor to continuously clean up stale connections. */ @Configuration @EnableScheduling public class HttpClientConfig { private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientConfig.class); // Determines the timeout in milliseconds until a connection is established. private static final int CONNECT_TIMEOUT = 30000; // The timeout when requesting a connection from the connection manager. private static final int REQUEST_TIMEOUT = 30000; // The timeout for waiting for data private static final int SOCKET_TIMEOUT = 60000; private static final int MAX_TOTAL_CONNECTIONS = 50; private static final int DEFAULT_KEEP_ALIVE_TIME_MILLIS = 20 * 1000; private static final int CLOSE_IDLE_CONNECTION_WAIT_TIME_SECS = 30; @Bean public PoolingHttpClientConnectionManager poolingConnectionManager() { SSLContextBuilder builder = new SSLContextBuilder(); try { builder.loadTrustMaterial(null, new TrustSelfSignedStrategy()); } catch (NoSuchAlgorithmException | KeyStoreException e) { LOGGER.error("Pooling Connection Manager Initialisation failure because of " + e.getMessage(), e); } SSLConnectionSocketFactory sslsf = null; try { sslsf = new SSLConnectionSocketFactory(builder.build()); } catch (KeyManagementException | NoSuchAlgorithmException e) { LOGGER.error("Pooling Connection Manager Initialisation failure because of " + e.getMessage(), e); } Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder .<ConnectionSocketFactory>create().register("https", sslsf) .register("http", new PlainConnectionSocketFactory()) .build(); PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); poolingConnectionManager.setMaxTotal(MAX_TOTAL_CONNECTIONS); return poolingConnectionManager; } @Bean public ConnectionKeepAliveStrategy connectionKeepAliveStrategy() { return new ConnectionKeepAliveStrategy() { @Override public long getKeepAliveDuration(HttpResponse response, HttpContext context) { HeaderElementIterator it = new BasicHeaderElementIterator (response.headerIterator(HTTP.CONN_KEEP_ALIVE)); while (it.hasNext()) { HeaderElement he = it.nextElement(); String param = he.getName(); String value = he.getValue(); if (value != null && param.equalsIgnoreCase("timeout")) { return Long.parseLong(value) * 1000; } } return DEFAULT_KEEP_ALIVE_TIME_MILLIS; } }; } @Bean public CloseableHttpClient httpClient() { RequestConfig requestConfig = RequestConfig.custom() .setConnectionRequestTimeout(REQUEST_TIMEOUT) .setConnectTimeout(CONNECT_TIMEOUT) .setSocketTimeout(SOCKET_TIMEOUT).build(); return HttpClients.custom() .setDefaultRequestConfig(requestConfig) .setConnectionManager(poolingConnectionManager()) .setKeepAliveStrategy(connectionKeepAliveStrategy()) .build(); } @Bean public Runnable idleConnectionMonitor(final PoolingHttpClientConnectionManager connectionManager) { return new Runnable() { @Override @Scheduled(fixedDelay = 10000) public void run() { try { if (connectionManager != null) { LOGGER.trace("run IdleConnectionMonitor - Closing expired and idle connections..."); connectionManager.closeExpiredConnections(); connectionManager.closeIdleConnections(CLOSE_IDLE_CONNECTION_WAIT_TIME_SECS, TimeUnit.SECONDS); } else { LOGGER.trace("run IdleConnectionMonitor - Http Client Connection manager is not initialised"); } } catch (Exception e) { LOGGER.error("run IdleConnectionMonitor - Exception occurred. msg={}, e={}", e.getMessage(), e); } } }; } }</code></pre> <h2 style="margin-left:0px; margin-right:0px; text-align:start">RestTemplate配置</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#333333"><span style="font-family:"varela round","helvetica neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">在这里,我们正在配置<code>RestTemplate</code>我们最终将用来调用REST API的bean。如上所述,它使用<code>CloseableHttpClient</code>bean实例来构建<code><a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/client/ClientHttpRequestFactory.html" rel="external nofollow" style="box-sizing:border-box; transition:all 0.1s ease-in-out; color:#0366d6; text-decoration:none" target="_blank">ClientHttpRequestFactory</a></code>,用于创建<code>RestTemplate</code>。</span></span></span></p> <ol style="margin-left:40px; margin-right:0px"> <li style="list-style-type:decimal"><code><a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.html" rel="external nofollow" style="box-sizing:border-box; transition:all 0.1s ease-in-out; color:#0366d6; text-decoration:none" target="_blank">HttpComponentsClientHttpRequestFactory</a></code>是<code>ClientHttpRequestFactory</code>使用<em>Apache HttpComponents HttpClient</em>创建请求的实现。</li> <li style="list-style-type:decimal">我们<code>@Scheduled</code>在<code>httpClient</code>配置中使用了注释。为了支持这个,我们必须添加对线程预定执行的支持。为此,我们使用了<code>ThreadPoolTaskScheduler</code>内部使用<a href="https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ScheduledThreadPoolExecutor.html" rel="external nofollow" style="box-sizing:border-box; transition:all 0.1s ease-in-out; color:#0366d6; text-decoration:none" target="_blank">ScheduledThreadPoolExecutor的</a> bean 来安排命令在给定的延迟后运行,或者定期执行。</li> </ol> <pre> <code class="language-java">import org.apache.http.impl.client.CloseableHttpClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.web.client.RestTemplate; public class RestTemplateConfig { @Autowired CloseableHttpClient httpClient; @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory()); return restTemplate; } @Bean public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() { HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(); clientHttpRequestFactory.setHttpClient(httpClient); return clientHttpRequestFactory; } @Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setThreadNamePrefix("poolScheduler"); scheduler.setPoolSize(50); return scheduler; } }</code></pre> <h2 style="margin-left:0px; margin-right:0px; text-align:start">如何使用RestTemplate</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#333333"><span style="font-family:"varela round","helvetica neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">要使用上述配置<code>RestTemplate</code>,只需将其注入控制器或测试类。</span></span></span></p> <pre> <code class="language-java">import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.web.client.RestTemplate; import com.leftso.config.HttpClientConfig; import com.leftso.config.RestTemplateConfig; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { RestTemplateConfig.class, HttpClientConfig.class }) public class TestApplication { @Autowired RestTemplate restTemplate; @Test public void getEmployees() { final String uri = "http://localhost:8080/employees"; String result = restTemplate.getForObject(uri, String.class); Assert.assertEquals(true, result.indexOf("Lokesh") > 0); } }</code></pre> <h2 style="margin-left:0px; margin-right:0px; text-align:start">Maven的依赖</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#333333"><span style="font-family:"varela round","helvetica neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">首先,你将被要求有两个依赖关系,即<code>httpclient</code>和<code>spring-web</code>。我正在使用spring启动应用程序,所以pom文件如下所示:</span></span></span></p> <pre> <code class="language-xml"><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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.leftso</groupId> <artifactId>springbootdemo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>springbootdemo</name> <url>http://maven.apache.org</url> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RELEASE</version> </parent> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project></code></pre>
  • HttpClient 使用SSL访问HTTPS(提高篇)

    本文将展示如何使用“接受所有”SSL支持来配置Apache HttpClient 4。目标很简单 - 使用没有有效证书的HTTPS URL。<h2 style="margin-left:0px; margin-right:0px; text-align:start"><strong>1.概述</strong></h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">本文将展示如何<strong>使用“接受所有”SSL支持</strong>来<strong>配置Apache HttpClient 4</strong>。目标很简单 - 使用没有有效证书的HTTPS URL。</span></span></span><br /> <br />  </p> <h2 style="margin-left:0px; margin-right:0px; text-align:start"><strong>2. <em>SSLPeerUnverifiedException</em></strong></h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">在没有使用<em>HttpClient</em>配置SSL的情况下,以下测试(使用HTTPS URL)将失败:</span></span></span></p> <pre> <code class="language-java">public class HttpLiveTest { @Test(expected = SSLPeerUnverifiedException.class) public void whenHttpsUrlIsConsumed_thenException() throws ClientProtocolException, IOException { DefaultHttpClient httpClient = new DefaultHttpClient(); String urlOverHttps = "https://localhost:8080/spring-security-rest-basic-auth"; HttpGet getMethod = new HttpGet(urlOverHttps); HttpResponse response = httpClient.execute(getMethod); assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); } }</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start">确切的失败是:</p> <pre> <code class="language-java">javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated at sun.security.ssl.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:397) at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:126) ...</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start">该<a href="http://docs.oracle.com/javase/7/docs/api/javax/net/ssl/SSLPeerUnverifiedException.html" rel="external nofollow" style="box-sizing:border-box; background-color:#ffffff; color:#63b175; text-decoration:none; font-family:raleway; font-size:18px; font-style:normal; font-variant-ligatures:normal; font-variant-caps:normal; font-weight:normal; letter-spacing:normal; orphans:2; text-align:start; text-transform:none; white-space:normal; widows:2; word-spacing:0px; -webkit-text-stroke-width:0px" target="_blank" title="Java SE 7中的SSLPeerUnverifiedException javadoc"><em>javax.net.ssl.SSLPeerUnverifiedException</em>例外</a>,只要有效信任链无法为URL建立发生。<br />  </p> <h2 style="margin-left:0px; margin-right:0px; text-align:start"><strong>3.配置SSL - 全部接受(HttpClient <4.3)</strong></h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">现在让我们将HTTP客户端配置为信任所有证书链,而不管它们的有效性如何:</span></span></span></p> <pre> <code class="language-java">@Test public void givenAcceptingAllCertificates_whenHttpsUrlIsConsumed_thenException() throws IOException, GeneralSecurityException { TrustStrategy acceptingTrustStrategy = (cert, authType) -> true; SSLSocketFactory sf = new SSLSocketFactory( acceptingTrustStrategy, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); SchemeRegistry registry = new SchemeRegistry(); registry.register(new Scheme("https", 8443, sf)); ClientConnectionManager ccm = new PoolingClientConnectionManager(registry); DefaultHttpClient httpClient = new DefaultHttpClient(ccm); String urlOverHttps = "https://localhost:8443/spring-security-rest-basic-auth/api/bars/1"; HttpGet getMethod = new HttpGet(urlOverHttps); HttpResponse response = httpClient.execute(getMethod); assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); }</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start">随着新的<em>TrustStrategy</em>现在<strong>覆盖标准证书验证过程</strong>(应该咨询配置的信任管理器) - 测试现在通过,<strong>客户端可以使用HTTPS URL</strong>。<br />  </p> <h2 style="margin-left:0px; margin-right:0px; text-align:start"><strong>4. 带SSL 的Spring <em>RestTemplate</em>(HttpClient <4.3)</strong></h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">现在我们已经看到了如何配置一个支持SSL 的原始<em>HttpClient</em>,让我们来看看更高级别的客户端<em>--Spring RestTemplate</em>。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">未配置SSL,以下测试将按预期失败:</span></span></span></p> <pre> <code class="language-java">@Test(expected = ResourceAccessException.class) public void whenHttpsUrlIsConsumed_thenException() { String urlOverHttps = "https://localhost:8443/spring-security-rest-basic-auth/api/bars/1"; ResponseEntity<String> response = new RestTemplate().exchange(urlOverHttps, HttpMethod.GET, null, String.class); assertThat(response.getStatusCode().value(), equalTo(200)); }</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start">那么让我们来配置SSL:<br />  </p> <pre> <code class="language-java">import static org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER; import java.security.GeneralSecurityException; import java.security.cert.X509Certificate; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.conn.ssl.TrustStrategy; import org.apache.http.impl.client.DefaultHttpClient; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; ... @Test public void givenAcceptingAllCertificates_whenHttpsUrlIsConsumed_thenException() throws GeneralSecurityException { HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); DefaultHttpClient httpClient = (DefaultHttpClient) requestFactory.getHttpClient(); TrustStrategy acceptingTrustStrategy = (cert, authType) -> true SSLSocketFactory sf = new SSLSocketFactory( acceptingTrustStrategy, ALLOW_ALL_HOSTNAME_VERIFIER); httpClient.getConnectionManager().getSchemeRegistry() .register(new Scheme("https", 8443, sf)); String urlOverHttps = "https://localhost:8443/spring-security-rest-basic-auth/api/bars/1"; ResponseEntity<String> response = new RestTemplate(requestFactory). exchange(urlOverHttps, HttpMethod.GET, null, String.class); assertThat(response.getStatusCode().value(), equalTo(200)); }</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start">如您所见,这<strong>与我们为原始HttpClient配置SSL的方式非常相似</strong> - 我们使用SSL支持配置请求工厂,然后实例化通过此预配置工厂的模板。<br />  </p> <h2 style="margin-left:0px; margin-right:0px; text-align:start"><strong>5.配置SSL - 全部接受(HttpClient 4.4)</strong></h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">在HttpClient 4.4版本中,现在不推荐使用<em>SSLSocketFactory</em>,我们可以简单地配置我们的<em>HttpClient</em>,如下所示:</span></span></span></p> <pre> <code class="language-java">@Test public void givenIgnoringCertificates_whenHttpsUrlIsConsumed_thenCorrect() throws Exception { SSLContext sslContext = new SSLContextBuilder() .loadTrustMaterial(null, (certificate, authType) -> true).build(); CloseableHttpClient client = HttpClients.custom() .setSSLContext(sslContext) .setSSLHostnameVerifier(new NoopHostnameVerifier()) .build(); HttpGet httpGet = new HttpGet(HOST_WITH_SSL); httpGet.setHeader("Accept", "application/xml"); HttpResponse response = client.execute(httpGet); assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); }</code></pre> <h2 style="margin-left:0px; margin-right:0px; text-align:start"><strong>6. 带SSL 的Spring <em>RestTemplate</em>(HttpClient 4.4)</strong></h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">我们可以用同样的方法来配置我们的<em>RestTemplate</em>:</span></span></span></p> <pre> <code class="language-java">@Test public void givenAcceptingAllCertificatesUsing4_4_whenUsingRestTemplate_thenCorrect() throws ClientProtocolException, IOException { CloseableHttpClient httpClient = HttpClients.custom() .setSSLHostnameVerifier(new NoopHostnameVerifier()) .build(); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setHttpClient(httpClient); ResponseEntity<String> response = new RestTemplate(requestFactory).exchange( urlOverHttps, HttpMethod.GET, null, String.class); assertThat(response.getStatusCode().value(), equalTo(200)); }</code></pre> <h2 style="margin-left:0px; margin-right:0px; text-align:start"><strong>7.总结</strong></h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">本教程讨论了如何为Apache HttpClient配置SSL,以便它能够使用任何HTTPS URL,而不考虑证书。还展示了Spring <em>RestTemplate</em>的相同配置。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">然而,要理解的重要一点是,<strong>这种策略完全忽略了证书检查</strong> - 这使得它不安全,只能在有意义的地方使用。</span></span></span></p>
  • 使用OAuth2安全的Spring security REST API(含demo代码下载)

    使用OAuth2安全的Spring REST API,Secure Spring REST API using OAuth2(含demo代码下载)<p>    让我们<code>Spring REST API</code>使用<code>OAuth2</code>这个时间来保护我们,这是一个简单的指南,显示使用REST API来保护REST API所需的内容<code>Spring OAuth2</code>。我们的用例符合<code>Resource-owner Password Grant</code>OAUth2规范的流程。我们将使用两个不同的客户端(Postman和<code>Spring RestTemplate</code>基于Java 的应用程序)访问我们的OAuth2受保护的REST资源。</p> <p>    如果您已经熟悉OAuth2概念,您可能希望跳过该理论,并直接跳转到代码中。一如以往,在本文末尾附件中可以找到完整的代码。让我们开始吧。<br />  </p> <h3>    什么是OAuth2</h3> <p>    OAuth2是一个标准化的授权协议/框架。根据官方OAuth2规范:</p> <p>    OAuth 2.0授权框架使得第三方应用程序可以通过协调资源所有者和HTTP服务之间的批准交互来代替资源所有者来<u>获取</u>对HTTP服务的<u>有限访问权限</u>,也可以允许第三方应用程序以自己的身份获得访问权限。</p> <p>    Google,Facebook等大型玩家已经在使用自己的OAuth2实现了很长一段时间。企业也正在朝OAuth2采纳方向发展。</p> <p>我发现OAuth2规范比较简单。然而,如果你想开始更快,可以在这里找到一篇关于OAuth2基础知识的优秀文章,从而深入了解OAUth2理论概念。</p> <p>Spring Security OAuth项目提供了使用Spring开发符合OAuth2标准的实现所需的所有API。官方的Spring安全oauth项目提供了一个实施OAuth2的综合示例。这个帖子的代码示例灵感来自这个例子本身。这篇文章的目的是为了保护我们的REST API,只需使用所需的最低限度的功能。</p> <p>至少你应该知道OAuth2中的四个关键概念:<br />  </p> <h3>OAuth2角色</h3> <p>OAuth2定义了四个角色:</p> <ul> <li><strong><code>resource owner</code>:</strong><br /> 可能是你 能够授予访问受保护资源的实体。当资源所有者是个人时,它被称为最终用户。</li> <li><strong><code>resource server</code>:</strong><br /> 托管受保护资源的服务器,能够使用访问令牌接受和响应受保护的资源请求。</li> <li><strong><code>client</code>:</strong><br /> 代表资源所有者及其授权的应用程序生成受保护的资源请求。它可能是一个移动应用程序,要求您访问您的Facebook订阅源,REST客户端尝试访问REST API,一个网站[Stackoverflow例如]使用Facebook帐户提供备用登录选项。</li> <li><strong><code>authorization server</code>:</strong><br /> 服务器在成功验证资源所有者并获得授权后,向客户端发出访问令牌。</li> </ul> <p>在我们的示例中,我们的REST API只能通过资源服务器进行访问,这将需要存在请求的访问令牌<br />  </p> <h3>2. OAuth2授权授权类型</h3> <p>授权授权是表示资源所有者授权(访问其受保护的资源)的凭据,由客户端使用以获取访问令牌。规范定义了四种授权类型:</p> <ul> <li><code>authorization code</code></li> <li><code>implicit</code></li> <li><code>resource owner password credentials</code></li> <li><code>client credentials</code></li> </ul> <p>我们将使用<code>resource owner password credentials</code>授权类型。原因很简单,我们没有实现将我们重定向到登录页面的视图。仅客户端[Postman或RestTemplate为基础的Java客户端例如]拥有资源所有者的凭据,并且他们将这些凭证以及客户机凭证提供给授权服务器,以便最终接收访问令牌[和可选刷新令牌],然后使用该令牌实际访问资源。</p> <p>一个常见的例子是<code>GMail app</code>您的智能手机上的[客户端],您需要您的凭据并使用它们进行连接<code>GMail servers</code>。它还显示“密码凭证授予”最适合当客户端和服务器与信任在同一个公司时,您不想向第三方提供凭据。</p> <h3>OAuth2令牌</h3> <p>令牌是实现特定的随机字符串,由授权服务器生成,并在客户端请求时发出。</p> <ul> <li><code>Access Token</code> :发送每个请求,通常有效期很短的一段时间[一小时例如]</li> <li><code>Refresh Token</code> :主要用于获取新的访问令牌,不发送每个请求,通常比访问令牌寿命更长。</li> </ul> <strong>HTTPS上的一个词</strong>:对于任何类型的安全实现,从基本身份验证到完整的OAuth2实现<strong><code>HTTPS</code></strong>都是必须的。没有HTTPS,无论您的实现是什么,安全性都将受到威胁。 <h3>OAuth2访问令牌范围</h3> <p>客户端可以使用范围[想要访问此用户的Facebook帐户的Feed和照片]来查询具有特定访问权限的资源,授权服务器又返回显示实际授予客户端访问权限的范围[资源所有者只允许Feed访问,没有照片例如]。</p> <hr />我们进入代码 <p>我们来实现使用Spring Security实现OAuth的必要构建块,以便访问我们的REST资源。</p> <h3>资源服务器</h3> <p>资源服务器托管客户端感兴趣的资源[我们的REST API]。资源位于<code>/user/</code>。<code>@EnableResourceServer</code>注释,应用于OAuth2资源服务器,启用使用传入OAuth2令牌对请求进行身份验证的Spring Security过滤器。类<code>ResourceServerConfigurerAdapter</code>实现<code>ResourceServerConfigurer</code>提供了调整由OAuth2安全保护的访问规则和路径的方法。</p> <pre> <code class="language-java">package com.websystique.springmvc.security;   import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler;   @Configuration @EnableResourceServer public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {       private static final String RESOURCE_ID = "my_rest_api";           @Override     public void configure(ResourceServerSecurityConfigurer resources) {         resources.resourceId(RESOURCE_ID).stateless(false);     }       @Override     public void configure(HttpSecurity http) throws Exception {         http.         anonymous().disable()         .requestMatchers().antMatchers("/user/**")         .and().authorizeRequests()         .antMatchers("/user/**").access("hasRole('ADMIN')")         .and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());     }   }</code></pre> <h3><br /> 授权服务器是一个负责验证凭据的人员,如果凭据确定,提供令牌[刷新令牌以及访问令牌]。它还包含有关注册的客户端和可能的访问范围和授权类型的信息。令牌存储用于存储令牌。我们将使用内存中的令牌存储。<code>@EnableAuthorizationServer</code>在当前应用程序上下文中启用授权服务器(即AuthorizationEndpoint和TokenEndpoint)。类<code>AuthorizationServerConfigurerAdapter</code>实现<code>AuthorizationServerConfigurer</code>,它提供了配置授权服务器的所有必要方法。2.授权服务器</h3> <pre> <code class="language-java"> package com.websystique.springmvc.security;   import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.approval.UserApprovalHandler; import org.springframework.security.oauth2.provider.token.TokenStore;   @Configuration @EnableAuthorizationServer public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {       private static String REALM="MY_OAUTH_REALM";           @Autowired     private TokenStore tokenStore;       @Autowired     private UserApprovalHandler userApprovalHandler;       @Autowired     @Qualifier("authenticationManagerBean")     private AuthenticationManager authenticationManager;       @Override     public void configure(ClientDetailsServiceConfigurer clients) throws Exception {           clients.inMemory()             .withClient("my-trusted-client")             .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")             .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")             .scopes("read", "write", "trust")             .secret("secret")             .accessTokenValiditySeconds(120).//Access token is only valid for 2 minutes.             refreshTokenValiditySeconds(600);//Refresh token is only valid for 10 minutes.     }       @Override     public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {         endpoints.tokenStore(tokenStore).userApprovalHandler(userApprovalHandler)                 .authenticationManager(authenticationManager);     }       @Override     public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {         oauthServer.realm(REALM+"/client");     }   }</code></pre> <p><br /> <br /> 向客户端注册客户端“我的信任客户端”和密码“秘密”以及允许的角色和范围。以上配置</p> <ul> <li>指定任何生成的访问令牌只有120秒有效</li> <li>指定任何生成的刷新令牌只有600秒有效</li> </ul> <h3>3.安全配置</h3> <p>粘在一起 端点<code>/oauth/token</code>用于请求令牌[访问或刷新]。资源所有者[bill,bob]在这里配置。</p> <pre> <code class="language-java">package com.websystique.springmvc.security;   import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.approval.ApprovalStore; import org.springframework.security.oauth2.provider.approval.TokenApprovalStore; import org.springframework.security.oauth2.provider.approval.TokenStoreUserApprovalHandler; import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;   @Configuration @EnableWebSecurity public class OAuth2SecurityConfiguration extends WebSecurityConfigurerAdapter {       @Autowired     private ClientDetailsService clientDetailsService;           @Autowired     public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {         auth.inMemoryAuthentication()         .withUser("bill").password("abc123").roles("ADMIN").and()         .withUser("bob").password("abc123").roles("USER");     }       @Override     protected void configure(HttpSecurity http) throws Exception {         http         .csrf().disable()         .anonymous().disable()         .authorizeRequests()         .antMatchers("/oauth/token").permitAll();     }       @Override     @Bean     public AuthenticationManager authenticationManagerBean() throws Exception {         return super.authenticationManagerBean();     }         @Bean     public TokenStore tokenStore() {         return new InMemoryTokenStore();     }       @Bean     @Autowired     public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore){         TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();         handler.setTokenStore(tokenStore);         handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));         handler.setClientDetailsService(clientDetailsService);         return handler;     }           @Bean     @Autowired     public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {         TokenApprovalStore store = new TokenApprovalStore();         store.setTokenStore(tokenStore);         return store;     }       }</code></pre> <p>另外,启用全局方法安全性,如果我们想要使用它,它将激活@PreFilter,@PostFilter,@PreAuthorize @PostAuthorize注释。</p> <pre> <code class="language-java"> package com.websystique.springmvc.security;   import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; import org.springframework.security.oauth2.provider.expression.OAuth2MethodSecurityExpressionHandler;   @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true) public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {     @Autowired     private OAuth2SecurityConfiguration securityConfig;       @Override     protected MethodSecurityExpressionHandler createExpressionHandler() {         return new OAuth2MethodSecurityExpressionHandler();     } }</code></pre> <h3>4.终点及其目的</h3> <ul> <li>尝试访问资源[REST API],无需任何授权[将当然失败]。<br /> <code>GET http://localhost:8080/SpringSecurityOAuth2Example/user/</code></li> <li>问令牌[接入+刷新]使用<strong>HTTP POST</strong>上<code>/oauth/token</code>,与grant_type =密码和资源所有者凭证REQ-PARAMS。另外,在授权头中发送客户端凭据。 <p> </p> <p><code>POST http://localhost:8080/SpringSecurityOAuth2Example/oauth/token?grant_type=password&username=bill&password=abc123</code></p> </li> <li>要求通过有效的刷新令牌新的访问令牌,使用<strong>HTTP POST</strong>上<code>/oauth/token</code>,与grant_type = refresh_token,并发送实际的刷新令牌。另外,在授权头中发送客户端凭据。 <p> </p> <p><code>POST http://localhost:8080/SpringSecurityOAuth2Example/oauth/token?grant_type=refresh_token&refresh_token=094b7d23-973f-4cc1-83ad-8ffd43de1845</code></p> </li> <li>通过使用<code>access_token</code>具有请求的查询参数提供访问令牌来访问资源。<br /> <code>GET http://localhost:8080/SpringSecurityOAuth2Example/user/?access_token=3525d0e4-d881-49e7-9f91-bcfd18259109</code></li> </ul> <h3>5.休息API</h3> <p>我在大部分帖子中使用的简单的Spring REST API。</p> <pre> <code class="language-java"> package com.websystique.springmvc.controller;    import java.util.List;    import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.UriComponentsBuilder;    import com.websystique.springmvc.model.User; import com.websystique.springmvc.service.UserService;    @RestController public class HelloWorldRestController {        @Autowired     UserService userService;  //Service which will do all data retrieval/manipulation work               //-------------------Retrieve All Users--------------------------------------------------------            @RequestMapping(value = "/user/", method = RequestMethod.GET)     public ResponseEntity<List<User>> listAllUsers() {         List<User> users = userService.findAllUsers();         if(users.isEmpty()){             return new ResponseEntity<List<User>>(HttpStatus.NO_CONTENT);//You many decide to return HttpStatus.NOT_FOUND         }         return new ResponseEntity<List<User>>(users, HttpStatus.OK);     }           //-------------------Retrieve Single User--------------------------------------------------------            @RequestMapping(value = "/user/{id}", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE,MediaType.APPLICATION_XML_VALUE})     public ResponseEntity<User> getUser(@PathVariable("id") long id) {         System.out.println("Fetching User with id " + id);         User user = userService.findById(id);         if (user == null) {             System.out.println("User with id " + id + " not found");             return new ResponseEntity<User>(HttpStatus.NOT_FOUND);         }         return new ResponseEntity<User>(user, HttpStatus.OK);     }                      //-------------------Create a User--------------------------------------------------------            @RequestMapping(value = "/user/", method = RequestMethod.POST)     public ResponseEntity<Void> createUser(@RequestBody User user, UriComponentsBuilder ucBuilder) {         System.out.println("Creating User " + user.getName());            if (userService.isUserExist(user)) {             System.out.println("A User with name " + user.getName() + " already exist");             return new ResponseEntity<Void>(HttpStatus.CONFLICT);         }            userService.saveUser(user);            HttpHeaders headers = new HttpHeaders();         headers.setLocation(ucBuilder.path("/user/{id}").buildAndExpand(user.getId()).toUri());         return new ResponseEntity<Void>(headers, HttpStatus.CREATED);     }               //------------------- Update a User --------------------------------------------------------            @RequestMapping(value = "/user/{id}", method = RequestMethod.PUT)     public ResponseEntity<User> updateUser(@PathVariable("id") long id, @RequestBody User user) {         System.out.println("Updating User " + id);                    User currentUser = userService.findById(id);                    if (currentUser==null) {             System.out.println("User with id " + id + " not found");             return new ResponseEntity<User>(HttpStatus.NOT_FOUND);         }            currentUser.setName(user.getName());         currentUser.setAge(user.getAge());         currentUser.setSalary(user.getSalary());                    userService.updateUser(currentUser);         return new ResponseEntity<User>(currentUser, HttpStatus.OK);     }        //------------------- Delete a User --------------------------------------------------------            @RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE)     public ResponseEntity<User> deleteUser(@PathVariable("id") long id) {         System.out.println("Fetching & Deleting User with id " + id);            User user = userService.findById(id);         if (user == null) {             System.out.println("Unable to delete. User with id " + id + " not found");             return new ResponseEntity<User>(HttpStatus.NOT_FOUND);         }            userService.deleteUserById(id);         return new ResponseEntity<User>(HttpStatus.NO_CONTENT);     }               //------------------- Delete All Users --------------------------------------------------------            @RequestMapping(value = "/user/", method = RequestMethod.DELETE)     public ResponseEntity<User> deleteAllUsers() {         System.out.println("Deleting All Users");            userService.deleteAllUsers();         return new ResponseEntity<User>(HttpStatus.NO_CONTENT);     }    }</code></pre> <h3><br /> 运行应用程序</h3> <p>运行它并使用两个不同的客户端进行测试。<br /> <br />  </p> <h4>客户端1:Postman</h4> <p>尝试访问一个没有任何信息的资源,将获得一个401。</p> <p><img alt="SpringOAuth2_img1" class="img-thumbnail" src="/assist/images/blog/bbd70c98-9e02-46e9-b4b7-c4bdfadb1d93.png" /></p> <p>让我们得到令牌。首先添加一个<strong>客户端凭证</strong> [my-trusted-client / secret] 的授权头文件。</p> <p><img alt="SpringOAuth2_img2" class="img-thumbnail" src="/assist/images/blog/9c19299c-5b53-4dfb-97f7-30b81963ac1f.png" /></p> <p>单击更新请求,验证标题标签中的标题。</p> <p><img alt="SpringOAuth2_img3" class="img-thumbnail" src="/assist/images/blog/e404605e-f273-476d-a582-0147146e96d9.png" /></p> <p>发送POST请求时,您将收到包含响应<code>access-token</code>以及<code>refresh-token</code>。</p> <p><img alt="SpringOAuth2_img4" class="img-thumbnail" src="/assist/images/blog/a0e2e85e-0a1b-40f8-86e4-3c9f1c062276.png" /></p> <p>将这些令牌保存在某个地方,您将需要它们。现在,您可以使用此访问令牌(有效期为2分钟)来访问资源。</p> <p><img alt="SpringOAuth2_img5" class="img-thumbnail" src="/assist/images/blog/77e1a43f-c866-4b33-b1f0-f1c7d5232f89.png" /></p> <p>2分钟后,access-token将过期,您的进一步资源请求将失败。</p> <p><img alt="SpringOAuth2_img6" class="img-thumbnail" src="/assist/images/blog/dc75aaec-9ce3-4fe0-8bb7-c98e3e8c6054.png" /></p> <p>我们需要一个新的访问令牌。通过刷新令牌来触发一个帖子,以获得全新的访问令牌。</p> <p><img alt="SpringOAuth2_img7" class="img-thumbnail" src="/assist/images/blog/34a5980f-0d49-416a-adf4-fe4182056648.png" /></p> <p>使用这个新的访问令牌访问资源。</p> <p><img alt="SpringOAuth2_img8" class="img-thumbnail" src="/assist/images/blog/4e0b3771-92ed-438a-bc0f-15de7bc6fcf1.png" /></p> <p>刷新令牌也过期[10分钟]。之后,您将看到刷新请求失败。</p> <p><img alt="SpringOAuth2_img9" class="img-thumbnail" src="/assist/images/blog/c8760e84-cdfa-4760-ad3e-0ff07768c117.png" /></p> <p>这意味着您需要请求新的刷新+访问令牌,如步骤2中所示。</p> <h4>客户端2:基于RestTemplate的java应用程序</h4> <p>方法<strong>sendTokenRequest</strong>用于实际获取令牌。然后,我们收到的访问令牌将被用于每个请求。如果需要,您可以在下面的示例中轻松实现刷新令牌流。</p> <pre> <code class="language-java">package com.websystique.springmvc; import java.net.URI; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import org.apache.commons.codec.binary.Base64; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.web.client.RestTemplate; import com.websystique.springmvc.model.AuthTokenInfo; import com.websystique.springmvc.model.User; public class SpringRestClient { public static final String REST_SERVICE_URI = "http://localhost:8080/SpringSecurityOAuth2Example"; public static final String AUTH_SERVER_URI = "http://localhost:8080/SpringSecurityOAuth2Example/oauth/token"; public static final String QPM_PASSWORD_GRANT = "?grant_type=password&username=bill&password=abc123"; public static final String QPM_ACCESS_TOKEN = "?access_token="; /* * Prepare HTTP Headers. */ private static HttpHeaders getHeaders(){ HttpHeaders headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); return headers; } /* * Add HTTP Authorization header, using Basic-Authentication to send client-credentials. */ private static HttpHeaders getHeadersWithClientCredentials(){ String plainClientCredentials="my-trusted-client:secret"; String base64ClientCredentials = new String(Base64.encodeBase64(plainClientCredentials.getBytes())); HttpHeaders headers = getHeaders(); headers.add("Authorization", "Basic " + base64ClientCredentials); return headers; } /* * Send a POST request [on /oauth/token] to get an access-token, which will then be send with each request. */ @SuppressWarnings({ "unchecked"}) private static AuthTokenInfo sendTokenRequest(){ RestTemplate restTemplate = new RestTemplate(); HttpEntity<String> request = new HttpEntity<String>(getHeadersWithClientCredentials()); ResponseEntity<Object> response = restTemplate.exchange(AUTH_SERVER_URI+QPM_PASSWORD_GRANT, HttpMethod.POST, request, Object.class); LinkedHashMap<String, Object> map = (LinkedHashMap<String, Object>)response.getBody(); AuthTokenInfo tokenInfo = null; if(map!=null){ tokenInfo = new AuthTokenInfo(); tokenInfo.setAccess_token((String)map.get("access_token")); tokenInfo.setToken_type((String)map.get("token_type")); tokenInfo.setRefresh_token((String)map.get("refresh_token")); tokenInfo.setExpires_in((int)map.get("expires_in")); tokenInfo.setScope((String)map.get("scope")); System.out.println(tokenInfo); //System.out.println("access_token ="+map.get("access_token")+", token_type="+map.get("token_type")+", refresh_token="+map.get("refresh_token") //+", expires_in="+map.get("expires_in")+", scope="+map.get("scope"));; }else{ System.out.println("No user exist----------"); } return tokenInfo; } /* * Send a GET request to get list of all users. */ @SuppressWarnings({ "unchecked", "rawtypes" }) private static void listAllUsers(AuthTokenInfo tokenInfo){ Assert.notNull(tokenInfo, "Authenticate first please......"); System.out.println("\nTesting listAllUsers API-----------"); RestTemplate restTemplate = new RestTemplate(); HttpEntity<String> request = new HttpEntity<String>(getHeaders()); ResponseEntity<List> response = restTemplate.exchange(REST_SERVICE_URI+"/user/"+QPM_ACCESS_TOKEN+tokenInfo.getAccess_token(), HttpMethod.GET, request, List.class); List<LinkedHashMap<String, Object>> usersMap = (List<LinkedHashMap<String, Object>>)response.getBody(); if(usersMap!=null){ for(LinkedHashMap<String, Object> map : usersMap){ System.out.println("User : id="+map.get("id")+", Name="+map.get("name")+", Age="+map.get("age")+", Salary="+map.get("salary"));; } }else{ System.out.println("No user exist----------"); } } /* * Send a GET request to get a specific user. */ private static void getUser(AuthTokenInfo tokenInfo){ Assert.notNull(tokenInfo, "Authenticate first please......"); System.out.println("\nTesting getUser API----------"); RestTemplate restTemplate = new RestTemplate(); HttpEntity<String> request = new HttpEntity<String>(getHeaders()); ResponseEntity<User> response = restTemplate.exchange(REST_SERVICE_URI+"/user/1"+QPM_ACCESS_TOKEN+tokenInfo.getAccess_token(), HttpMethod.GET, request, User.class); User user = response.getBody(); System.out.println(user); } /* * Send a POST request to create a new user. */ private static void createUser(AuthTokenInfo tokenInfo) { Assert.notNull(tokenInfo, "Authenticate first please......"); System.out.println("\nTesting create User API----------"); RestTemplate restTemplate = new RestTemplate(); User user = new User(0,"Sarah",51,134); HttpEntity<Object> request = new HttpEntity<Object>(user, getHeaders()); URI uri = restTemplate.postForLocation(REST_SERVICE_URI+"/user/"+QPM_ACCESS_TOKEN+tokenInfo.getAccess_token(), request, User.class); System.out.println("Location : "+uri.toASCIIString()); } /* * Send a PUT request to update an existing user. */ private static void updateUser(AuthTokenInfo tokenInfo) { Assert.notNull(tokenInfo, "Authenticate first please......"); System.out.println("\nTesting update User API----------"); RestTemplate restTemplate = new RestTemplate(); User user = new User(1,"Tomy",33, 70000); HttpEntity<Object> request = new HttpEntity<Object>(user, getHeaders()); ResponseEntity<User> response = restTemplate.exchange(REST_SERVICE_URI+"/user/1"+QPM_ACCESS_TOKEN+tokenInfo.getAccess_token(), HttpMethod.PUT, request, User.class); System.out.println(response.getBody()); } /* * Send a DELETE request to delete a specific user. */ private static void deleteUser(AuthTokenInfo tokenInfo) { Assert.notNull(tokenInfo, "Authenticate first please......"); System.out.println("\nTesting delete User API----------"); RestTemplate restTemplate = new RestTemplate(); HttpEntity<String> request = new HttpEntity<String>(getHeaders()); restTemplate.exchange(REST_SERVICE_URI+"/user/3"+QPM_ACCESS_TOKEN+tokenInfo.getAccess_token(), HttpMethod.DELETE, request, User.class); } /* * Send a DELETE request to delete all users. */ private static void deleteAllUsers(AuthTokenInfo tokenInfo) { Assert.notNull(tokenInfo, "Authenticate first please......"); System.out.println("\nTesting all delete Users API----------"); RestTemplate restTemplate = new RestTemplate(); HttpEntity<String> request = new HttpEntity<String>(getHeaders()); restTemplate.exchange(REST_SERVICE_URI+"/user/"+QPM_ACCESS_TOKEN+tokenInfo.getAccess_token(), HttpMethod.DELETE, request, User.class); } public static void main(String args[]){ AuthTokenInfo tokenInfo = sendTokenRequest(); listAllUsers(tokenInfo); getUser(tokenInfo); createUser(tokenInfo); listAllUsers(tokenInfo); updateUser(tokenInfo); listAllUsers(tokenInfo); deleteUser(tokenInfo); listAllUsers(tokenInfo); deleteAllUsers(tokenInfo); listAllUsers(tokenInfo); } }</code></pre> <br /> 以上代码将产生以下输出: <pre> <code>AuthTokenInfo [access_token=fceed386-5923-4bf8-b193-1d76f95da4c4, token_type=bearer, refresh_token=29d28ee2-9d09-483f-a2d6-7f93e7a31667, expires_in=71, scope=read write trust] Testing listAllUsers API----------- User : id=1, Name=Sam, Age=30, Salary=70000.0 User : id=2, Name=Tom, Age=40, Salary=50000.0 User : id=3, Name=Jerome, Age=45, Salary=30000.0 User : id=4, Name=Silvia, Age=50, Salary=40000.0 Testing getUser API---------- User [id=1, name=Sam, age=30, salary=70000.0] Testing create User API---------- Location : http://localhost:8080/SpringSecurityOAuth2Example/user/5 Testing listAllUsers API----------- User : id=1, Name=Sam, Age=30, Salary=70000.0 User : id=2, Name=Tom, Age=40, Salary=50000.0 User : id=3, Name=Jerome, Age=45, Salary=30000.0 User : id=4, Name=Silvia, Age=50, Salary=40000.0 User : id=5, Name=Sarah, Age=51, Salary=134.0 Testing update User API---------- User [id=1, name=Tomy, age=33, salary=70000.0] Testing listAllUsers API----------- User : id=1, Name=Tomy, Age=33, Salary=70000.0 User : id=2, Name=Tom, Age=40, Salary=50000.0 User : id=3, Name=Jerome, Age=45, Salary=30000.0 User : id=4, Name=Silvia, Age=50, Salary=40000.0 User : id=5, Name=Sarah, Age=51, Salary=134.0 Testing delete User API---------- Testing listAllUsers API----------- User : id=1, Name=Tomy, Age=33, Salary=70000.0 User : id=2, Name=Tom, Age=40, Salary=50000.0 User : id=4, Name=Silvia, Age=50, Salary=40000.0 User : id=5, Name=Sarah, Age=51, Salary=134.0 Testing all delete Users API---------- Testing listAllUsers API----------- No user exist----------</code></pre>   <h3>项目结构</h3> <img alt="10" class="img-thumbnail" src="/assist/images/blog/aab904e5-276f-4fac-8bd4-3762adb1a28c.png" /> <h3>pom.xml</h3> <pre> <code class="language-xml"><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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.websystique.springmvc</groupId> <artifactId>SpringSecurityOAuth2Example</artifactId> <version>1.0.0</version> <packaging>war</packaging> <name>SpringSecurityOAuth2Example</name> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <springframework.version>4.3.1.RELEASE</springframework.version> <springsecurity.version>4.1.1.RELEASE</springsecurity.version> <springsecurityoauth2.version>2.0.10.RELEASE</springsecurityoauth2.version> <jackson.library>2.7.5</jackson.library> </properties> <dependencies> <!-- Spring --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${springframework.version}</version> </dependency> <!-- Spring Security --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>${springsecurity.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>${springsecurity.version}</version> </dependency> <!-- Spring Security OAuth2--> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>${springsecurityoauth2.version}</version> </dependency> <!-- Jackson libraries --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.library}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> <version>${jackson.library}</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.2</version> <configuration> <source>1.7</source> <target>1.7</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.4</version> <configuration> <warSourceDirectory>src/main/webapp</warSourceDirectory> <warName>SpringSecurityOAuth2Example</warName> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> </plugins> <finalName>SpringSecurityOAuth2Example</finalName> </build> </project></code></pre> <h4><strong><a href="http://websystique.com/?smd_process_download=1&download_id=2926" rel="external nofollow" target="_blank"><em><u>下载源代码</u></em></a></strong></h4> <br />  
  • Spring Security OAuth 2开发者指南

    本文主要翻译spring官方的基于spring security框架的oauth2开发指南,spring,oauth2,spring框架,Java编程<h2>介绍</h2> <p>这是用户指南的支持<a href="https://tools.ietf.org/html/draft-ietf-oauth-v2" rel="external nofollow" target="_blank"><code>OAuth 2.0</code></a>。对于OAuth 1.0,一切都是不同的,所以<a href="http://projects.spring.io/spring-security-oauth/docs/oauth1.html" rel="external nofollow" target="_blank">看到它的用户指南</a>。</p> <p>本用户指南分为两部分,第一部分为OAuth 2.0提供者,第二部分为OAuth 2.0客户端。对于提供商和客户端,示例代码的最佳来源是<a href="https://github.com/spring-projects/spring-security-oauth/tree/master/tests" rel="external nofollow" target="_blank">集成测试</a>和<a href="https://github.com/spring-projects/spring-security-oauth/tree/master/samples/oauth2" rel="external nofollow" target="_blank">示例应用程序</a>。</p> <h2>OAuth 2.0提供程序</h2> <p>OAuth 2.0提供者机制负责公开OAuth 2.0受保护的资源。该配置包括建立可独立或代表用户访问其受保护资源的OAuth 2.0客户端。提供者通过管理和验证用于访问受保护资源的OAuth 2.0令牌来实现。在适用的情况下,提供商还必须提供用户界面,以确认客户端可以被授权访问受保护资源(即确认页面)。</p> <h2>OAuth 2.0提供程序实现</h2> <p>OAuth 2.0中的提供者角色实际上是在授权服务和资源服务之间分割的,而有时它们位于同一个应用程序中,使用Spring Security OAuth,您可以选择在两个应用程序之间进行拆分,并且还可以共享多个资源服务授权服务。令牌的请求由Spring MVC控制器端点处理,对受保护资源的访问由标准的Spring Security请求过滤器处理。为了实现OAuth 2.0授权服务器,Spring Security过滤器链中需要以下端点:</p> <ul> <li><a href="http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/provider/endpoint/AuthorizationEndpoint.html" rel="external nofollow" target="_blank" title="授权终点"><code>AuthorizationEndpoint</code></a>用于服务授权请求。默认网址:<code>/oauth/authorize</code>。</li> <li><a href="http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/provider/endpoint/TokenEndpoint.html" rel="external nofollow" target="_blank" title="令牌终点"><code>TokenEndpoint</code></a>用于服务访问令牌的请求。默认网址:<code>/oauth/token</code>。</li> </ul> <p>实施OAuth 2.0资源服务器需要以下过滤器:</p> <ul> <li>将<a href="http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/provider/authentication/OAuth2AuthenticationProcessingFilter.html" rel="external nofollow" target="_blank" title="OAuth2AuthenticationProcessingFilter"><code>OAuth2AuthenticationProcessingFilter</code></a>用于加载给定的认证访问令牌请求的认证。</li> </ul> <p>对于所有OAuth 2.0提供程序功能,使用特殊的Spring OAuth <code>@Configuration</code>适配器简化了配置。还有一个用于OAuth配置的XML命名空间,并且模式位于<a href="http://www.springframework.org/schema/security/spring-security-oauth2.xsd" rel="external nofollow" target="_blank" title="oauth2.xsd">http://www.springframework.org/schema/security/spring-security-oauth2.xsd</a>。命名空间是<code>http://www.springframework.org/schema/security/oauth2</code>。</p> <h2>授权服务器配置</h2> <p>在配置授权服务器时,必须考虑客户端用于从最终用户获取访问令牌(例如授权代码,用户凭据,刷新令牌)的授权类型。服务器的配置用于提供客户端详细信息服务和令牌服务的实现,并且启用或禁用全局机制的某些方面。但是请注意,每个客户端都可以特别配置,以便能够使用某些授权机制和访问授权。也就是因为您的提供商配置为支持“客户端凭据”授权类型,并不意味着特定客户端被授权使用该授权类型。</p> <p>该<code>@EnableAuthorizationServer</code>注释用于配置OAuth 2.0授权服务器机制,以及任何<code>@Beans</code>实现<code>AuthorizationServerConfigurer</code>(有一个方便的适配器实现)。将以下功能委派给由Spring创建并传递到以下内容的单独配置程序<code>AuthorizationServerConfigurer</code>:</p> <ul> <li><code>ClientDetailsServiceConfigurer</code>:一个定义客户端详细信息服务的配置程序。客户端的详细信息可以初始化,也可以参考现有的存储。</li> <li><code>AuthorizationServerSecurityConfigurer</code>:定义令牌端点上的安全约束。</li> <li><code>AuthorizationServerEndpointsConfigurer</code>:定义授权和令牌端点和令牌服务。</li> </ul> <p>提供商配置的一个重要方面是授权代码提供给OAuth客户端(授权代码授权)的方式。授权代码由OAuth客户端通过将最终用户指向用户可以输入其凭据的授权页面获得,导致从提供商授权服务器重定向到具有授权码的OAuth客户端。这在OAuth 2规范中有详细说明。</p> <p>在XML中,有一个<code><authorization-server/></code>元素以类似的方式用于配置OAuth 2.0授权服务器。</p> <h3>配置客户端详细信息</h3> <p>将<code>ClientDetailsServiceConfigurer</code>(从您的回调<code>AuthorizationServerConfigurer</code>)可以用来在内存或JDBC实现客户的细节服务来定义的。客户端的重要属性是</p> <ul> <li><code>clientId</code>:(必填)客户端ID。</li> <li><code>secret</code>:(可信客户端需要)客户机密码(如果有)。</li> <li><code>scope</code>:客户受限的范围。如果范围未定义或为空(默认值),客户端不受范围限制。</li> <li><code>authorizedGrantTypes</code>:授予客户端使用授权的类型。默认值为空。</li> <li><code>authorities</code>授予客户的授权机构(普通的Spring Security权威机构)。</li> </ul> <p>客户端的详细信息可以通过直接访问底层商店(例如,在数据库表中<code>JdbcClientDetailsService</code>)或通过<code>ClientDetailsManager</code>接口(这两种实现<code>ClientDetailsService</code>也实现)来更新运行的应用程序。</p> <p>注意:JDBC服务的架构未与库一起打包(因为在实践中可能需要使用太多变体),而是可以从<a href="https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql" rel="external nofollow" target="_blank">github</a>中的<a href="https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql" rel="external nofollow" target="_blank">测试代码中</a>开始。</p> <h3>管理令牌</h3> <p>该<a href="http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/provider/token/AuthorizationServerTokenServices.html" rel="external nofollow" target="_blank" title="AuthorizationServerTokenServices"><code>AuthorizationServerTokenServices</code></a>接口定义了所必需的管理OAuth 2.0令牌的操作。请注意以下事项:</p> <ul> <li>当创建访问令牌时,必须存储身份验证,以便接受访问令牌的资源可以稍后引用。</li> <li>访问令牌用于加载用于授权其创建的认证。</li> </ul> <p>在创建<code>AuthorizationServerTokenServices</code>实现时,您可能需要考虑使用<a href="http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/provider/token/DefaultTokenServices.html" rel="external nofollow" target="_blank" title="DefaultTokenServices"><code>DefaultTokenServices</code></a>可插入的策略来更改访问令牌的格式和存储。默认情况下,它将通过随机值创建令牌,并处理除代表它的令牌持久化之外的所有内容<code>TokenStore</code>。默认存储是<a href="http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/provider/token/store/InMemoryTokenStore.html" rel="external nofollow" target="_blank" title="InMemoryTokenStore">内存中的实现</a>,但还有一些其他可用的实现。这是一个关于每一个的一些讨论的描述</p> <ul> <li> <p>默认值<code>InMemoryTokenStore</code>对于单个服务器是完全正常的(即,在发生故障的情况下,低流量和热备份备份服务器)。大多数项目可以从这里开始,也可以在开发模式下运行,以便轻松启动没有依赖关系的服务器。</p> </li> <li> <p>这<code>JdbcTokenStore</code>是同一件事的<a href="http://projects.spring.io/spring-security-oauth/docs/JdbcTokenStore" rel="external nofollow" target="_blank">JDBC版本</a>,它将令牌数据存储在关系数据库中。如果您可以在服务器之间共享数据库,则可以使用JDBC版本,如果只有一个,则扩展同一服务器的实例,或者如果有多个组件,则授权和资源服务器。要使用<code>JdbcTokenStore</code>你需要“spring-jdbc”的类路径。</p> </li> <li> <p>商店的<a href="http://projects.spring.io/spring-security-oauth/docs/%60JwtTokenStore%60" rel="external nofollow" target="_blank">JSON Web令牌(JWT)版本</a>将所有关于授权的数据编码到令牌本身(因此,根本没有后端存储是一个显着的优势)。一个缺点是您不能轻易地撤销访问令牌,因此通常被授予短期到期权,撤销在刷新令牌处理。另一个缺点是,如果您在其中存储了大量用户凭据信息,令牌可能会变得非常大。这<code>JwtTokenStore</code>不是一个真正的“商店”,因为它不会保留任何数据,但它在翻译令牌值和验证信息之间起着相同的作用<code>DefaultTokenServices</code>。</p> </li> </ul> <p>注意:JDBC服务的架构未与库一起打包(因为在实践中可能需要使用太多变体),而是可以从<a href="https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql" rel="external nofollow" target="_blank">github</a>中的<a href="https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql" rel="external nofollow" target="_blank">测试代码中</a>开始。确保<code>@EnableTransactionManagement</code>在创建令牌时,防止在竞争相同行的客户端应用程序之间发生冲突。还要注意,示例模式有明确的<code>PRIMARY KEY</code>声明 - 这些在并发环境中也是必需的。</p> <h3>JWT令牌</h3> <p>要使用JWT令牌,您需要<code>JwtTokenStore</code>在授权服务器中。资源服务器还需要能够对令牌进行解码,因此它<code>JwtTokenStore</code>具有依赖性<code>JwtAccessTokenConverter</code>,并且授权服务器和资源服务器都需要相同的实现。默认情况下,令牌被签名,资源服务器还必须能够验证签名,因此它需要与授权服务器(共享密钥或对称密钥)相同的对称(签名)密钥,或者需要公共密钥(验证者密钥),其与授权服务器中的私钥(签名密钥)匹配(公私属或非对称密钥)。公钥(如果可用)由<code>/oauth/token_key</code>端点上的授权服务器公开,默认情况下,访问规则为“denyAll()”。<code>AuthorizationServerSecurityConfigurer</code></p> <p>要使用<code>JwtTokenStore</code>你需要的“spring-security-jwt”你的类路径(你可以在与Spring OAuth相同的github仓库中找到它,但发行周期不同)。</p> <h3>赠款类型</h3> <p><code>AuthorizationEndpoint</code>可以通过以下方式配置支持的授权类型<code>AuthorizationServerEndpointsConfigurer</code>。默认情况下,所有授权类型均受支持,除了密码(有关如何切换它的详细信息,请参见下文)。以下属性会影响授权类型:</p> <ul> <li><code>authenticationManager</code>:通过注入密码授权被打开<code>AuthenticationManager</code>。</li> <li><code>userDetailsService</code>:如果您注入<code>UserDetailsService</code>或者全局配置(例如a <code>GlobalAuthenticationManagerConfigurer</code>),则刷新令牌授权将包含对用户详细信息的检查,以确保该帐户仍然活动</li> <li><code>authorizationCodeServices</code>:定义<code>AuthorizationCodeServices</code>授权代码授权的授权代码服务(实例)。</li> <li><code>implicitGrantService</code>:在批准期间管理状态。</li> <li><code>tokenGranter</code>:(<code>TokenGranter</code>完全控制授予和忽略上述其他属性)</li> </ul> <p>在XML授予类型中包含作为子元素<code>authorization-server</code>。</p> <h3>配置端点URL</h3> <p>该<code>AuthorizationServerEndpointsConfigurer</code>有一个<code>pathMapping()</code>方法。它有两个参数:</p> <ul> <li>端点的默认(框架实现)URL路径</li> <li>需要的自定义路径(以“/”开头)</li> </ul> <p>由框架提供的URL路径<code>/oauth/authorize</code>(授权端点)<code>/oauth/token</code>(令牌端点)<code>/oauth/confirm_access</code>(用户发布批准此处)<code>/oauth/error</code>(用于在授权服务器中呈现错误)<code>/oauth/check_token</code>(由资源服务器用于解码访问令牌) ,并且<code>/oauth/token_key</code>(如果使用JWT令牌,则公开用于令牌验证的公钥)。</p> <p>注意,授权端点<code>/oauth/authorize</code>(或其映射替代方案)应使用Spring Security进行保护,以便只有经过身份验证的用户才能访问。例如使用标准的Spring Security <code>WebSecurityConfigurer</code>:</p> <pre> <code class="language-java"> @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests().antMatchers("/login").permitAll().and() // default protection for all resources (including /oauth/authorize) .authorizeRequests() .anyRequest().hasRole("USER") // ... more configuration, e.g. for form login } </code></pre> <p>注意:如果您的授权服务器也是资源服务器,那么还有另一个优先级较低的安全过滤器链控制API资源。通过访问令牌来保护这些请求,您需要他们的路径<em>不</em>与主用户面临的过滤器链中的路径匹配,因此请务必包含仅在<code>WebSecurityConfigurer</code>上述中选择非API资源的请求匹配器。</p> <p>默认情况下,通过Spring OAuth在<code>@Configuration</code>使用客户机密码的HTTP Basic认证的支持中为您保护令牌端点。在XML中不是这样(因此应该明确保护)。</p> <p>在XML中,<code><authorization-server/></code>元素具有一些可以用于以类似方式更改默认端点URL的属性。该<code>/check_token</code>端点必须(与显式启用<code>check-token-enabled</code>属性)。</p> <h2>自定义UI</h2> <p>大多数授权服务器端点主要由机器使用,但是有一些资源需要一个UI,而这些资源是GET <code>/oauth/confirm_access</code>和HTML响应<code>/oauth/error</code>。它们是在框架中使用白名单实现提供的,因此授权服务器的大多数真实世界实例都希望提供自己的实例,以便他们可以控制样式和内容。所有您需要做的是<code>@RequestMappings</code>为这些端点提供一个Spring MVC控制器,并且框架默认在调度程序中占用较低的优先级。在<code>/oauth/confirm_access</code>端点中,您可以期待<code>AuthorizationRequest</code>绑定到会话中,携带所有需要用户查询的数据(默认的实现是<code>WhitelabelApprovalEndpoint</code>这样查找起始点复制)。<code>/oauth/authorize</code>您可以从该请求中获取所有数据,然后根据需要进行渲染,然后所有用户需要执行的操作都是回复有关批准或拒绝授权的信息。请求参数直接传递给您<code>UserApprovalHandler</code>,<code>AuthorizationEndpoint</code>所以您可以随便解释数据。默认<code>UserApprovalHandler</code>取决于您是否已经提供了一个<code>ApprovalStore</code>在你的<code>AuthorizationServerEndpointsConfigurer</code>(在这种情况下,它是一个<code>ApprovalStoreUserApprovalHandler</code>)或不(在这种情况下,它是一个<code>TokenStoreUserApprovalHandler</code>)。标准审批处理程序接受以下内容:默认取决于您是否已经提供了一个在你的(在这种情况下,它是一个)或不(在这种情况下,它是一个)。标准审批处理程序接受以下内容:默认取决于您是否已经提供了一个在你的(在这种情况下,它是一个)或不(在这种情况下,它是一个)。标准审批处理程序接受以下内容:</p> <ul> <li> <p><code>TokenStoreUserApprovalHandler</code>:简单的是/否决定通过<code>user_oauth_approval</code>等于“真”或“假”。</p> </li> <li> <p><code>ApprovalStoreUserApprovalHandler</code>:一组<code>scope.*</code>参数键与“*”等于所请求的范围。参数的值可以是“true”或“approved”(如果用户批准了授权),则该用户被认为已经拒绝了该范围。如果批准了至少一个范围,则赠款是成功的。</p> </li> </ul> <p>注意:不要忘记在您为用户呈现的表单中包含CSRF保护。默认情况下,Spring Security正期待一个名为“_csrf”的请求参数(它在请求属性中提供值)。有关更多信息,请参阅Spring Security用户指南,或查看whitelabel实现的指导。</p> <h3>执行SSL</h3> <p>普通HTTP对于测试是很好的,但授权服务器只能在生产中使用SSL。您可以在安全容器或代理服务器后面运行应用程序,如果正确设置代理和容器(这与OAuth2无关),则应该可以正常运行。您也可能希望使用Spring Security <code>requiresChannel()</code>限制来保护端点。对于<code>/authorize</code>端点,由您来做,作为您正常应用程序安全性的一部分。对于<code>/token</code>端点<code>AuthorizationServerEndpointsConfigurer</code>,可以使用该<code>sslOnly()</code>方法设置一个标志。在这两种情况下,安全通道设置是可选的,但是如果Spring Security在不安全的通道上检测到请求,则会导致Spring Security重定向到安全通道。</p> <h2>自定义错误处理</h2> <p>授权服务器中的错误处理使用标准Spring MVC功能,即<code>@ExceptionHandler</code>端点本身的方法。用户还可以向<code>WebResponseExceptionTranslator</code>端点自身提供这些改变响应内容的最佳方式,而不是渲染方式。在授权<code>HttpMesssageConverters</code>端点的情况下,在令牌端点和OAuth错误视图(<code>/oauth/error</code>)的情况下,异常呈现(可以添加到MVC配置中)。该白色标签错误的端点提供了HTML的响应,但用户可能需要提供自定义实现(如只需添加一个<code>@Controller</code>带<code>@RequestMapping("/oauth/error")</code>)。</p> <h2>将用户角色映射到范围</h2> <p>限制令牌范围不仅仅是分配给客户端的范围,还可以根据用户自己的权限来进行限制。如果您在其中使用<code>DefaultOAuth2RequestFactory</code>,<code>AuthorizationEndpoint</code>则可以设置一个标志<code>checkUserScopes=true</code>,以将允许的范围限制为仅与那些与用户角色匹配的范围。你也可以注入<code>OAuth2RequestFactory</code>,<code>TokenEndpoint</code>但只有工作(即密码授权),如果你也安装一个<code>TokenEndpointAuthenticationFilter</code>- 你只需要在HTTP之后添加该过滤器<code>BasicAuthenticationFilter</code>。当然,您还可以实现自己的规则,将作用域映射到角色并安装自己的版本<code>OAuth2RequestFactory</code>。将<code>AuthorizationServerEndpointsConfigurer</code>让你注入一个定制的<code>OAuth2RequestFactory</code>,所以你可以使用该功能来建立一个工厂,如果你使用<code>@EnableAuthorizationServer</code>。</p> <h2>资源服务器配置</h2> <p>资源服务器(可以与授权服务器或单独的应用程序相同)提供受OAuth2令牌保护的资源。Spring OAuth提供了实现此保护的Spring Security认证过滤器。您可以<code>@EnableResourceServer</code>在<code>@Configuration</code>类上打开它,并使用a进行配置(必要时)<code>ResourceServerConfigurer</code>。可以配置以下功能:</p> <ul> <li><code>tokenServices</code>:定义令牌服务的bean(实例<code>ResourceServerTokenServices</code>)。</li> <li><code>resourceId</code>:资源的ID(可选,但建议并由验证服务器验证,如果存在)。</li> <li>其他扩展点(例如<code>tokenExtractor</code>从传入请求中提取令牌)</li> <li>请求匹配的受保护资源(默认为全部)</li> <li>受保护资源的访问规则(默认为“已验证”)</li> <li><code>HttpSecurity</code>Spring Security中配置程序允许的受保护资源的其他自定义</li> </ul> <p>该<code>@EnableResourceServer</code>注释添加类型的过滤器<code>OAuth2AuthenticationProcessingFilter</code>自动Spring Security的过滤器链。</p> <p>在XML中有一个<code><resource-server/></code>带有<code>id</code>属性的元素- 这是一个servlet的bean ID,<code>Filter</code>然后可以手动添加到标准的Spring Security链。</p> <p>您<code>ResourceServerTokenServices</code>是与授权服务器的合同的另一半。如果资源服务器和授权服务器在同一个应用程序中,然后使用,<code>DefaultTokenServices</code>那么您不需要太费心思考,因为它实现了所有必要的接口,因此它自动一致。如果您的资源服务器是一个单独的应用程序,那么您必须确保与授权服务器的功能相匹配,并提供一个<code>ResourceServerTokenServices</code>正确的解码令牌。与授权服务器一样,您经常可以使用该<code>DefaultTokenServices</code>选项,并且选择主要通过<code>TokenStore</code>(后端存储或本地编码)来表达。<code>RemoteTokenServices</code>一个替代方案是Spring OAuth功能(不是规范的一部分),允许资源服务器通过授权服务器(<code>/oauth/check_token</code>)上的HTTP资源解码令牌。<code>RemoteTokenServices</code>如果资源服务器中没有大量的流量(每个请求都必须与授权服务器进行验证),或者如果能够缓存结果,那么它们是方便的。要使用<code>/oauth/check_token</code>端点,您需要通过更改其访问规则(默认为“denyAll()”)来公开它<code>AuthorizationServerSecurityConfigurer</code>,例如</p> <pre> <code class="language-java"> @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')").checkTokenAccess( "hasAuthority('ROLE_TRUSTED_CLIENT')"); } </code></pre> <p>在这个例子中,我们配置了<code>/oauth/check_token</code>端点和<code>/oauth/token_key</code>端点(所以信任的资源可以获得JWT验证的公钥)。这两个端点受到使用客户端凭据的HTTP基本身份验证的保护。</p> <h3>配置OAuth感知表达式处理程序</h3> <p>您可能希望利用Spring Security <a href="http://docs.spring.io/spring-security/site/docs/3.2.5.RELEASE/reference/htmlsingle/#el-access" rel="external nofollow" target="_blank" title="表达式访问控制">基于表达式的访问控制</a>。表达式处理程序将默认在<code>@EnableResourceServer</code>安装程序中注册。这些表达式包括<em>#oauth2.clientHasRole</em>,<em>#oauth2.clientHasAnyRole</em>和<em>#oath2.denyClient</em>,可用于根据oauth客户端的角色提供访问(请参阅<code>OAuth2SecurityExpressionMethods</code>全面的列表)。在XML中,您可以<code>expression-handler</code>使用常规<code><http/></code>安全配置的元素注册一个oauth感知表达式处理程序。</p> <h2>OAuth 2.0客户端</h2> <p>OAuth 2.0客户端机制负责访问其他服务器的OAuth 2.0保护资源。该配置包括建立用户可能访问的相关受保护资源。客户端还可能需要提供用于存储用户的授权码和访问令牌的机制。</p> <h3>受保护的资源配置</h3> <p>受保护的资源(或“远程资源”)可以使用类型的bean定义来定义<a href="http://projects.spring.io/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/client/resource/OAuth2ProtectedResourceDetails.java" rel="external nofollow" target="_blank"><code>OAuth2ProtectedResourceDetails</code></a>。受保护的资源具有以下属性:</p> <ul> <li><code>id</code>:资源的id。该id仅由客户端用于查找资源; 它在OAuth协议中从未使用过。它也被用作bean的id。</li> <li><code>clientId</code>:OAuth客户端ID。这是OAuth提供商识别您的客户端的ID。</li> <li><code>clientSecret</code>:与资源相关的秘密。默认情况下,没有密码为空。</li> <li><code>accessTokenUri</code>:提供访问令牌的提供者OAuth端点的URI。</li> <li><code>scope</code>:逗号分隔的字符串列表,指定对资源的访问范围。默认情况下,不指定范围。</li> <li><code>clientAuthenticationScheme</code>:您的客户端用于向访问令牌端点进行身份验证的方案。建议的值:“http_basic”和“form”。默认值为“http_basic”。请参阅OAuth 2规范的第2.1节。</li> </ul> <p>不同的授权类型具有不同的具体实现<code>OAuth2ProtectedResourceDetails</code>(例如<code>ClientCredentialsResource</code>,对于“client_credentials”授权类型)。对于需要用户授权的授权类型,还有一个其他属性:</p> <ul> <li><code>userAuthorizationUri</code>:如果用户需要授权访问资源,则用户将被重定向到的uri。请注意,这并不总是需要,具体取决于支持哪个OAuth 2配置文件。</li> </ul> <p>在XML中有一个<code><resource/></code>可以用来创建类型的bean的元素<code>OAuth2ProtectedResourceDetails</code>。它具有匹配上述所有属性的属性。</p> <h3>客户端配置</h3> <p>对于OAuth 2.0客户端,使用简化配置<code>@EnableOAuth2Client</code>。这有两件事情:</p> <ul> <li> <p>创建一个过滤器bean(带有ID <code>oauth2ClientContextFilter</code>)来存储当前的请求和上下文。在需要在请求期间进行身份验证的情况下,管理重定向到和从OAuth认证uri。</p> </li> <li> <p><code>AccessTokenRequest</code>在请求范围中创建一个类型的bean 。授权代码(或隐式)授权客户端可以使用这种方式来保持与个别用户的状态相关。</p> </li> </ul> <p>过滤器必须连接到应用程序中(例如,使用 同一名称的Servlet初始化程序或<code>web.xml</code>配置<code>DelegatingFilterProxy</code>)。</p> <p>本<code>AccessTokenRequest</code>可以在使用 <code>OAuth2RestTemplate</code>这样的:</p> <pre> <code class="language-java">@Autowired private OAuth2ClientContext oauth2Context; @Bean public OAuth2RestTemplate sparklrRestTemplate() { return new OAuth2RestTemplate(sparklr(), oauth2Context); } </code></pre> <p>在会话范围中放置OAuth2ClientContext(为您),以保持不同用户的状态分开。没有了,您将不得不自己在服务器上管理等效的数据结构,将传入的请求映射到用户,并将每个用户与单独的实例相关联<code>OAuth2ClientContext</code>。</p> <p>在XML中有一个<code><client/></code>带有<code>id</code>属性的元素- 这是一个servlet的bean id,<code>Filter</code>在这种<code>@Configuration</code>情况下必须映射为一个<code>DelegatingFilterProxy</code>(具有相同名称)。</p> <h3>访问受保护的资源</h3> <p>一旦您提供了资源的所有配置,您现在可以访问这些资源。用于访问这些资源的建议的方法是通过使用<a href="http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html" rel="external nofollow" target="_blank" title="RestTemplate">所述<code>RestTemplate</code>在弹簧3引入</a>。Spring Security的OAuth提供<a href="http://projects.spring.io/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/client/OAuth2RestTemplate.java" rel="external nofollow" target="_blank">了</a>只需要提供一个实例的<a href="http://projects.spring.io/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/client/OAuth2RestTemplate.java" rel="external nofollow" target="_blank">RestTemplate的扩展</a><a href="http://projects.spring.io/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/client/resource/OAuth2ProtectedResourceDetails.java" rel="external nofollow" target="_blank"><code>OAuth2ProtectedResourceDetails</code></a>。要使用用户令牌(授权代码授权),您应该考虑使用创建一些请求和会话作用域上下文对象的<code>@EnableOAuth2Client</code>配置(或XML等效项<code><oauth:rest-template/></code>),以便不同用户的请求在运行时不会相冲突。</p> <p>作为一般规则,Web应用程序不应使用密码授权,因此<code>ResourceOwnerPasswordResourceDetails</code>如果可以支持,请避免使用<code>AuthorizationCodeResourceDetails</code>。如果您非常需要从Java客户端工作的密码授权,则使用相同的机制来配置您的凭据,并将凭据<code>OAuth2RestTemplate</code>添加到<code>AccessTokenRequest</code>(这是一个<code>Map</code>短暂的),而不是<code>ResourceOwnerPasswordResourceDetails</code>(在所有访问令牌之间共享)。</p> <h3>在客户端中持久化令牌</h3> <p>客户端并不<em>需要</em>坚持令牌,但它可以很好的为不要求用户每次在客户端应用程序重新启动时批准新的代金券授予。该<a href="http://projects.spring.io/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/client/token/ClientTokenServices.java" rel="external nofollow" target="_blank"><code>ClientTokenServices</code></a>接口定义了所必需的持续的OAuth为特定用户2.0的令牌的动作。提供了一个JDBC实现,但如果您希望实现自己的服务来将持久性数据库中的访问令牌和关联的身份验证实例存储起来,那么您可以使用。如果要使用此功能,您需要提供一个专门配置<code>TokenProvider</code>的<code>OAuth2RestTemplate</code>如</p> <pre> <code class="language-java">@Bean @Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES) public OAuth2RestOperations restTemplate() { OAuth2RestTemplate template = new OAuth2RestTemplate(resource(), new DefaultOAuth2ClientContext(accessTokenRequest)); AccessTokenProviderChain provider = new AccessTokenProviderChain(Arrays.asList(new AuthorizationCodeAccessTokenProvider())); provider.setClientTokenServices(clientTokenServices()); return template; } </code></pre> <h2>外部OAuth2提供商客户端的定制</h2> <p>一些外部OAuth2提供者(例如<a href="https://developers.facebook.com/docs/authentication" rel="external nofollow" target="_blank" title="Facebook">Facebook</a>)不能正确地实现规范,或者他们只是坚持使用旧版本的规范,而不是Spring Security OAuth。要在客户端应用程序中使用这些提供程序,您可能需要调整客户端基础架构的各个部分。</p> <p>以Facebook为例,应用程序中有一个Facebook功能<code>tonr2</code>(您需要更改配置以添加您自己的,有效的客户端ID和密码 - 它们很容易在Facebook网站上生成)。</p> <p>Facebook令牌响应在令牌的到期时间(它们使用<code>expires</code>而不是<code>expires_in</code>)中也包含不符合规定的JSON条目,因此,如果要在应用程序中使用到期时间,则必须使用自定义手动解码<code>OAuth2SerializationService</code>。</p>
  • Spring Security 配置多个Authentication Providers认证器

    Spring Security 配置多个Authentication Providers认证器Spring Security 配置多个Authentication Providers认证器 <h2><strong>1.概述</strong></h2> 在这篇快速文章中,我们将重点介绍如何使用多种机制在Spring Security中对用户进行身份验证。<br /> 我们将通过配置多个身份验证提供程序来完成此操作<br />   <h2><strong>2. Authentication Providers</strong></h2> AuthenticationProvider是从特定存储库(如数据库,LDAP,自定义第三方来源等)获取用户信息的抽象概念。 它使用获取的用户信息来验证提供的凭证。<br /> <br /> 简而言之,当定义多个身份验证提供程序时,提供程序将按其声明的顺序进行查询。<br /> <br /> 为了快速演示,我们将配置两个身份验证提供程序 - 一个定制身份验证提供程序和一个内存身份验证提供程序 <h2><strong>3. Maven 依赖</strong></h2> 我们首先在我们的Web应用程序中添加必要的Spring Security依赖项: <pre> <code class="language-xml"><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency></code></pre> 没有Spring Boot: <pre> <code class="language-xml"><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>5.0.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> <version>5.0.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>5.0.4.RELEASE</version> </dependency></code></pre> <h2><strong>4. 自定义 Authentication Provider</strong></h2> 现在让我们通过实现AuthneticationProvider接口来创建一个自定义身份验证提供程序。<br /> <br /> 我们将实施验证方法 - 它将尝试验证。 输入的Authentication对象包含用户提供的用户名和密码凭证。<br /> <br /> 如果身份验证成功,则身份验证方法会返回完全填充的身份验证对象。 如果身份验证失败,则会抛出AuthenticationException类型的异常:<br />   <pre> <code class="language-java">@Component public class CustomAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication auth) throws AuthenticationException { String username = auth.getName(); String password = auth.getCredentials() .toString(); if ("externaluser".equals(username) && "pass".equals(password)) { return new UsernamePasswordAuthenticationToken (username, password, Collections.emptyList()); } else { throw new BadCredentialsException("External system authentication failed"); } } @Override public boolean supports(Class<?> auth) { return auth.equals(UsernamePasswordAuthenticationToken.class); } }</code></pre> 当然,这是我们这里例子的一个简单实现。 <h2><strong>5. 配置Multiple Authentication Providers</strong></h2> 现在让我们将CustomAuthenticationProvider和一个内存中验证提供程序添加到我们的Spring Security配置中。 <h3><strong>5.1. Java Configuration</strong></h3> 在我们的配置类中,现在让我们使用AuthenticationManagerBuilder创建和添加身份验证提供程序。<br /> <br /> 首先,通过使用inMemoryAuthentication(),使用CustomAuthenticationProvider,然后使用内存认证提供程序。<br /> <br /> 我们还确保访问URL模式“/ api / **”需要进行身份验证: <pre> <code class="language-java">@EnableWebSecurity public class MultipleAuthProvidersSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired CustomAuthenticationProvider customAuthProvider; @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(customAuthProvider); auth.inMemoryAuthentication() .withUser("memuser") .password(encoder().encode("pass")) .roles("USER"); } @Override protected void configure(HttpSecurity http) throws Exception { http.httpBasic() .and() .authorizeRequests() .antMatchers("/api/**") .authenticated(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }</code></pre>   <h3><strong>5.2. XML Configuration</strong></h3> 或者,如果我们想使用XML配置而不是Java配置: <pre> <code class="language-xml"><security:authentication-manager> <security:authentication-provider> <security:user-service> <security:user name="memuser" password="pass" authorities="ROLE_USER" /> </security:user-service> </security:authentication-provider> <security:authentication-provider ref="customAuthenticationProvider" /> </security:authentication-manager> <security:http> <security:http-basic /> <security:intercept-url pattern="/api/**" access="isAuthenticated()" /> </security:http></code></pre> <h2><strong>6. 应用Application</strong></h2> 接下来,让我们创建一个由我们的两个身份验证提供程序保护的简单REST端点。<br /> <br /> 要访问此端点,必须提供有效的用户名和密码。 我们的身份验证提供程序将验证凭据并确定是否允许访问: <pre> <code class="language-java">@RestController public class MultipleAuthController { @GetMapping("/api/ping") public String getPing() { return "OK"; } }</code></pre>   <h2><strong>7. 测试</strong></h2> 最后,我们现在测试对安全应用程序的访问权限。 只有提供有效凭证时才允许访问: <pre> <code class="language-java">@Autowired private TestRestTemplate restTemplate; @Test public void givenMemUsers_whenGetPingWithValidUser_thenOk() { ResponseEntity<String> result = makeRestCallToGetPing("memuser", "pass"); assertThat(result.getStatusCodeValue()).isEqualTo(200); assertThat(result.getBody()).isEqualTo("OK"); } @Test public void givenExternalUsers_whenGetPingWithValidUser_thenOK() { ResponseEntity<String> result = makeRestCallToGetPing("externaluser", "pass"); assertThat(result.getStatusCodeValue()).isEqualTo(200); assertThat(result.getBody()).isEqualTo("OK"); } @Test public void givenAuthProviders_whenGetPingWithNoCred_then401() { ResponseEntity<String> result = makeRestCallToGetPing(); assertThat(result.getStatusCodeValue()).isEqualTo(401); } @Test public void givenAuthProviders_whenGetPingWithBadCred_then401() { ResponseEntity<String> result = makeRestCallToGetPing("user", "bad_password"); assertThat(result.getStatusCodeValue()).isEqualTo(401); } private ResponseEntity<String> makeRestCallToGetPing(String username, String password) { return restTemplate.withBasicAuth(username, password) .getForEntity("/api/ping", String.class, Collections.emptyMap()); } private ResponseEntity<String> makeRestCallToGetPing() { return restTemplate .getForEntity("/api/ping", String.class, Collections.emptyMap()); }</code></pre> <h2><strong>8. 总结</strong></h2> 在本快速教程中,我们已经看到了如何在Spring Security中配置多个身份验证提供程序。 我们使用自定义身份验证提供程序和内存身份验证提供程序确保了一个简单应用程序。<br /> <br /> 我们还编写了测试,以验证对我们应用程序的访问需要至少有一个身份验证提供程序可以验证的凭据。
  • Java编程之Spring Cloud Hystrix Circuit熔断/断路

    Java编程之Spring Cloud Hystrix Circuit熔断/断路<h2>一、本博客讲解内容</h2>   学习怎么利用spring cloud 的Hystrix组件,在调用底层微服务时实现断路器。通常需要在应用程序中启用容错功能,因为某些底层服务会永久地降低/抛出错误,因此我们需要自动返回不同的程序执行路径。这与使用大量底层微服务的生态系统的分布式计算风格有关。这就是断路器模式的作用,而Hystrix是建立断路器的一个工具。 <h2>二、Hystrix Example for real impatient</h2> Hystrix配置主要分4个步骤。 <h3>2.1添加Hystrix starter 和dashboard的maven依赖</h3> <pre> <code class="language-xml"><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId> </dependency></code></pre> <h3>2.2添加注解 <code>@EnableCircuitBreaker</code></h3> <h3>2.3添加注解<code>@EnableHystrixDashboard</code></h3> <h3>2.4添加注解 <code>@HystrixCommand(fallbackMethod = "myFallbackMethod")</code></h3> <h2>三、什么是断路器模式?</h2> <p>    如果我们在基于微服务的体系结构上设计我们的系统,我们通常会开发许多微服务,这些服务将会在实现特定的业务目标时相互影响。现在,我们所有人都可以假设,如果所有服务都启动并运行,并且每个服务的响应时间都令人满意,那么这将给出预期的结果。</p> <p>    如果当前生态系统中的任何服务都有一些问题,并且停止了对请求的服务,那么将会发生什么。这将导致时间/异常,整个生态系统将由于这一单点故障而变得不稳定。</p> <p>    在这里,电路断路器模式很方便,当它看到任何这样的场景时,它会将流量重定向到一个后退的路径。此外,它还会密切监视有缺陷的服务,一旦服务恢复正常,就会恢复流量。</p> <p>    断路器是一种包装方法的服务调用和监视服务健康,一旦它得到了一些问题,断路器旅行,所有进一步调用goto断路器回落,最后恢复后自动服务回来! !那很酷对吗?<br /> <img alt="seq" class="img-thumbnail" src="/assist/images/blog/807b7fbeb77f427daba28f046a7f1dc8.jpg" /></p> <h2>四、Hystrix Circuit 断路由的演示</h2> 对于演示电路断路器,我们将创建以下两个微服务,首先依赖于另一个。 <ul> <li>学生微服务——它将提供学生实体的一些基本功能。它将是一个基于REST的服务。我们将从学校的服务中调用这项服务来理解断路器。它将在本地主机端口8098上运行。</li> <li>学校微服务——再一次简单的基于REST的微服务,我们将使用Hystrix实现断路器。学生服务将从这里被调用,当学生服务不可用时,我们将测试下降的路径。它将在本地主机端口9098上运行。</li> </ul> <strong>演示所需要的环境:</strong> <ul> <li>Java 1.8</li> <li>Eclipse as IDE</li> <li>Maven as build tool</li> <li>Spring cloud Hystrix as circuit breaker framework</li> <li>Spring boot</li> <li>Spring Rest</li> </ul> <h3>4.1创建学生服务</h3> 按照以下步骤创建和运行学生服务——一个简单的REST服务,提供学生实体的一些基本功能。 <h4><br /> 4.1.1创建spring boot项目</h4> 去spring boot的创建网站创建一个spring boot项目,包括以下依赖<code>Web</code>, <code>Rest Repositories</code>and <code>Actuator。</code>提供其他maven GAV坐标并下载该项目。<br /> <img alt="学生服务" class="img-thumbnail" src="/assist/images/blog/7550d951ad054044bf6db5b0b9ffcb8b.jpg" />解压缩并将项目导入到Eclipse中,作为现有的maven项目。在此步骤中,将从maven存储库下载所有必需的依赖项。 <h4>4.1.2服务的端口设置</h4> 打开文件application.properties并且添加以下配置内容: <pre> <code>server.port = 8098</code></pre> 这将使该应用程序在缺省端口8098上运行。我们可以通过提供-Dserver来轻松地覆盖这一点。在启动服务器时Dserver.port=XXXX的参数。 <h4>4.1.3创建学生的微服务REST接口</h4> 现在,添加一个名为student servicecon掣的REST控制器类,并公开一个REST端点,以获取特定学校的所有学生详细信息。我们在这里暴露/ getStudentDetailsForSchool / { schoolname }端点服务于商业目的。为了简单起见,我们对学生的详细信息进行了编码。<br /> <strong>StudentServiceController.java</strong> <pre> <code class="language-java">import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import com.example.howtodoinjava.springhystrixstudentservice.domain.Student; @RestController public class StudentServiceController { private static Map<String, List<Student>> schooDB = new HashMap<String, List<Student>>(); static { schooDB = new HashMap<String, List<Student>>(); List<Student> lst = new ArrayList<Student>(); Student std = new Student("Sajal", "Class IV"); lst.add(std); std = new Student("Lokesh", "Class V"); lst.add(std); schooDB.put("abcschool", lst); lst = new ArrayList<Student>(); std = new Student("Kajal", "Class III"); lst.add(std); std = new Student("Sukesh", "Class VI"); lst.add(std); schooDB.put("xyzschool", lst); } @RequestMapping(value = "/getStudentDetailsForSchool/{schoolname}", method = RequestMethod.GET) public List<Student> getStudents(@PathVariable String schoolname) { System.out.println("Getting Student details for " + schoolname); List<Student> studentList = schooDB.get(schoolname); if (studentList == null) { studentList = new ArrayList<Student>(); Student std = new Student("Not Found", "N/A"); studentList.add(std); } return studentList; } }</code></pre> <strong>Student.java</strong> <pre> <code class="language-java">public class Student { private String name; private String className; public Student(String name, String className) { super(); this.name = name; this.className = className; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getClassName() { return className; } public void setClassName(String className) { this.className = className; } }</code></pre> <h4>4.1.4测试学生微服务接口</h4>     现在使用maven命令mvn clean install构建,并且使用命令java -jar target\spring-hystrix-student-service-0.0.1-SNAPSHOT.jar启动。这将启动默认端口8098的学生服务。<br />   用浏览器打开地址:<code>http://localhost:8098/getStudentDetailsForSchool/abcschool</code>.<br />   浏览器显示内容如下:<br /> <img alt="浏览器显示" class="img-thumbnail" src="/assist/images/blog/f0d2ae03ad634c53a5074336ded781f0.jpg" /> <h3>4.2创建学校服务并启用Hystrix</h3> 与学生服务类似,为学校创建另一项微服务。它将在内部调用已开发的学生服务。 <h4>4.2.1生成spring boot项目</h4> 创建一个spring boot项目,并且添加以下依赖: <ul> <li><strong>Web</strong> – REST Endpoints</li> <li><strong>Actuator</strong> – providing basic management URL</li> <li><strong>Hystrix</strong> – Enable Circuit Breaker</li> <li><strong>Hystrix Dashboard</strong> – Enable one Dashboard screen related to the Circuit Breaker monitoring</li> </ul> 下载从spring boot创建器官网创建的项目<br /> <img alt="spring boot创建" class="img-thumbnail" src="/assist/images/blog/4974516f532d43c6bf4e1a1e3771e5e2.jpg" />解压缩并将项目导入到Eclipse中,作为现有的maven项目。在此步骤中,将从maven存储库下载所有必需的依赖项。 <h4>4.2.2配置服务端口</h4> 打开配置文件<code>application.properties</code> 添加以下内容: <pre> <code>server.port = 9098</code></pre> 这将使该应用程序在缺省端口9098上运行。我们可以很容易地通过供给来覆盖-Dserver.port = XXXX启动服务器时的参数。 <h4>4.2.3启用Hystrix设置</h4>   打开<code>SpringHystrixSchoolServiceApplication</code> ,也就是使用 <code>@SpringBootApplication注解生成的类并且添加注解</code> <code>@EnableHystrixDashboard</code> 和 <code>@EnableCircuitBreaker</code> annotations.<br />   这将使Hystrix在应用程序中启用断路器,并将在Hystrix提供的本地主机上添加一个有用的指示板。 <pre> <code class="language-java">import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard; @SpringBootApplication @EnableHystrixDashboard @EnableCircuitBreaker public class SpringHystrixSchoolServiceApplication { public static void main(String[] args) { SpringApplication.run(SpringHystrixSchoolServiceApplication.class, args); } }</code></pre> <h4>4.2.4添加controller</h4>     添加学校serviceconcontroller Rest控制器,在这里我们将公开/getSchoolDetails/schoolname端点,它将简单地返回学校的详细信息和学生的详细信息。对于学生的详细信息,它将调用已经开发的学生服务端点。我们将创建一个委托层学生servicedelegate。java调用学生服务。这个简单的代码看起来就像: <pre> <code class="language-java">import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import com.example.howtodoinjava.springhystrixschoolservice.delegate.StudentServiceDelegate; @RestController public class SchoolServiceController { @Autowired StudentServiceDelegate studentServiceDelegate; @RequestMapping(value = "/getSchoolDetails/{schoolname}", method = RequestMethod.GET) public String getStudents(@PathVariable String schoolname) { System.out.println("Going to call student service to get data!"); return studentServiceDelegate.callStudentServiceAndGetData(schoolname); } }</code></pre> <h4>4.2.5StudentServiceDelegate</h4> 我们将在这里做下面的事情来支持Hystrix断路器。 <ul> <li>通过spring框架调用学生服务提供的RestTemplate</li> <li>添加Hystrix命令以启用回调方法 – <code>@HystrixCommand(fallbackMethod = "callStudentServiceAndGetData_Fallback")</code>这意味着我们需要添加另一种方法<code>callStudentServiceAndGetData_Fallback</code> 具有相同的签名,当实际的学生服务停止时将调用该签名。</li> <li>添加回调函数 - callStudentServiceAndGetData_Fallback他将会返回一些简单的默认值</li> </ul> <pre> <code class="language-java">import java.util.Date; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; @Service public class StudentServiceDelegate { @Autowired RestTemplate restTemplate; @HystrixCommand(fallbackMethod = "callStudentServiceAndGetData_Fallback") public String callStudentServiceAndGetData(String schoolname) { System.out.println("Getting School details for " + schoolname); String response = restTemplate .exchange("http://localhost:8098/getStudentDetailsForSchool/{schoolname}" , HttpMethod.GET , null , new ParameterizedTypeReference<String>() { }, schoolname).getBody(); System.out.println("Response Received as " + response + " - " + new Date()); return "NORMAL FLOW !!! - School Name - " + schoolname + " ::: " + " Student Details " + response + " - " + new Date(); } @SuppressWarnings("unused") private String callStudentServiceAndGetData_Fallback(String schoolname) { System.out.println("Student Service is down!!! fallback route enabled..."); return "CIRCUIT BREAKER ENABLED!!! No Response From Student Service at this moment. " + " Service will be back shortly - " + new Date(); } @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }</code></pre> <h3>4.2.6构建并且测试学校服务</h3>   使用maven命令mvn clean install来构建项目,并且使用命令java -jar target\spring-hystrix-school-service-0.0.1-SNAPSHOT.jar来启动项目.学校服务将会默认启动在9098端口。<br />   启动如上所述的学生服务,然后通过打开浏览器和类型来测试学校服务http://localhost:9098/getSchoolDetails/abcschool。它应该在浏览器中显示以下输出:<br /> <img alt="学校服务" class="img-thumbnail" src="/assist/images/blog/53e5b9259e3640e78ba0fa31d9190611.jpg" /><br />   <h2>五、<span style="line-height:1.2">测试Hystrix Circuit Breaker – Demo</span></h2> 打开链接<code>http://localhost:9098/getSchoolDetails/abcschool</code>.在浏览器应该显示:<br /> <img alt="多少" class="img-thumbnail" src="/assist/images/blog/76e198ce0a924afb8f6bc7e1ad56f539.jpg" /> <p>现在,我们已经知道学校服务在内部呼叫学生服务,它正在从那个服务中获取学生的详细信息。因此,如果两个服务都在运行,学校服务将显示学生服务返回的数据,正如我们在上面的学校服务浏览器输出中看到的那样。这是电路闭合状态。</p> <p> </p> <p>现在,让我们停止学生服务,只需在学生服务服务器控制台中按CTRL+C(停止服务器),并从浏览器中再次测试学校服务。这一次,它将返回返回的方法响应。在这里,Hystrix进入了一幅画面,它在频繁的时间间隔内监控学生服务,而Hystrix的组件已经打开了电路和后退的道路。</p> <p> </p> <p>下面是浏览器的输出结果。</p> <img alt="回调" class="img-thumbnail" src="/assist/images/blog/1384bf63495d410aa2e83d5de2f88f24.jpg" /><br /> 再次启动学生服务,等待几分钟,然后回到学校服务,它将再次开始正常的响应。 <h2>六、<span style="line-height:1.2">Hystrix Dashboard</span></h2> 在我们添加了hystrix指示板依赖的情况下,hystrix提供了一个不错的仪表盘和一个hystrix流。<br /> <br /> <a rel="external nofollow" href="http://localhost:9098/hystrix.stream" target="_blank"><strong>http://localhost:9098/hystrix.stream</strong></a>这是Hystrix产生的连续流。这只是一个健康检查结果,以及所有被Hystrix监控的服务电话。示例输出将在浏览器中显示<br /> <br /> <img alt="监控" class="img-thumbnail" src="/assist/images/blog/ef48413e3174411db1df65f22e8892af.jpg" /><a rel="external nofollow" href="http://localhost:9098/hystrix" target="_blank"><strong>http://localhost:9098/hystrix</strong></a> -这是可视指示板的初始状态。<br /> <img alt="控制3" class="img-thumbnail" src="/assist/images/blog/df1e14fda603435998861ee393f6540c.jpg" /><br /> 现在,在仪表板中添加http://localhost:9098/hystrix.stream,以获得由Hystrix组件监视的电路的有意义的动态可视化表示。在主页上提供了流输入之后,可视化指示板。<br /> <img alt="444" class="img-thumbnail" src="/assist/images/blog/933028aeb67944f6a71a4a6f0a47b5c2.jpg" /> <h2>八、总结</h2> <p>这都是关于创建弹簧的,可以是Hystrix断路器,我们已经测试了电路的开放路径和电路闭合路径。您自己的设置,并使用不同的组合服务状态来更清楚地说明整个概念。</p> <p>如果您在执行本文时遇到任何困难,请添加注释。我们将乐于看到这个问题。<br /> <br /> <br /> <a rel="external nofollow" href="http://howtodoinjava.com/wp-content/uploads/2017/07/Circuit-Breaker.zip" target="_blank">演示例子源码</a></p>
  • Spring WebFlux 和Reactive MongoDB来构建Reactive Rest API

    1.引言Spring 5通过引入一种名为Spring WebFlux的全新反应框架来支持响应式编程范例1.引言Spring 5通过引入一种名为Spring WebFlux的全新反应框架来支持响应式编程范例。Spring WebFlux是一个自下而上的异步框架。它可以使用Servlet 3.1非阻塞IO API以及其他异步运行时环境(如netty或undertow)在Servlet容器上运行。它可以与Spring MVC一起使用。是的,Spring MVC不会去任何地方。这是一个开发人员长期以来使用的流行的Web框架。但是你现在可以在新的反应框架和传统的Spring MVC之间做出选择。您可以根据自己的使用情况选择使用它们中的任何一个。Spring WebFlux使用一个名为Reactor的库作为响应支持。Reactor是Reactive Streams规范的一个实现。Reactor提供两种主要的类型,称为Flux和Mono。这两种类型都实现了PublisherReactive Streams提供的接口。Flux用于表示0..N个元素的流,Mono用于表示0..1个元素的流。虽然Spring使用Reactor作为其大部分内部API的核心依赖,但它也支持在应用程序级别使用RxJava。2.Spring WebFlux支持的编程模型Spring WebFlux支持两种类型的编程模型:带有@Controller,@RequestMapping和其他注释的基于注释的传统模型,您在Spring MVC中一直使用。基于Java 8 lambda表达式的全新功能样式模型,用于路由和处理请求。在本文中,我们将使用传统的基于注释的编程模型。我将在未来的文章中撰写功能风格模型。3.让我们在Spring Boot中构建一个Reactive Restful服务在本文中,我们将为迷你Twitter应用程序构建一个Restful API。该应用程序将只有一个称为的域模型Tweet。每个Tweet人都有text一个createdAt领域。我们将使用MongoDB作为我们的数据存储以及反应型mongodb驱动程序。我们将构建用于创建,检索,更新和删除Tweet的REST API。所有的REST API都是异步的,并且会返回一个发布者。我们还将学习如何将数据从数据库传输到客户端。最后,我们将编写集成测试以使用Spring 5提供的新异步WebTestClient测试所有API。4.创建项目我们使用Spring Initializr Web应用程序来生成我们的应用程序。按照以下步骤生成项目 -转到http://start.spring.io选择Spring Boot版本2.x输入工件的值作为webflux-demo添加Reactive Web和Reactive MongoDB依赖项点击生成项目生成并下载项目。​下载项目后,将其解压缩并导入到您最喜欢的IDE中。该项目的目录结构应该如下所示 -​配置MongoDB您可以通过简单地将以下属性添加到application.properties文件来配置MongoDB -spring.data.mongodb.uri=mongodb://localhost:27017/webflux_demoSpring Boot将在启动时读取此配置并自动配置数据源。创建领域模型让我们创建我们的领域模型 - Tweet。创建一个名为modelinside com.example.webfluxdemopackage 的新包,然后创建一个名为Tweet.java以下内容的文件-import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.util.Date; @Document(collection = "tweets") public class Tweet { @Id private String id; @NotBlank @Size(max = 140) private String text; @NotNull private Date createdAt = new Date(); public Tweet() { } public Tweet(String text) { this.id = id; this.text = text; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getText() { return text; } public void setText(String text) { this.text = text; } public Date getCreatedAt() { return createdAt; } public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; } }够简单!Tweet模型包含一个text和一个createdAt字段。该text字段用注释@NotBlank和@Size注释确保它不是空白并且最多有140个字符。5.创建存储库接下来,我们将创建将用于访问MongoDB数据库的数据访问层。创建一个名为repositoryinside 的新包com.example.webfluxdemo,然后TweetRepository.java使用以下内容创建一个新文件-import com.example.webfluxdemo.model.Tweet; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.stereotype.Repository; @Repository public interface TweetRepository extends ReactiveMongoRepository<Tweet, String> { }该TweetRepository接口扩展ReactiveMongoRepository了文档中的各种CRUD方法。Spring Boot自动插入在SimpleReactiveMongoRepository运行时调用的此接口的实现。因此,您无需编写任何代码就可以轻松获取文档上的所有CRUD方法。以下是一些可用的方法SimpleReactiveMongoRepository-reactor.core.publisher.Flux<T> findAll(); reactor.core.publisher.Mono<T> findById(ID id); <S extends T> reactor.core.publisher.Mono<S> save(S entity); reactor.core.publisher.Mono<Void> delete(T entity);请注意,所有方法都是异步的,并以a Flux或Mono类型的形式返回发布者。创建控制器端点最后,让我们编写将暴露给客户端的API。创建一个名为controllerinside 的新包com.example.webfluxdemo,然后TweetController.java使用以下内容创建一个新文件-import com.example.webfluxdemo.model.Tweet; import com.example.webfluxdemo.repository.TweetRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import javax.validation.Valid; @RestController public class TweetController { @Autowired private TweetRepository tweetRepository; @GetMapping("/tweets") public Flux<Tweet> getAllTweets() { return tweetRepository.findAll(); } @PostMapping("/tweets") public Mono<Tweet> createTweets(@Valid @RequestBody Tweet tweet) { return tweetRepository.save(tweet); } @GetMapping("/tweets/{id}") public Mono<ResponseEntity<Tweet>> getTweetById(@PathVariable(value = "id") String tweetId) { return tweetRepository.findById(tweetId) .map(savedTweet -> ResponseEntity.ok(savedTweet)) .defaultIfEmpty(ResponseEntity.notFound().build()); } @PutMapping("/tweets/{id}") public Mono<ResponseEntity<Tweet>> updateTweet(@PathVariable(value = "id") String tweetId, @Valid @RequestBody Tweet tweet) { return tweetRepository.findById(tweetId) .flatMap(existingTweet -> { existingTweet.setText(tweet.getText()); return tweetRepository.save(existingTweet); }) .map(updatedTweet -> new ResponseEntity<>(updatedTweet, HttpStatus.OK)) .defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND)); } @DeleteMapping("/tweets/{id}") public Mono<ResponseEntity<Void>> deleteTweet(@PathVariable(value = "id") String tweetId) { return tweetRepository.findById(tweetId) .flatMap(existingTweet -> tweetRepository.delete(existingTweet) .then(Mono.just(new ResponseEntity<Void>(HttpStatus.OK))) ) .defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND)); } // Tweets are Sent to the client as Server Sent Events @GetMapping(value = "/stream/tweets", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<Tweet> streamAllTweets() { return tweetRepository.findAll(); } }所有的控制器端点都以Flux或Mono的形式返回一个Publisher。我们将内容类型设置为的最后一个端点非常有趣text/event-stream。它以服务器发送事件的形式将推文发送到像这样的浏览器 -data: {"id":"59ba5389d2b2a85ed4ebdafa","text":"tweet1","createdAt":1505383305602} data: {"id":"59ba5587d2b2a85f93b8ece7","text":"tweet2","createdAt":1505383814847}现在我们正在讨论事件流,您可能会问以下端点是否也返回一个Stream?@GetMapping("/tweets") public Flux<Tweet> getAllTweets() { return tweetRepository.findAll(); }答案是肯定的。Flux<Tweet>代表推文流。但是,默认情况下,它将生成一个JSON数组,因为如果将单个JSON对象流发送给浏览器,那么它将不会是一个有效的JSON文档。除了使用Server-Sent-Events或WebSocket之外,浏览器客户端无法使用流。但是,非浏览器客户端可以通过设置Accept标头来请求JSON流application/stream+json,并且响应将是类似于Server-Sent-Events的JSON流,但不需要额外的格式:{"id":"59ba5389d2b2a85ed4ebdafa","text":"tweet1","createdAt":1505383305602} {"id":"59ba5587d2b2a85f93b8ece7","text":"tweet2","createdAt":1505383814847}使用WebTestClient进行集成测试Spring 5还提供了一个异步和被动的http客户端,WebClient用于处理异步和流式API。这是一个被动的选择RestTemplate。此外,你还可以得到一个WebTestClient写作集成测试。测试客户端可以运行在实时服务器上,也可以用于模拟请求和响应。我们将使用WebTestClient为我们的REST API编写集成测试。打开WebfluxDemoApplicationTests.java文件并将以下测试添加到它 -import com.example.webfluxdemo.model.Tweet; import com.example.webfluxdemo.repository.TweetRepository; import org.assertj.core.api.Assertions; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; import reactor.core.publisher.Mono; import java.util.Collections; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class WebfluxDemoApplicationTests { @Autowired private WebTestClient webTestClient; @Autowired TweetRepository tweetRepository; @Test public void testCreateTweet() { Tweet tweet = new Tweet("This is a Test Tweet"); webTestClient.post().uri("/tweets") .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaType.APPLICATION_JSON_UTF8) .body(Mono.just(tweet), Tweet.class) .exchange() .expectStatus().isOk() .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8) .expectBody() .jsonPath("$.id").isNotEmpty() .jsonPath("$.text").isEqualTo("This is a Test Tweet"); } @Test public void testGetAllTweets() { webTestClient.get().uri("/tweets") .accept(MediaType.APPLICATION_JSON_UTF8) .exchange() .expectStatus().isOk() .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8) .expectBodyList(Tweet.class); } @Test public void testGetSingleTweet() { Tweet tweet = tweetRepository.save(new Tweet("Hello, World!")).block(); webTestClient.get() .uri("/tweets/{id}", Collections.singletonMap("id", tweet.getId())) .exchange() .expectStatus().isOk() .expectBody() .consumeWith(response -> Assertions.assertThat(response.getResponseBody()).isNotNull()); } @Test public void testUpdateTweet() { Tweet tweet = tweetRepository.save(new Tweet("Initial Tweet")).block(); Tweet newTweetData = new Tweet("Updated Tweet"); webTestClient.put() .uri("/tweets/{id}", Collections.singletonMap("id", tweet.getId())) .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaType.APPLICATION_JSON_UTF8) .body(Mono.just(newTweetData), Tweet.class) .exchange() .expectStatus().isOk() .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8) .expectBody() .jsonPath("$.text").isEqualTo("Updated Tweet"); } @Test public void testDeleteTweet() { Tweet tweet = tweetRepository.save(new Tweet("To be deleted")).block(); webTestClient.delete() .uri("/tweets/{id}", Collections.singletonMap("id", tweet.getId())) .exchange() .expectStatus().isOk(); } }在上面的例子中,我为所有的CRUD API编写了测试。您可以通过转到项目的根目录并键入来运行测试mvn test。6.总结在本文中,我们学习了使用Spring进行反应式编程的基础知识,并使用Spring WebFlux框架提供的反应式支持构建了一个简单的Restful服务。我们还使用WebTestClient测试了所有Rest API。提示:项目源码下载 demo-springboot2.0-webflux-mongodb.zip
  • Spring WebFlux 项目实战 spring boot 2.0正式版

    引言Spring Boot 2.0最近去了GA,所以我决定写我关于Spring的第一篇文章很长一段时间引言Spring Boot 2.0最近去了GA,所以我决定写我关于Spring的第一篇文章很长一段时间。自发布以来,我一直在看到越来越多的Spring WebFlux以​​及如何使用它的教程。但是在阅读完它们并尝试让它自己工作之后,我发现从包含在我阅读的文章和教程中的代码跳转到编写实际上比返回字符串更有趣的事情从后端。现在,我希望我不会在自己的脚下说自己可能会对我在这篇文章中使用的代码做出同样的批评,但这里是我试图给Spring WebFlux的教程,它实际上类似于你可能会在野外使用的东西。项目结构:​在我继续之前,在提及WebFlux之后,究竟是什么呢?Spring WebFlux是Spring MVC的完全非阻塞反应式替代方案。它允许更好的垂直缩放而不增加硬件资源。被动反应它现在使用Reactive Streams来允许从调用返回到服务器的数据的异步处理。这意味着我们将看到更少的Lists,Collection甚至单个对象,而不是他们的反应等价物,例如Flux和Mono(来自Reactor)。我不会深入研究Reactive Streams是什么,诚实地说,在我尝试向任何人解释它之前,我需要更加深入地研究它。相反,让我们回过头来关注WebFlux。像往常一样,我使用Spring Boot在本教程中编写代码。以下是我在这篇文章中使用的依赖关系。<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-cassandra-reactive</artifactId> <version>2.0.0.RELEASE</version> </dependency> </dependencies>尽管我没有将它包含在上面的依赖代码片段中,但是它spring-boot-starter-parent被使用了,最终可以将其提升到版本2.0.0.RELEASE。本教程是关于WebFlux的,包括这spring-boot-starter-webflux显然是一个好主意。spring-boot-starter-data-cassandra-reactive也被包括在内,因为我们将用它作为示例应用程序的数据库,因为它是少数几个有反应支持的数据库之一(在编写本文时)。通过一起使用这些依赖关系,我们的应用程序可以从前到后完全反应。WebFlux引入了一种不同的方式来处理请求,而不是使用Spring MVC 中使用的@Controller或@RestController编程模型。但是,它并没有取代它。相反,它已被更新以允许使用被动类型。这使您可以保持与使用Spring编写相同的格式,但对返回类型进行一些更改,以便返回Fluxs或Monos。下面是一个非常人为的例子。@RestController public class PersonController { private final PersonRepository personRepository; public PersonController(PersonRepository personRepository) { this.personRepository = personRepository; } @GetMapping("/people") public Flux<Person> all() { return personRepository.findAll(); } @GetMapping("/people/{id}") Mono<Person> findById(@PathVariable String id) { return personRepository.findOne(id); } }对我来说,这看起来非常熟悉,并且从一眼就可以看出它与标准的Spring MVC控制器没有任何区别,但通过阅读方法后,我们可以看到不同的返回类型。在这个例子中PersonRepository必须是一个被动库,因为我们已经能够直接返回他们的搜索查询的结果供参考,被动库会返回一个Flux集合和一个Mono单一的实体。注释方法不是我想在这篇文章中关注的内容。这对我们来说不够酷,时髦。没有足够的lambda表达式来满足我们以更有效的方式编写Java的渴望。但Spring WebFlux有我们的支持。它提供了一种替代方法来路由和处理请求到我们的服务器,轻轻地使用lambdas编写路由器功能。我们来看一个例子。@Configuration public class PersonRouter { @Bean public RouterFunction<ServerResponse> route(PersonHandler personHandler) { return RouterFunctions.route(GET("/people/{id}").and(accept(APPLICATION_JSON)), personHandler::get) .andRoute(GET("/people").and(accept(APPLICATION_JSON)), personHandler::all) .andRoute(POST("/people").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::post) .andRoute(PUT("/people/{id}").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::put) .andRoute(DELETE("/people/{id}"), personHandler::delete) .andRoute(GET("/people/country/{country}").and(accept(APPLICATION_JSON)), personHandler::getByCountry); } } 这些都是PersonHandler我们稍后会看到的方法的所有路线。我们创建了一个将处理我们路由的bean。为了设置路由功能,我们使用了名为的RouterFunctions类为我们提供了一个静态方法,但现在我们只关心它的route方法。以下是该route方法的签名。public static <T extends ServerResponse> RouterFunction<T> route( RequestPredicate predicate, HandlerFunction<T> handlerFunction) { // stuff }  该方法表明,它与a RequestPredicate一起HandlerFunction并输出a RouterFunction。这RequestPredicate是我们用来指定路由的行为,比如我们处理函数的路径,它是什么类型的请求以及它可以接受的输入类型。由于我使用静态导入将所有内容读得更清晰,所以一些重要信息已经隐藏起来。要创建一个RequestPredicate我们应该使用RequestPredicates(复数),一个静态帮助类为我们提供我们需要的所有方法。就个人而言,我建议静态导入,RequestPredicates否则由于使用RequestPredicates静态方法可能需要的次数,您的代码将会一团糟。在上述例子中,GET,POST,PUT,DELETE,accept和contentType都是静态RequestPredicates方法。下一个参数是a HandlerFunction,它是一个功能接口。这里有三件重要的信息,它有一个泛型类型<T extends ServerResponse>,它的handle方法返回一个Mono<T>并且需要一个ServerRequest。使用这些我们可以确定我们需要传递一个返回一个Mono<ServerResponse>(或它的一个子类型)的函数。这显然对我们的处理函数返回的内容有严格的约束,因为它们必须满足这个要求,否则它们将不适合以这种格式使用。最后的结果是一个RouterFunction。这可以返回并用于路由到我们指定的任何函数。但通常情况下,我们希望一次将很多不同的请求发送给各种处理程序,这是WebFlux迎合的。由于route返回a RouterFunction以及RouterFunction也有其自己的路由方法的事实andRoute,我们可以将这些调用链接在一起并继续添加我们所需的所有额外路由。如果我们再回头看一下PersonRouter上面的例子,我们可以看到这些方法是以REST动词命名的,例如GET,POST它们定义了处理程序将要执行的请求的路径和类型。GET例如,如果我们以第一个请求为例,它将/people使用路径变量名称id(path表示的路径变量{id})和返回内容的类型(具体来说,使用该方法定义的APPLICATION_JSON静态字段from MediaType)进行路由accept。如果使用不同的路径,则不会被处理。如果路径正确但Accept头不是可接受的类型之一,则请求将失败。在我们继续之前,我想了解一下accept和contentType方法。这两个设置请求标头都accept与Accept标头和contentTypeContent-Type 匹配。Accept头定义了响应可接受的媒体类型,因为我们返回的Person对象的JSON表示设置为APPLICATION_JSON(application/json在实际头文件中)是有意义的。Content-Type具有相同的想法,但是却描述了发送请求正文内的媒体类型。这就是为什么只有动词POST和PUT动词才contentType包括在内,因为其他人在他们的身体中没有任何东西。DELETE不包括accept和contentType 所以我们可以得出这样的结论:它既没有期望返回任何东西,也没有在其请求中包含任何东西。现在我们知道如何设置路由,让我们看看如何编写处理传入请求的处理程序方法。以下是处理前面示例中定义的路由的所有请求的代码。@Component public class PersonHandler { private final PersonManager personManager; public PersonHandler(PersonManager personManager) { this.personManager = personManager; } public Mono<ServerResponse> get(ServerRequest request) { final UUID id = UUID.fromString(request.pathVariable("id")); final Mono<Person> person = personManager.findById(id); return person .flatMap(p -> ok().contentType(APPLICATION_JSON).body(fromPublisher(person, Person.class))) .switchIfEmpty(notFound().build()); } public Mono<ServerResponse> all(ServerRequest request) { return ok().contentType(APPLICATION_JSON) .body(fromPublisher(personManager.findAll(), Person.class)); } public Mono<ServerResponse> put(ServerRequest request) { final UUID id = UUID.fromString(request.pathVariable("id")); final Mono<Person> person = request.bodyToMono(Person.class); return personManager .findById(id) .flatMap( old -> ok().contentType(APPLICATION_JSON) .body( fromPublisher( person .map(p -> new Person(p, id)) .flatMap(p -> personManager.update(old, p)), Person.class))) .switchIfEmpty(notFound().build()); } public Mono<ServerResponse> post(ServerRequest request) { final Mono<Person> person = request.bodyToMono(Person.class); final UUID id = UUID.randomUUID(); return created(UriComponentsBuilder.fromPath("people/" + id).build().toUri()) .contentType(APPLICATION_JSON) .body( fromPublisher( person.map(p -> new Person(p, id)).flatMap(personManager::save), Person.class)); } public Mono<ServerResponse> delete(ServerRequest request) { final UUID id = UUID.fromString(request.pathVariable("id")); return personManager .findById(id) .flatMap(p -> noContent().build(personManager.delete(p))) .switchIfEmpty(notFound().build()); } public Mono<ServerResponse> getByCountry(ServerRequest serverRequest) { final String country = serverRequest.pathVariable("country"); return ok().contentType(APPLICATION_JSON) .body(fromPublisher(personManager.findAllByCountry(country), Person.class)); } }  有一点非常明显,就是缺少注释。酒吧的@Component注释自动创建一个PersonHandler豆没有其他Spring注解。我试图将大部分存储库逻辑保留在这个类之外,并且通过经由它所包含的PersonManager代理来隐藏对实体对象的任何引用PersonRepository。如果你对代码感兴趣,PersonManager那么可以在我的GitHub上看到,关于它的进一步解释将被排除在这篇文章之外,所以我们可以专注于WebFlux本身。好的,回到手头的代码。让我们仔细看看get和post方法来弄清楚发生了什么。public Mono<ServerResponse> get(ServerRequest request) { final UUID id = UUID.fromString(request.pathVariable("id")); final Mono<Person> person = personManager.findById(id); return person .flatMap(p -> ok().contentType(APPLICATION_JSON).body(fromPublisher(person, Person.class))) .switchIfEmpty(notFound().build()); }此方法用于从支持此示例应用程序的数据库中检索单个记录。由于Cassandra是选择的数据库,我决定使用UUID每个记录的主键,这使得测试示例更令人讨厌的不幸效果,但没有任何复制和粘贴无法解决的问题。请记住,此GET请求的路径中包含路径变量。使用pathVariable的方法ServerRequest传递到我们能够提取它的价值通过提供变量的名称,在这种情况下,方法id。然后将ID转换成一个UUID,如果字符串格式不正确,它会抛出一个异常,我决定忽略这个问题,所以示例代码不会变得混乱。一旦我们有了ID,我们就可以查询数据库中是否存在匹配的记录。Mono<Person>返回的A 包含映射到a的现有记录,Person或者它保留为空Mono。使用返回的,Mono我们可以根据它的存在输出不同的响应。这意味着我们可以将有用的状态代码返回给客户端以跟随主体的内容。如果记录存在,则flatMap返回一个ServerResponse与OK状态。伴随着这种状态,我们希望输出记录,为此,我们在这种情况下指定正文的内容类型APPLICATION_JSON,并将记录添加到记录中。fromPublisher需要我们Mono<Person>(这是一个Publisher)与Person课程一起,因此它知道它映射到身体中的是什么。fromPublisher是类的静态方法BodyInserters。如果记录不存在,那么流程将移动到switchIfEmpty块中并返回NOT FOUND状态。因为没有发现,身体可以留空,所以我们只是创建ServerResponse那里。现在到post处理程序。public Mono<ServerResponse> post(ServerRequest request) { final Mono<Person> person = request.bodyToMono(Person.class); final UUID id = UUID.randomUUID(); return created(UriComponentsBuilder.fromPath("people/" + id).build().toUri()) .contentType(APPLICATION_JSON) .body( fromPublisher( person.map(p -> new Person(p, id)).flatMap(personManager::save), Person.class)); }  即使只是从第一行开始,我们就可以看到,这种get方法的工作方式已经不同了。由于这是一个POST请求,它需要接受我们希望从请求主体持续存在的对象。由于我们试图插入单个记录,因此我们将使用请求的bodyToMono方法Person从正文中检索。如果您正在处理多个记录,则可能需要使用它们bodyToFlux。我们将CREATED使用created接受a 的方法返回状态URI以确定插入记录的路径。然后,get通过使用该fromPublisher方法将新记录添加到响应主体,然后采用与该方法类似的设置。形成该代码的代码Publisher稍有不同,但输出仍然Mono<Person>是一个重要的内容。为了进一步解释如何完成插入Person,从请求传入的内容将被映射到Person使用UUID我们生成的新内容,然后通过save调用传递给新内容flatMap。通过创建一个新的Person我们只将值插入我们允许的Cassandra中,在这种情况下,我们不希望UUID从请求体传入。所以说,这是关于处理程序。显然还有其他方法,我们没有经历。它们的工作方式都不相同,但都遵循相同的概念,ServerResponse如果需要,它返回一个包含适当状态代码和记录的体系。现在我们已经编写了所有我们需要的代码来获得基本的Spring WebFlux后端运行。剩下的就是将所有配置绑定在一起,这对Spring Boot来说很简单。@SpringBootApplication public class Application { public static void main(String args[]) { SpringApplication.run(Application.class); } }  我们应该研究如何真正使用代码,而不是结束这篇文章。Spring提供了WebClient该类来处理请求而不会阻塞。我们现在可以利用这个来测试应用程序,尽管WebTestClient我们也可以在这里使用它。该WebClient是你可以使用,而不是阻止什么RestTemplate产生反应的应用程序时。下面是一些调用在PersonHandler。中定义的处理程序的代码。public class Client { private WebClient client = WebClient.create("http://localhost:8080"); public void doStuff() { // POST final Person record = new Person(UUID.randomUUID(), "John", "Doe", "UK", 50); final Mono<ClientResponse> postResponse = client .post() .uri("/people") .body(Mono.just(record), Person.class) .accept(APPLICATION_JSON) .exchange(); postResponse .map(ClientResponse::statusCode) .subscribe(status -> System.out.println("POST: " + status.getReasonPhrase())); // GET client .get() .uri("/people/{id}", "a4f66fe5-7c1b-4bcf-89b4-93d8fcbc52a4") .accept(APPLICATION_JSON) .exchange() .flatMap(response -> response.bodyToMono(Person.class)) .subscribe(person -> System.out.println("GET: " + person)); // ALL client .get() .uri("/people") .accept(APPLICATION_JSON) .exchange() .flatMapMany(response -> response.bodyToFlux(Person.class)) .subscribe(person -> System.out.println("ALL: " + person)); // PUT final Person updated = new Person(UUID.randomUUID(), "Peter", "Parker", "US", 18); client .put() .uri("/people/{id}", "ec2212fc-669e-42ff-9c51-69782679c9fc") .body(Mono.just(updated), Person.class) .accept(APPLICATION_JSON) .exchange() .map(ClientResponse::statusCode) .subscribe(response -> System.out.println("PUT: " + response.getReasonPhrase())); // DELETE client .delete() .uri("/people/{id}", "ec2212fc-669e-42ff-9c51-69782679c9fc") .exchange() .map(ClientResponse::statusCode) .subscribe(status -> System.out.println("DELETE: " + status)); } } 不要忘了在Client某个地方实例化,下面是一个很好的偷懒方式来做到这一点!@SpringBootApplication public class Application { public static void main(String args[]) { SpringApplication.run(Application.class); Client client = new Client(); client.doStuff(); } } 首先我们创建一个WebClient。private final WebClient client = WebClient.create("http://localhost:8080"); 一旦创建,我们就可以开始做它的东西,因此doStuff方法。我们来分解一下POST发送给后端的请求。final Mono<ClientResponse> postResponse = client .post() .uri("/people") .body(Mono.just(record), Person.class) .accept(APPLICATION_JSON) .exchange(); postResponse .map(ClientResponse::statusCode) .subscribe(status -> System.out.println("POST: " + status.getReasonPhrase()));我写下这个稍有不同,所以你可以看到a Mono<ClientResponse>是从发送请求返回的。该exchange方法将HTTP请求发送到服务器。然后,只要响应到达,就会处理响应,如果有的话。使用WebClient我们指定我们想要POST使用post当然的方法发送请求。在URI随后与所添加的uri方法(重载的方法,这一个接受一个String但另一个接受URI)。我厌倦了说这个方法做了什么方法,所以,身体的内容随后与Accept头一起添加。最后我们通过电话发送请求exchange。请注意,媒体类型APPLICATION_JSON与POST路由器功能中定义的类型相匹配。如果我们要发送不同的类型,比如说TEXT_PLAIN我们会得到一个404错误,因为没有处理程序存在与请求期望返回的内容相匹配的地方。使用Mono<ClientResponse>通过调用返回exchange,我们可以绘制它的内容给我们所需的输出。在上面的例子中,状态代码被打印到控制台。如果我们回想一下post方法PersonHandler,请记住它只能返回“创建”状态,但如果发送的请求没有正确匹配,则会打印出“未找到”。我们来看看其他请求之一。client .get() .uri("/people/{id}", "a4f66fe5-7c1b-4bcf-89b4-93d8fcbc52a4") .accept(APPLICATION_JSON) .exchange() .flatMap(response -> response.bodyToMono(Person.class)) .subscribe(person -> System.out.println("GET: " + person));这是我们的典型GET要求。它看起来与POST我们刚刚经历的请求非常相似。主要区别在于uri,请求路径和UUID(作为String在这种情况下)作为参数来取代路径变量{id}并且主体留空。响应如何处理也是不同的。在这个例子中,它提取了响应的主体并将其映射到a Mono<Person>并打印出来。这可以在前面的POST例子中完成,但是响应的状态代码对于它的情况更有用。对于略有不同的观点,我们可以使用cURL发出请求并查看响应的样子。CURL -H "Accept:application/json" -i localhost:8080/people HTTP/1.1 200 OK transfer-encoding: chunked Content-Type: application/json [ { "id": "13c403a2-6770-4174-8b76-7ba7b75ef73d", "firstName": "John", "lastName": "Doe", "country": "UK", "age": 50 }, { "id": "fbd53e55-7313-4759-ad74-6fc1c5df0986", "firstName": "Peter", "lastName": "Parker", "country": "US", "age": 50 } ] 响应看起来像这样,显然它会根据您存储的数据而有所不同。请注意响应标题。transfer-encoding: chunked Content-Type: application/json    在transfer-encoding这里表示的是在可用于流式传输的数据块传输的数据。这就是我们需要的,因此客户可以对返回给它的数据采取反应态度。我认为这应该是一个停止的好地方。我们在这里已经涵盖了相当多的材料,希望能够帮助您更好地理解Spring WebFlux。还有一些其他的话题我想关注WebFlux,但是我会在单独的帖子中做这些,因为我认为这个主题足够长。    总之,在这篇文章中,我们非常简要地讨论了为什么你想在典型的Spring MVC后端中使用Spring WebFlux。然后我们看看如何设置路由和处理程序来处理传入的请求。处理程序实现了可以处理大多数REST动词的方法,并在响应中返回了正确的数据和状态代码。最后,我们研究了向后端发送请求的两种方式,一种是使用a WebClient直接在客户端处理输出,另一种使用cURL查看返回的JSON的外观。注意源码中使用的数据为:cassandra提示:项目源码下载:demo-springboot-webflux-0401.zip