• java与es8实战之五:SpringBoot应用中操作es8(带安全检查:https、账号密码、API Key)


    欢迎访问我的GitHub

    这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

    本篇概览

    • 本篇是《java与es8实战》系列的第五篇,总体目标明确:实战在SpringBoot应用中操作elasticsearch8,今天的重点是SpringBoot应用连接带有安全检查的elasticsearch8服务端
    • 连接需要安全检查的elasticsearch8是为了更接近真实环境,首先,连接是基于自签证书的https协议,其次,认证方式有两种
    1. 第一种是账号密码
    2. 第二种是es服务端授权的API Key

    在这里插入图片描述

    • 今天的实战总体目标可以拆解为两个子任务
    1. 在SpringBoot中连接elasticsearch8
    2. 在SpringBoot中使用elasticsearch8官方的Java API Client
    • 接下来直接开始

    部署elasticsearch集群(需要安全检查)

    创建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
    1. 首先,是builder pattern,连接es有关的代码,各种对象都是其builder对象的build方法创建的,建议您提前阅读《java与es8实战之一》一文,看完后,满屏的builder代码可以从丑变成美...
    2. 其次,就是java应用能向es发请求的关键:ElasticsearchClient对象,该对象的创建是有套路的,如下图,先创建RestClient,再基于RestClient创建ElasticsearchTransport,最后基于ElasticsearchTransport创建ElasticsearchClient,这是个固定的套路,咱们后面的操作都是基于此的,可能会加一点东西,但不会改变流程和图中的对象
      在这里插入图片描述
    • 准备完毕,开始写代码

    新建子工程

    • 为了便于管理依赖库版本和源码,《java与es8实战》系列的所有代码都以子工程的形式存放在父工程elasticsearch-tutorials

    • 《java与es8实战之二:实战前的准备工作》一文说明了创建父工程的详细过程

    • 在父工程elasticsearch-tutorials中新建名为crud-with-security的子工程,其pom.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">
        
        <parent>
            <artifactId>elasticsearch-tutorialsartifactId>
            <groupId>com.bolingcavalrygroupId>
            <version>1.0-SNAPSHOTversion>
            <relativePath>../pom.xmlrelativePath>
        parent>
        <modelVersion>4.0.0modelVersion>
        <groupId>com.bolingcavalrygroupId>
        
        <artifactId>crud-with-securityartifactId>
        <packaging>jarpackaging>
        
        <name>crud-with-securityname>
        <version>1.0-SNAPSHOTversion>
        <url>https://github.com/zq2599url>
    
        
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-dependenciesartifactId>
    
                    <version>${springboot.version}version>
                    <type>pomtype>
                    <scope>importscope>
                dependency>
            dependencies>
        dependencyManagement>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-actuatorartifactId>
            dependency>
    
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-configuration-processorartifactId>
                <optional>trueoptional>
            dependency>
    
            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
                <scope>testscope>
    
                
                <exclusions>
                    <exclusion>
                        <groupId>junitgroupId>
                        <artifactId>junitartifactId>
                    exclusion>
                exclusions>
    
            dependency>
    
            
            <dependency>
                <groupId>org.junit.jupitergroupId>
                <artifactId>junit-jupiter-apiartifactId>
                <scope>testscope>
            dependency>
    
            <dependency>
                <groupId>org.junit.jupitergroupId>
                <artifactId>junit-jupiter-engineartifactId>
                <scope>testscope>
            dependency>
    
            
            <dependency>
                <groupId>co.elastic.clientsgroupId>
                <artifactId>elasticsearch-javaartifactId>
            dependency>
    
            <dependency>
                <groupId>com.fasterxml.jackson.coregroupId>
                <artifactId>jackson-databindartifactId>
            dependency>
    
            
            <dependency>
                <groupId>jakarta.jsongroupId>
                <artifactId>jakarta.json-apiartifactId>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
        dependencies>
    
        <build>
            <plugins>
                
                <plugin>
                    <groupId>org.apache.maven.pluginsgroupId>
                    <artifactId>maven-surefire-pluginartifactId>
                    <version>3.0.0-M4version>
                    <configuration>
                        <skipTests>falseskipTests>
                    configuration>
                plugin>
    
                <plugin>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-maven-pluginartifactId>
                    <configuration>
                        <excludes>
                            <exclude>
                                <groupId>org.projectlombokgroupId>
                                <artifactId>lombokartifactId>
                            exclude>
                        excludes>
                    configuration>
                plugin>
            plugins>
    
            <resources>
                <resource>
                    <directory>src/main/resourcesdirectory>
                    <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
    

    编码:启动类

    • 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对象的时候,属于前面总结的套路步骤中的一步,如下图红框所示

    image-20220710215632738

    • 配置类的详细代码如下,有几处需要注意的地方稍后会说明
    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);
        }
    }
    
    • 上述代码有以下几处需要注意
    1. 这个配置类为业务代码提供了两个服务bean,作用是操作es,这两个服务bean分别由clientByPasswdclientByApiKey两个方法负责提供
    2. 名为getElasticsearchTransport的方法有两个,分别负责配置两种鉴权方式:账号密码和apiKey
    3. 设置证书的操作被封装在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访问方式,希望本文能给您一些参考

    源码下载

    名称 链接 备注
    项目主页 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,如下图红框
      在这里插入图片描述

    欢迎关注博客园:程序员欣宸

    学习路上,你不孤单,欣宸原创一路相伴...

  • 相关阅读:
    就业 | 面试签约问题
    动态时间规整算法——DTW
    Java分库分表/读写分离
    python毕业设计作品基于django框架 电影院购票选座系统毕设成品(6)开题答辩PPT
    腾讯云国际Linux 系统如何配置防火墙软件 iptables?
    llinux的更目录下的文件作用和举例
    想转行互联网行业,是选择网络安全还是人工智能?
    【三维目标检测】SASSD(一)
    容器环境注入Spring属性不一致却能生效
    Unity Android 接入高德定位SDK
  • 原文地址:https://www.cnblogs.com/bolingcavalry/p/17658362.html