ToB企服应用市场:ToB评测及商务社交产业平台
标题:
java与es8实战之五:SpringBoot应用中操作es8(带安全检查:https、账号密码
[打印本页]
作者:
郭卫东
时间:
2023-9-2 16:27
标题:
java与es8实战之五:SpringBoot应用中操作es8(带安全检查:https、账号密码
欢迎访问我的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>
<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>crud-with-security</artifactId>
<packaging>jar</packaging>
<name>crud-with-security</name>
<version>1.0-SNAPSHOT</version>
<url>https://github.com/zq2599</url>
<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>
<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>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<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>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<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>
<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)git@github.com:zq2599/blog_demos.git该项目源码的仓库地址,ssh协议
这个git项目中有多个文件夹,本次实战的源码在
elasticsearch-tutorials
文件夹下,如下图红框
elasticsearch-tutorials
是个父工程,里面有多个module,本篇实战的module是
crud-with-security
,如下图红框
欢迎关注博客园:程序员欣宸
学习路上,你不孤单,欣宸原创一路相伴...
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/)
Powered by Discuz! X3.4