### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 本篇是《java與es8實戰》系列的第五 ...
歡迎訪問我的GitHub
這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos
本篇概覽
- 本篇是《java與es8實戰》系列的第五篇,總體目標明確:實戰在SpringBoot應用中操作elasticsearch8,今天的重點是SpringBoot應用連接帶有安全檢查的elasticsearch8服務端
- 連接需要安全檢查的elasticsearch8是為了更接近真實環境,首先,連接是基於自簽證書的https協議,其次,認證方式有兩種
- 第一種是賬號密碼
- 第二種是es服務端授權的API Key
- 以上兩種認證方式,在今天的實戰中都會嘗試,再加上前文《java與es8實戰之四:SpringBoot應用中操作es8(無安全檢查)》,可以小小的梳理一下SpringBoot應用連接es8的方式了,如下所示,直連、證書+賬號密碼、證書+API key等三種
- 今天的實戰總體目標可以拆解為兩個子任務
- 在SpringBoot中連接elasticsearch8
- 在SpringBoot中使用elasticsearch8官方的Java API Client
- 接下來直接開始
部署elasticsearch集群(需要安全檢查)
- 關於快速部署elasticsearch集群(需要安全檢查),可以參考《docker-compose快速部署elasticsearch-8.x集群+kibana》
創建API Key
- 除了賬號密碼,ES還提供了一種安全的訪問方式:API Key,java應用持有es簽發的API Key也能順利發送指令到es,接下來咱們先生成API Key,再在應用中使用此API Key
- 《docker-compose快速部署elasticsearch-8.x集群+kibana》一文中,的咱們將自簽證書從容器中複製出來了,現在在證書所在目錄執行以下命令,註意參數expiration代表這個ApiKey的有效期,我這裡隨意設置為10天
curl -X POST "https://localhost:9200/_security/api_key?pretty" \
--cacert es01.crt \
-u elastic:123456 \
-H 'Content-Type: application/json' \
-d'
{
"name": "my-api-key-10d",
"expiration": "10d"
}
'
- 會收到以下響應,其中的encoded欄位就是API Key
{
"id" : "eUV1V4EBucGIxpberGuJ",
"name" : "my-api-key-10d",
"expiration" : 1655893738633,
"api_key" : "YyhSTh9ETz2LKBk3-Iy2ew",
"encoded" : "ZVVWMVY0RUJ1Y0dJeHBiZXJHdUo6WXloU1RoOUVUejJMS0JrMy1JeTJldw=="
}
Java應用連接elasticsearch的核心套路
- 不論是直連,還是帶安全檢查的連接,亦或是與SpringBoot的集成使之更方便易用,都緊緊圍繞著一個不變的核心套路,該套路由兩部分組成,掌握了它們就能在各種條件下成功連接es
- 首先,是builder pattern,連接es有關的代碼,各種對象都是其builder對象的build方法創建的,建議您提前閱讀《java與es8實戰之一》一文,看完後,滿屏的builder代碼可以從醜變成美...
- 其次,就是java應用能向es發請求的關鍵:ElasticsearchClient對象,該對象的創建是有套路的,如下圖,先創建RestClient,再基於RestClient創建ElasticsearchTransport,最後基於ElasticsearchTransport創建ElasticsearchClient,這是個固定的套路,咱們後面的操作都是基於此的,可能會加一點東西,但不會改變流程和圖中的對象
- 準備完畢,開始寫代碼
新建子工程
-
為了便於管理依賴庫版本和源碼,《java與es8實戰》系列的所有代碼都以子工程的形式存放在父工程elasticsearch-tutorials中
-
《java與es8實戰之二:實戰前的準備工作》一文說明瞭創建父工程的詳細過程
-
在父工程elasticsearch-tutorials中新建名為crud-with-security的子工程,其pom.xml內容如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 請改為自己項目的parent坐標 -->
<parent>
<artifactId>elasticsearch-tutorials</artifactId>
<groupId>com.bolingcavalry</groupId>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.bolingcavalry</groupId>
<!-- 請改為自己項目的artifactId -->
<artifactId>crud-with-security</artifactId>
<packaging>jar</packaging>
<!-- 請改為自己項目的name -->
<name>crud-with-security</name>
<version>1.0-SNAPSHOT</version>
<url>https://github.com/zq2599</url>
<!--不用spring-boot-starter-parent作為parent時的配置-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${springboot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 不加這個,configuration類中,IDEA總會添加一些提示 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<!-- exclude junit 4 -->
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- junit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<!-- elasticsearch引入依賴 start -->
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- 使用spring boot Maven插件時需要添加該依賴 -->
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 需要此插件,在執行mvn test命令時才會執行單元測試 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M4</version>
<configuration>
<skipTests>false</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.*</include>
</includes>
</resource>
</resources>
</build>
</project>
配置文件
- 為了成功連接es,需要兩個配置文件:SpringBoot常規的配置application.yml和es的自簽證書
- 首先是application.yml,如下所示,因為本篇要驗證兩種授權方式,所以賬號、密碼、apiKey全部填寫在配置文件中,如下所示
elasticsearch:
username: elastic
passwd: 123456
apikey: ZVVWMVY0RUJ1Y0dJeHBiZXJHdUo6WXloU1RoOUVUejJMS0JrMy1JeTJldw==
# 多個IP逗號隔開
hosts: 127.0.0.1:9200
- 接下來是es的自簽證書,這是SpringBoot應用在向es8發起https請求時需要用到的,在《docker-compose快速部署elasticsearch-8.x集群+kibana》一文中已經將其成功從容器中複製出來,現在請將其放在application.yml文件所在位置,如下圖
編碼:啟動類
- SpringBoot啟動類,平淡無奇的那種
@SpringBootApplication
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
}
編碼:配置文件
-
接下來是全文的重點:通過Config類向Spring環境註冊服務bean,這裡有這兩處要註意的地方
-
第一個要註意的地方:向Spring環境註冊的服務bean一共有兩個,它們都是ElasticsearchClient類型,一個基於賬號密碼認證,另一個基於apiKey認證
-
第二個要註意的地方:SpringBoot向es服務端發起的是https請求,這就要求在建立連接的時候使用正確的證書,也就是剛纔咱們從容器中複製出來再放入application.yml所在目錄的es01.crt文件,使用證書的操作發生在創建ElasticsearchTransport對象的時候,屬於前面總結的套路步驟中的一步,如下圖紅框所示
- 配置類的詳細代碼如下,有幾處需要註意的地方稍後會說明
package com.bolingcavalry.security.config;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.message.BasicHeader;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestClientBuilder.HttpClientConfigCallback;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.StringUtils;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
@ConfigurationProperties(prefix = "elasticsearch") //配置的首碼
@Configuration
@Slf4j
public class ClientConfig {
@Setter
private String hosts;
@Setter
private String username;
@Setter
private String passwd;
@Setter
private String apikey;
/**
* 解析配置的字元串,轉為HttpHost對象數組
* @return
*/
private HttpHost[] toHttpHost() {
if (!StringUtils.hasLength(hosts)) {
throw new RuntimeException("invalid elasticsearch configuration");
}
String[] hostArray = hosts.split(",");
HttpHost[] httpHosts = new HttpHost[hostArray.length];
HttpHost httpHost;
for (int i = 0; i < hostArray.length; i++) {
String[] strings = hostArray[i].split(":");
httpHost = new HttpHost(strings[0], Integer.parseInt(strings[1]), "https");
httpHosts[i] = httpHost;
}
return httpHosts;
}
@Bean
public ElasticsearchClient clientByPasswd() throws Exception {
ElasticsearchTransport transport = getElasticsearchTransport(username, passwd, toHttpHost());
return new ElasticsearchClient(transport);
}
private static SSLContext buildSSLContext() {
ClassPathResource resource = new ClassPathResource("es01.crt");
SSLContext sslContext = null;
try {
CertificateFactory factory = CertificateFactory.getInstance("X.509");
Certificate trustedCa;
try (InputStream is = resource.getInputStream()) {
trustedCa = factory.generateCertificate(is);
}
KeyStore trustStore = KeyStore.getInstance("pkcs12");
trustStore.load(null, null);
trustStore.setCertificateEntry("ca", trustedCa);
SSLContextBuilder sslContextBuilder = SSLContexts.custom()
.loadTrustMaterial(trustStore, null);
sslContext = sslContextBuilder.build();
} catch (CertificateException | IOException | KeyStoreException | NoSuchAlgorithmException |
KeyManagementException e) {
log.error("ES連接認證失敗", e);
}
return sslContext;
}
private static ElasticsearchTransport getElasticsearchTransport(String username, String passwd, HttpHost...hosts) {
// 賬號密碼的配置
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, passwd));
// 自簽證書的設置,並且還包含了賬號密碼
HttpClientConfigCallback callback = httpAsyncClientBuilder -> httpAsyncClientBuilder
.setSSLContext(buildSSLContext())
.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
.setDefaultCredentialsProvider(credentialsProvider);
// 用builder創建RestClient對象
RestClient client = RestClient
.builder(hosts)
.setHttpClientConfigCallback(callback)
.build();
return new RestClientTransport(client, new JacksonJsonpMapper());
}
private static ElasticsearchTransport getElasticsearchTransport(String apiKey, HttpHost...hosts) {
// 將ApiKey放入header中
Header[] headers = new Header[] {new BasicHeader("Authorization", "ApiKey " + apiKey)};
// es自簽證書的設置
HttpClientConfigCallback callback = httpAsyncClientBuilder -> httpAsyncClientBuilder
.setSSLContext(buildSSLContext())
.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
// 用builder創建RestClient對象
RestClient client = RestClient
.builder(hosts)
.setHttpClientConfigCallback(callback)
.setDefaultHeaders(headers)
.build();
return new RestClientTransport(client, new JacksonJsonpMapper());
}
@Bean
public ElasticsearchClient clientByApiKey() throws Exception {
ElasticsearchTransport transport = getElasticsearchTransport(apikey, toHttpHost());
return new ElasticsearchClient(transport);
}
}
- 上述代碼有以下幾處需要註意
- 這個配置類為業務代碼提供了兩個服務bean,作用是操作es,這兩個服務bean分別由clientByPasswd和clientByApiKey兩個方法負責提供
- 名為getElasticsearchTransport的方法有兩個,分別負責配置兩種鑒權方式:賬號密碼和apiKey
- 設置證書的操作被封裝在buildSSLContext方法中,在創建ElasticsearchTransport對象的時候會用到
編碼:業務類
-
既然兩個ElasticsearchClient對象都已經註冊到Spring環境,那麼只要在業務類中註入就能用來操作es了
-
新建業務類ESService.java,如下,可見通過Resource註解選擇了賬號密碼鑒權的ElasticsearchClient對象
package com.bolingcavalry.security.service;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
@Service
public class ESService {
@Resource(name="clientByPasswd")
private ElasticsearchClient elasticsearchClient;
public void addIndex(String name) throws IOException {
elasticsearchClient.indices().create(c -> c.index(name));
}
public boolean indexExists(String name) throws IOException {
return elasticsearchClient.indices().exists(b -> b.index(name)).value();
}
public void delIndex(String name) throws IOException {
elasticsearchClient.indices().delete(c -> c.index(name));
}
}
- 至此,基本功能算是開發完成了,接下來編寫單元測試代碼,驗證能否成功操作es8
編碼:單元測試
- 新增單元測試類ESServiceTest.java,如下,功能是調用業務類ESService執行創建、刪除、查找等索引操作
package com.bolingcavalry.security.service;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class ESServiceTest {
@Autowired
ESService esService;
@Test
void addIndex() throws Exception {
String indexName = "test_index";
Assertions.assertFalse(esService.indexExists(indexName));
esService.addIndex(indexName);
Assertions.assertTrue(esService.indexExists(indexName));
esService.delIndex(indexName);
Assertions.assertFalse(esService.indexExists(indexName));
}
}
- 編碼完成,開始驗證
驗證:賬號密碼鑒權
- 現在ESService中使用的es服務類是賬號密碼鑒權的,運行單元測試,看看是否可以成功操作ES,如下圖,符合預期
驗證:ApiKey鑒權
-
再來試試ApiKey鑒權操作es,修改ESService.java源碼,改動如下圖紅框所示
-
為了檢查創建的索引是否符合預期,註釋掉單元測試類中刪除索引的代碼,如下圖,如此一來,單元測試執行完成後,新增的索引還保留在es環境中
-
再執行一次單元測試,依舊符合預期
-
用eshead查看,可見索引創建成功
-
至此,SpringBoot操作帶有安全檢查的elasticsearch8的實戰就完成了,在SpringData提供elasticsearch8操作的庫之前,基於es官方原生client庫的操作是常見的elasticsearch8訪問方式,希望本文能給您一些參考
源碼下載
- 本篇實戰的完整源碼可在GitHub下載到,地址和鏈接信息如下表所示(https://github.com/zq2599/blog_demos)
名稱 | 鏈接 | 備註 |
---|---|---|
項目主頁 | https://github.com/zq2599/blog_demos | 該項目在GitHub上的主頁 |
git倉庫地址(https) | https://github.com/zq2599/blog_demos.git | 該項目源碼的倉庫地址,https協議 |
git倉庫地址(ssh) | [email protected]:zq2599/blog_demos.git | 該項目源碼的倉庫地址,ssh協議 |
- 這個git項目中有多個文件夾,本次實戰的源碼在elasticsearch-tutorials文件夾下,如下圖紅框
- elasticsearch-tutorials是個父工程,裡面有多個module,本篇實戰的module是crud-with-security,如下圖紅框