-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.json
1 lines (1 loc) · 285 KB
/
content.json
1
{"pages":[{"title":"","text":"熟悉 Java 技术栈,熟悉设计模式,看过一些 Spring Cloud 组件源码并自定义开发过一些逻辑; 熟悉应用安全设计,保障应用的安全性; 熟悉 JVM 调优、OOM、性能优化以及线上问题处理; 参加过 APP 类型、B 端服务、人工智能相关后端项目,自认除电商系统外,任何系统设计与实现都可完成。 也熟悉一点 go、python、js、云计算、k8s。 目前在继续提升系统架构能力与加深底层知识了解。","link":"/about/index.html"}],"posts":[{"title":"GraalVM浅析","text":"1. 架构篇 GraalVM 主要包含两个功能及其他附属功能: 多语言的编译器和运行时环境 对基于 JVM 的语言的原生编译 1.1 多语言的编译器和运行时环境对于编程语言的开发人员而言,只需要完成对一门编程语言的解析工作,它的虚拟机实现则可以交给 GraalVM。 GraalVM 分为三部分: 本身就基于 jvm 的语言,如 Java、Scala、kotlin 等等 动态语言,如 Python,JS,Ruby 等 其他静态语言,如 golang,R 语言 等 GraalVM 对这三种语言的虚拟机实现,原理如下: 对字节码文件 “翻译“ 成机器码 将动态语言解析成 “AST”,编译器将 “AST” 转换成机器码 c/c++ 代码 -> llvm 字节码 -> “AST” -> 机器码 1.2 原生编译原生编译依赖于 AOT 技术,AOT 技术就是在编译的时候,直接将源代码编译成机器码。 jit 编译技术是用 javac 将源码编译成字节码,执行的时候,再由 jvm 的 jit 编译器将字节码翻译成机器码。 GraalVM 有三种运行模式: jvm 模式 native image 模式 Java on truffle 模式 其中,native image 模式就是使用的 AOT 技术。 2. 安装方式GraalVM 本身就包含了 jdk (社区版基于 openjdk,商业版基于Oracle jdk),在它的 /bin 目录中,包含了 jdk 的各种命令,如 java、javac等。 直接根据官方文档,根据自己的操作系统一步一步走就行。文档地址:https://www.graalvm.org/22.2/docs/getting-started/#install-graalvm 需要额外说明的是,graalvm 不支持 windows 系统安装 python。所以最好直接用 ubuntu 系统或者用 wsl 3. 主要组件GraalVM 安装后,/bin 目录中除了 jdk 本身自带的 Java 相关命令,还默认带了三个工具: js:js 执行器 lli:LLVM 字节码执行器 gu:安装其他语言和工具 比如,想在 GraalVM 中使用 nodejs,那么只需要执行:gu install nodejs,执行成功之后,GraalVM 的 bin 目录中将会包含 nodejs 的命令行工具,在终端中也可以直接使用 node -v。","link":"/2022/05/02/GraalVM%E6%B5%85%E6%9E%90/"},{"title":"Nginx学习总结","text":"1 概述主要对nginx进行介绍 2 基础知识2.1 应用场景 静态资源服务 反向代理 缓存 负载均衡 API服务 OpenResty 2.2 组成 nginx 二进制组成文件 nginx配置文件 access.log:记录每一条http请求信息 error.log:记录错误信息 2.3 版本使用nginx:开源版 OpenResty:开源版 2.4 编译安装nginx步骤: 下载nginx 执行./configure make make install 源碼目錄解釋: nginx 语法高亮加载 123# 进入nginx解压目录cp -r contrib/vim/* ~/.vim/ ./configure参数: 可以指定安装目录 可以指定加载和不加载模块 可以指定编译优化参数 如何给已安装的nginx重新安装模块 2.5 配置文件conf文件中,由http包含所有,http中可以包含http、upstream、server、location四个部分 2.6 静态资源搭建 alias命令使用 开启gzip autoindex模块 限速:$limit_rate log_format格式化日志 1234567location / { alias /opt/upload/file/; autoindex on; autoindex_localtime on; charset utf-8;} 2.7 反向代理2.8 使用goaccess可视化1goaccess access.log -o /opt/html/report.html --real-time-html --time-format='%H:%M:%S' --date-format='%d/%b/%Y' --log-format=COMBINED 2.9 CORS处理1234567891011121314151617181920212223242526272829303132333435363738394041424344http { map $http_origin $allow_origin { default ""; "~^(https?://localhost(:[0-9]+)?)" $1; "~^(https?://127.0.0.1(:[0-9]+)?)" $1; "~^(https?://192.168.254.[\\d]+(:[0-9]+)?)" $1; "~^(https?://202.104.150.229(:[0-9]+)?)" $1; "~^(https?://103.39.235.17(:[0-9]+)?)" $1; "~^(https?://([\\w]+.)?([\\w]+.)?[\\w]+.rainbowecho.top)" $1; #"~^(https?://qzs.stcn.com)" $1; } server { location / { # proxy_pass http://appapi.stcn.com; # 压测临时调整 proxy_pass proxy_pass http://192.168.254.205:8582; # proxy_pass http://192.168.254.77:8582; # nginx的代理后端是另一个LSB proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; # 是否允许请求带有验证信息 add_header Access-Control-Allow-Credentials true; # 允许跨域访问的域名,可以是一个域的列表,空格隔开,也可以是通配符*(不建议) add_header Access-Control-Allow-Origin $allow_origin; # 允许使用的请求方法,以逗号隔开,可以用 * add_header Access-Control-Allow-Methods 'POST,GET,OPTIONS,PUT,DELETE'; # 预检命令的缓存,如果不缓存每次会发送两次请求,单位为秒。 # 第一次是浏览器使用OPTIONS方法发起一个预检请求,第二次才是真正的异步请求 add_header Access-Control-Max-Age 3600; # 允许脚本访问的返回头 #add_header Access-Control-Allow-Headers 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,X-Requested-With'; add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,credential'; # OPTIONS类的请求,是跨域先验请求 if ($request_method = 'OPTIONS') { return 204; # http状态码 204 (无内容) 服务器成功处理了请求,但没有返回任何内容。可以返回 200 } } }} CORS测试,在一台配置了带有https的nginx的主机上编写一个HTML,在HTML中发送ajax请求,如: 123456789101112131415161718192021222324252627282930<!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>CORS跨域</title> <script src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script></head><body><div style="text-align:center;margin-top: 100px;font-size: 60px;color: brown;cursor: pointer;"> <button onclick="sendAjaxReq()">发送Ajax请求</button></div><script type="text/javascript"> function sendAjaxReq() { $.ajax({ headers: { Authorization: "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyZWd1c2VyNjBAcXNjbiIsImlhdCI6MTYxMDQyMTcxMCwiZXhwIjoxNjE1NjA1NzEwLCJhdXRoVHlwZSI6MH0.Q57Wc7n0gSMuwYqSLe0llN1gssspLKu00pYii9HQGMg" }, type: "get", // contentType: "application/json", url: "https://qzsapi.stcn.com/comment/api/comments/article/2317345?page=0&size=9", success: function (message) { console.log("成功!" + message); }, error: function (a, b, c) { console.log("失败!" + a.statusText); } }); }</script>","link":"/2022/05/02/Nginx%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93/"},{"title":"Keycloak token 有效期配置","text":"关键配置如下图: 更改 realm 配置:Access token lifespan:token 有效时长SSO Session Idle:refresh token 有效时长","link":"/2022/05/02/Keycloak-%E6%9C%89%E6%95%88%E6%9C%9F%E9%85%8D%E7%BD%AE/"},{"title":"Jackson使用指北","text":"1. 概述Jackson是目前在web开发中使用最多,速度最快的一种json序列化和反序列化框架。本文主要结合Jackson在项目中的实际使用进行介绍。 2. 字段忽略2.1 json转POJO时,忽略某些字段只需要在实体类上加上@JsonIgnoreProperties(ignoreUnknown = true)注解就可以。 2.2 POJO转json时,忽略某些字段@JsonIgnore注解用来忽略某些字段,可以用在Field或者Getter方法上,用在Setter方法时,和Filed效果一样 3. json转换为POJO3.1 json字符串直接转POJOobjectMapper.readValue(json, xxx.class) 适用于json字符串与POJO直接一一对应。 3.2 json字符串转JsonNode12345JsonNode rootNode = objectMapper.readTree(json);// 获取其中某个字段的数据JsonNode dataNode = rootNode.findValue("xxx");// 将数据反序列化为POJOobjectMapper.treeToValue(dataNode, xxx.class); 适用于Json字符串的某一部分与JsonNode对应。先获取json响应中的某一部分数据,再将该数据转换为POJO 3.3 json转泛型类1AiReply<SensitiveImageReply> reply = objectMapper.readValue(json, new TypeReference<AiReply<SensitiveImageReply>>() {}); 3.4 json转数组和列表12345678910List<Person> personList = new ArrayList<Person>() { { add(new Person("yangjian", 22)); add(new Person("zhanghaoman", 22)); }};String personJson = this.objectMapper.writeValueAsString(personList);List<Person> persons = this.objectMapper.readValue(personJson, List.class);System.out.println("persons = " + persons); 3.5 类拷贝1234567891011121314 // 拷贝成map Person person = new Person("yangjian", 23); String json = this.objectMapper.writeValueAsString(person); Map<String, Object> map = this.objectMapper.convertValue(person, Map.class); // 链表到数组拷贝 List<String> list = new ArrayList<String>() { { add("one"); add("two"); } }; String[] strings = this.objectMapper.convertValue(list, String[].class); System.out.println("this.objectMapper.writeValueAsString(strings) = " + this.objectMapper.writeValueAsString(strings)); 4. POJO转换为json4.1 设置视图@JsonView(xxx.class)注解可以实现视图显示,序列化的json只会包含有相同视图修饰的字段。并且,视图可以继承。此注解,POJO类上和controller的方法上都需要加。 4.2 自定义某个字段的序列化/反序列化方式使用SpringMVC的@RestController时,我们都知道如果返回的是一个POJO,那么SpringMVC将会自动进行POJO的序列化。但有些时候我们往往需要对该POJO某个字段的序列化和反序列化的方式进行一些修改,以满足我们的业务需求。这时候就可以用到@JsonSerialize和@JsonDeserialize 用法: POJO 12345678910111213141516171819202122232425262728293031323334/** * Tencent AI 身份证OCR响应数据的POJO * * @author rainbow * @since 2020/3/20 10:56 */@Data@EqualsAndHashCode(callSuper = false)@Accessors(chain = true)@JsonIgnoreProperties(ignoreUnknown = true)@NoArgsConstructorpublic class IdCardOcrData { private String name; private String sex; private String nation; @JsonDeserialize(using = IdCardLocalDateDeserializer.class) @JsonSerialize(using = IdCardLocalDateSerializer.class) private LocalDate birth; private String address; private String id; private String authority; /** * 有效时限 */ private String valid_date;} IdCardLocalDateSerializer类 123456789101112/** * @author rainbow * @since 2020/3/20 16:25 */public class IdCardLocalDateSerializer extends JsonSerializer<LocalDate> { @Override public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider serializers) throws IOException { DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); gen.writeString(timeFormatter.format(value)); }} IdCardLocalDateDeserializer类 12345678910111213141516/** * @author rainbow * @since 2020/3/20 13:05 */public class IdCardLocalDateDeserializer extends JsonDeserializer<LocalDate> { @Override public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { String dateString = p.getText(); if (StringUtils.isNotBlank(dateString)) { String[] split = dateString.split("/"); return LocalDate.of(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Integer.parseInt(split[2])); } return null; }} 以上的例子可以将 “2020/2/1” 格式的字符串反序列化为LocalDate类,并将其序列化为 “2020-02-02” 格式的json数据。 4.3 自定义字段序列化方式@JsonFormat此注解用于属性或者方法上(最好是属性上),可以方便的把Date类型直接转化为我们想要的模式,比如@JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss", timezone = "GMT+8") 5. 自定义objectMapper使用SpringBoot自动装配的ObjectMapper在某些情况不太适用,比如它会将值为null的字段也进行序列化返回。因此,我们对ObjectMapper进行设置,达到自己想要的效果。 具体配置类为: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657@Configurationpublic class JacksonConfig { private static final String FORMAT = "yyyy-MM-dd HH:mm:ss"; @Bean public Jackson2ObjectMapperBuilderCustomizer customizer() { return builder -> { builder.locale(Locale.CHINA); builder.timeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); builder.simpleDateFormat(FORMAT); JavaTimeModule javaTimeModule = new JavaTimeModule(); javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer( DateTimeFormatter.ofPattern(FORMAT))); javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss"))); javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss"))); builder.modules(javaTimeModule); }; } @Bean @Primary public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { ObjectMapper objectMapper = builder.createXmlMapper(false).build(); // 通过该方法对mapper对象进行设置,所有序列化的对象都将按改规则进行系列化 // Include.Include.ALWAYS 默认 // Include.NON_DEFAULT 属性为默认值不序列化 // Include.NON_EMPTY 属性为 空("") 或者为 NULL 都不序列化,则返回的json是没有这个字段的 // Include.NON_NULL 属性为NULL 不序列化 objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 允许出现特殊字符和转义符 objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true); // 允许出现单引号 objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); //objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); //objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); /** * 将Long,BigInteger序列化的时候,转化为String */ // SimpleModule simpleModule = new SimpleModule(); // // simpleModule.addSerializer(Long.class, ToStringSerializer.instance); // simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance); // simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); // // objectMapper.registerModule(simpleModule); return objectMapper; }} 该配置类能对类的LocalDateTime等新时间类进行序列化,也能对序列化和反序列化解析方式进行设置。","link":"/2022/05/02/Jackson%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8C%97/"},{"title":"RabbitMQ总结","text":"1. 原理及基本概念原理如图: 其中涉及到的基本分别为: broker:消息队列服务器的实体 exchange:消息交换机。publisher将消息交给exchange,exchange负责将消息发送到queue中 publisher:消息生产者 consumer:消息消费者 channel:消息传递的通道 routing key:路由关键字。exchange根据routing key投递消息与之绑定的queue binding:将queue与exchange按照routing key的规则绑定起来 virtual host:用作不同用户的权限分离 2. 交换机类型最新版本的RabbitMQ有四种交换机类型,分别是Direct exchange、Fanout exchange、Topic exchange、Headers exchange。 Direct Exchange:要求该消息与一个特定的路由键完全匹配。 Fanout Exchange:发送到交换机的消息都会被转发到与该交换机绑定的所有队列上 Topic Exchange:将路由键和某模式进行匹配。 Headers exchange:根据发送的消息内容中的headers属性进行匹配。在绑定Queue与Exchange时指定一组键值对;当消息发送到RabbitMQ时会取到该消息的headers与Exchange绑定时指定的键值对进行匹配;如果完全匹配则消息会路由到该队列,否则不会路由到该队列 3. 集成SpringBoot使用Spring提供的启动器:spring-boot-starter-amqp,就可以很方便的开始使用启动器。 进行测试的时候,最好是将消费者与生产者分为两个不同的项目。使用延迟队列时,消费者最好使用字节数组去接收数据 4. 延迟队列4.1 流程图 4.2 原理RabbitMq实现延迟队列本身有两种方式: 使用延迟插件 使用TTL消息和死信队列 本文阐述的是第二种。原理很简单,TTL消息如果在规定的时间内没有被消费,RabbitMq就会认为是死信,转发到死信交换机。所以我们只需要向延时交换机发送一个我们想延时处理的消息,不去监听延时队列,而是监听死信队列进行消费,就会达到一个延时的效果。 死信的判定标准: 消息的TTL过期且未消费 消费者对broker应答Nack,并且消息禁止重回队列 Queue队列长度已达上限。 4.3 操作步骤本文使用SpringBoot结合RabbitMq的方式进行操作,引入的依赖版本是:2.1.5 - spring - boot - starter - amqp。 步骤如下: 引入amqp依赖 配置文件中配置RabbitMq服务器信息 使用Java代码进行队列、交换机等配置 声明死信队列、死信交换机,绑定死信队列和死信交换机 声明延时交换机,声明延时队列时添加x-dead-letter-exchange、x-dead-letter-routing-key两个参数,绑定延时队列和延时交换机 声明消费者,监听死信队列 第三步代码如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263package top.rainbowecho.rabbit.consumer.config;import com.google.common.collect.ImmutableMap;import org.springframework.amqp.core.Binding;import org.springframework.amqp.core.BindingBuilder;import org.springframework.amqp.core.Queue;import org.springframework.amqp.core.TopicExchange;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;/** * @author rainbow * @since 2020/3/8 18:23 */@Configurationpublic class RabbitMqConfiguration { public static final String DELAY_ROUTING_KEY = "delay.routing"; public static final String DELAY_EXCHANGE = "delay_exchange_10"; public static final String DELAY_QUEUE = "delay_queue"; public static final String DLX_EXCHANGE = "dlx_exchange"; public static final String DLX_QUEUE = "dlx_queue"; public static final String DLX_ROUTING = "#"; @Bean public Binding dlxBinding() { return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with(DLX_ROUTING); } @Bean public TopicExchange dlxExchange() { return new TopicExchange(DLX_EXCHANGE); } @Bean public Queue dlxQueue() { return new Queue(DLX_QUEUE); } /** * 声明延迟队列的时候将该队列绑定到死信交换机 * * @return 延迟队列 */ @Bean public Queue businessQueue() { ImmutableMap<String, Object> arguments = ImmutableMap.of("x-dead-letter-exchange", DLX_EXCHANGE, "x-dead-letter-routing-key", DLX_ROUTING); return new Queue(DELAY_QUEUE, true, false, false, arguments); } @Bean public TopicExchange businessExchange() { return new TopicExchange(DELAY_EXCHANGE); } @Bean public Binding businessBinding() { return BindingBuilder.bind(businessQueue()).to(businessExchange()).with(DELAY_ROUTING_KEY); }} 第四步代码: 12345678910111213141516171819202122232425262728package top.rainbowecho.rabbit.consumer.listener;import com.rabbitmq.client.Channel;import lombok.extern.slf4j.Slf4j;import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.amqp.support.AmqpHeaders;import org.springframework.messaging.handler.annotation.Header;import org.springframework.stereotype.Component;import top.rainbowecho.rabbit.consumer.config.RabbitMqConfiguration;import java.io.IOException;/** * @author rainbow * @since 2020/3/8 18:44 */@Component@Slf4jpublic class BusinessMqListener { @RabbitListener(queues = RabbitMqConfiguration.DLX_QUEUE) public String businessMqListen(String mess, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException { log.info("生产者发送的消息内容:" + mess); channel.basicAck(deliveryTag, false); return "消费者已经收到deliverTag为 " + deliveryTag + " 的消息"; }} 4.4 生产者发送测试消息123456789101112131415161718192021222324252627282930package top.rainbowecho.rabbit;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.amqp.core.MessageDeliveryMode;import org.springframework.amqp.rabbit.core.RabbitTemplate;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;import top.rainbowecho.rabbit.consumer.config.RabbitMqConfiguration;@RunWith(SpringRunner.class)@SpringBootTestpublic class RabbitApplicationTests { @Autowired private RabbitTemplate rabbitTemplate; @Test public void sendMessToMq() throws InterruptedException { String mess = "hello, 死信队列"; rabbitTemplate.convertAndSend(RabbitMqConfiguration.DELAY_EXCHANGE, RabbitMqConfiguration.DELAY_ROUTING_KEY, mess, message -> { message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT); message.getMessageProperties().setExpiration("10000"); return message; }); // 避免因为生产者发送成功而停掉整个程序 Thread.sleep(20000); }}","link":"/2022/05/02/RabbitMQ%E6%80%BB%E7%BB%93/"},{"title":"es 指北","text":"1 概述基于 ES 7.14.1,主要讲述 es 如何搭建、集成以及一些常见的坑 2 es 搭建https://gitee.com/zhengqingya/docker-compose.git 3 es 集成使用 spring data elasticsearch Spring data elasticsearch 与 springboot、elasticsearch 版本对照表 es 的版本可以稍高,比如笔者用的是 springboot 2.4.x,但是 es 用的是 7.14.1 集成方式: 引入 maven 依赖 1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId></dependency> 定义 repository 12public interface EsStatusRepository extends ElasticsearchRepository<EsStatus, String> {} 定义了 repository 之后,就可以直接通过 autowire 注解引入,然后进行 es 数据操作 4 常见坑4.1 时间存储与显示使用 field 注解,格式化模式定义为:uuuu-MM-dd HH:mm:ss 这样,存储到 es 中的日期就是 年月日 时分秒 格式 但是,这个时候在 kibana 上显示出来,日期可能跟插入的日期不对,这是因为 kibana 设置了显示时的时区。更改方式如下: 设置为 UTC 4.2 开发工具点击开发工具,能够直接调用 es api 完成操作","link":"/2022/06/24/es-%E6%8C%87%E5%8C%97/"},{"title":"spring data 使用多种数据源的 repository","text":"1 前言笔者在使用 spring官方提供的关于 neo4j 和 elasticsearch 的 spring data 工程时发现,这个工程对应 repository 无法同时扫描 后面在官方文档中,还是找到了解决方法,解决方法如下: 2 具体方案12345@Configuration@EnableNeo4jRepositories("com.example.neo4j.repository")@EnableElasticsearchRepositories("com.example.es.repository")public class RepositoryConfig {} 添加这样一个配置,指定 repository 对应的包名即可","link":"/2022/06/24/spring-data-%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%A7%8D%E6%95%B0%E6%8D%AE%E6%BA%90%E7%9A%84-repository/"},{"title":"nginx 配置 wss","text":"1 概述实际上,wss 配置也是基于 https。因此,在配置 wss 之前,应该先配置好 https。 以 conf.d 下 nginx 配置文件举例,https 参考配置如下: 123456789101112131415161718192021222324252627282930313233upstream 服务名(英文) { ip_hash; # 内网地址 server 内网机器ip:9998 weight=1 fail_timeout=10s max_fails=1;}server { listen 80; server_name 域名; return 301 https://$server_name$request_uri;}server { listen 443 ssl; server_name 域名; # ssl证书地址 ssl_certificate pem文件; ssl_certificate_key key文件; # ssl验证相关配置 ssl_session_timeout 5m; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; location / { add_header X-Upstream $upstream_addr; proxy_pass http://服务名(英文); }} 以上配置即可将一个后端项目通过 nginx 进行反向代理、负载均衡以及配置 https。 2 wss 实现在配置好 https 之后,假如你的后端项目本身已经添加了 websocket 端点,那么只需要再加三行配置就可以再让 nginx 配置好 wss。示例配置如下: 12345678910111213141516171819202122232425262728293031323334upstream 服务名(英文) { ip_hash; # 内网地址 server 内网机器ip:9998 weight=1 fail_timeout=10s max_fails=1;}server { listen 80; server_name 域名; return 301 https://$server_name$request_uri;}server { listen 443 ssl; server_name 域名; # ssl证书地址 ssl_certificate pem文件; ssl_certificate_key key文件; # ssl验证相关配置 ssl_session_timeout 5m; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; location / { add_header X-Upstream $upstream_addr; proxy_pass http://服务名(英文); proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }} 注意 location 代码段新加的三行配置。","link":"/2022/06/24/nginx-%E9%85%8D%E7%BD%AE-wss/"},{"title":"Nginx反向代理","text":"1. 概述Nginx 支持两种反向代理,四层反向代理和七层反向代理。这个层数指的是网络协议的层级,以 OSI 七层模型为准。 七层代理指的是代理具体的协议,如 http 和 https 协议。 四层代理是直接针对 TCP、UDP 等 下面来看一下具体的实现方式 2. 七层反向代理七层反向代理的通用场景是反向代理 web 服务,配置 https 证书,使得这个 web 服务可以通过 https 方式访问。 举个例子,比如在机器 A 运行了一个 rabbitmq 服务,这个服务监听的是机器 A 的 15672 端口。但如果我们想通过 https 访问的话,就需要在机器 B 上运行一个 Nginx 服务,配置七层反向代理和 https 证书文件。 配置方式为: 12345678910111213141516171819202122232425262728293031upstream mq { ip_hash; # 内网地址 server 127.0.0.1:15672 weight=1 fail_timeout=10s max_fails=1;}server { listen 80; server_name mq.rainbowecho.cn; return 301 https://$server_name$request_uri;}server { listen 443 ssl; server_name mq.rainbowecho.cn; # ssl证书地址 ssl_certificate /etc/nginx/ssl/mq.rainbowecho.cn/mq.rainbowecho.cn.pem; ssl_certificate_key /etc/nginx/ssl/mq.rainbowecho.cn/mq.rainbowecho.cn.key; # ssl验证相关配置 ssl_session_timeout 5m; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; location / { add_header X-Upstream $upstream_addr; proxy_pass http://mq; }} 3. 四层代理七层代理的局限性在于它只能代理 web 服务并手动配置 https。但如果我们需要代理 MySQL 或者为已经配置了 https 的服务配置代理,这个时候就需要四层反向代理。 由于四层反向代理是直接转发 TCP/UDP 连接,因此适配多种协议。 以代理 drone 为例,由于 drone 容器本身就配置了 https 访问,因此必须使用四层代理。配置如下,通过使用 Nginx 的 stream 模块,将匹配到的域名转发到对应的 upstream 上,upstream 中配置的就是代理的服务。 12345678910111213141516171819stream { map_hash_bucket_size 64; map $ssl_preread_server_name $backend_pool { drone.rainbowecho.cn drone_server; } upstream drone_server{ server 172.20.126.143:444; } server { listen 443; ssl_preread on; proxy_pass $backend_pool; proxy_connect_timeout 15s; proxy_timeout 15s; proxy_next_upstream_timeout 15s; }} 4. 总结对于同一个 Nginx,四层代理和七层代理监听的端口不能相同。也就是说,如果你想在同一个 nginx 上使用四层代理和七层代理去进行 https 代理的话,那么将会端口冲突。","link":"/2022/05/02/Nginx%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86/"},{"title":"spring 自定义注入 bean","text":"1 概述有时候,需要想通过编程方式自定义一些 bean,因为我们需要根据业务逻辑,将这些 bean 需要以特定格式命名。 2 具体实现实现方式如下: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980@Slf4j@Componentpublic class ApplicationContextOperator implements ApplicationContextAware { private ApplicationContext applicationContext = null; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { log.info("application context 初始化"); this.applicationContext = applicationContext; registerStrategyBean(); } private void registerStrategyBean() { log.info("开始注册策略 bean"); List<String> allImplClassesByInterface = ClazzUtil.getAllImplClassesByInterface(FeatureInferStrategy.class); addStrategyBean(FeatureConstant.SPOT, allImplClassesByInterface); addStrategyBean(FeatureConstant.WRINKLE, allImplClassesByInterface); } private void addStrategyBean(String featureType, List<String> allImplClassesByInterface) { String featureName = TaskListUtil.getFeatureName(featureType); allImplClassesByInterface.stream().filter(e -> e.toLowerCase().contains(featureName.toLowerCase())).findFirst().ifPresent(clazzName -> { try { Class<?> subclass = Class.forName(clazzName); Constructor<?> constructor = subclass.getConstructor(String.class); FeatureInferStrategy strategyImpl = (FeatureInferStrategy) constructor.newInstance(featureName); String beanName = generateStrategyBeanName(strategyImpl.type()); registerSingletonBean(beanName, strategyImpl); } catch (ClassNotFoundException e) { e.printStackTrace(); log.info("no sub class, {}", clazzName); } catch (NoSuchMethodException e) { e.printStackTrace(); log.info("sub class {} no such method {}", clazzName, e.getMessage()); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (InstantiationException | IllegalAccessException e) { log.info("msg: {}", e.getMessage()); e.printStackTrace(); } }); } /** * 生成策略类的 bean 名称 * * @param type * @return */ public static String generateStrategyBeanName(String type) { return type + "Impl"; } /** * 动态注入单例bean实例 * * @param beanName bean名称 * @param singletonObject 单例bean实例 * @return 注入实例 */ public Object registerSingletonBean(String beanName, Object singletonObject) { log.info("开始动态注入单例 bean, {}", beanName); //将applicationContext转换为ConfigurableApplicationContext ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) this.applicationContext; //获取BeanFactory DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getAutowireCapableBeanFactory(); //动态注册bean. defaultListableBeanFactory.registerSingleton(beanName, singletonObject); //获取动态注册的bean. return configurableApplicationContext.getBean(beanName); }} 原理也很简单,通过 application context 的 spi,在注入 application context 的时候,进行 bean 的手动注册。 为什么要在这个时候注册呢? 因为这个时候能够在程序启动较早的时候直接拿到 BeanFactory,如果要在启动程序较晚的时候再去拿,可能在跑测试用例的时候,并不能进行注册操作。 因为单元测试启动的时间,可能比你手动注册好 bean 之前还早。","link":"/2022/06/24/spring-%E8%87%AA%E5%AE%9A%E4%B9%89%E6%B3%A8%E5%85%A5-bean/"},{"title":"websocket session 分布式集群处理","text":"1 概述由于业务需要,导致开发的后端程序需要与前端通过 websocket 进行消息通信。于是采用了网上的一些做法,使用 spring websocket 集成 websocket 到 java 后端项目中。 但到了后来发现,这种将 websocket 端点与应用程序绑定在一起的方式有很大的问题。首先是业务服务的更新会导致跟客户端的连接断开,其次是比较难对 websocket 服务进行扩容。 于是想出了以下方案来针对上面两个主要问题来进行优化。 2 分布式 websocket session 具体实现之前通过网上资料,得到 websocket session 存储方式是将 session 存储到服务内存中。 2.1 websocket session 持久化基于上述方式实现,想到的第一个优化方式是参考 http session 一样,将 http session 序列化存储到 redis 中,以达到分布式的效果。 然而,等实现的时候才发现 websocket session 并不能序列化,这也就导致这种方案直接放弃。 2.2 websocket session 分布式 既然 websocket session 不能持久化,那是不是就意味着不能实现分布式呢? 当然不会。持久化是一种实现方式,我们也可以不持久化。那不持久化又怎么实现分布式呢? 我们可以参照注册中心和客户端服务的方式来处理。也就是说,我们可以将 redis 作为注册中心,websocket 服务自己去添加或删除自己服务中的 websocket session 信息,并将自身的 ip、端口等信息一起传输到 redis 特定键存储。 实现方式如图: 这样,当业务服务需要给某个用户的 websocket 客户端发信息时,只需要从 redis 的特定键中查找出用户属于哪个 websocket 服务,调用 websocket 服务的接口。websocket 服务收到业务服务的接口请求,则发送消息。","link":"/2022/06/24/websocket-session-%E5%88%86%E5%B8%83%E5%BC%8F%E9%9B%86%E7%BE%A4%E5%A4%84%E7%90%86/"},{"title":"解决Python3pip安装速度慢问题","text":"1. 概述Python的pip源默认为国外源,下载速度慢。因此可以通过更换为国内源的方式进行解决。分为两种更换方式: 临时更换 全局更换 windows Linux 2. 临时更换 在使用pip的时候加参数 -i https://pypi.tuna.tsinghua.edu.cn/simple 如: 1pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pyspider 3. 全局更换3.1 windows环境 点击此电脑,在最上面的的文件夹窗口输入 :%APPDATA%,回车 在该目录下新建一个pip目录,并在该文件夹下再新建一个pip.ini文件 在pip.ini文件中添加以下信息 1234[global]timeout = 6000index-url = https://pypi.tuna.tsinghua.edu.cn/simpletrusted-host = pypi.tuna.tsinghua.edu.cn 测试命令: 1pip install requests 3.2 linux环境与windows类似 Linux下,在home目录下创建一下.pip文件夹(命令:mkdir .pip) 在.pip文件夹下创建pip.conf文件(命令:touch pip.conf),文件内容如上。文件夹要加“.”,表示是隐藏文件夹) 至此,可以成功解决各种情况下pip安装Python库速度慢的问题。","link":"/2022/05/02/%E8%A7%A3%E5%86%B3Python3pip%E5%AE%89%E8%A3%85%E9%80%9F%E5%BA%A6%E6%85%A2%E9%97%AE%E9%A2%98/"},{"title":"常见接口优化思路","text":"1 概述大多数时候,等到功能实现完成,进行性能测试后会发现,接口速度响应速度不及预期。 一般的接口响应速度与用户体验对应关系如下: 接口响应耗时 用户体验 200 ms 及以下 瞬时发生 200 ms - 1s 用户可以感受到一些延迟,但却是可以接受的 1s 以上 用户就会有明显等待的感觉,等待时间越长,用户的使用体验就越差 比较好的系统的一个接口响应指标为: 99% 的接口请求响应速度要在 200 ms 以内, 99.99% 的请求要在 1s 以内 实际上,接口耗时有两个原因:io 与 cpu 计算。 io 很好理解,作为业务后端,必须有的数据库 io,还可能会有网络 io,文件 io。 cpu 计算主要是指通过编程语言进行算法运算的耗时,比如大数据的算法计算等。 大多数情况下,我们接口耗时较长都是因为 io。因为大概率,我们调用 cpu 计算,可能也就是算算 md5 之类的东西。 并且,对于 cpu 计算耗时的情况,也比较好处理,换用性能更好的算法就行了。 因此,在对接口优化之前,应该先分清接口所属的类型,是属于 CPU 型还是 io 型。 本文后面主要对 io 型的接口优化进行分析 2 优化方案在笔者之前遇到的项目中,大概可以以下几步去对接口进行优化: 使用并行进行处理 减少 io 次数 提升中间件性能 2.1 使用并行进行处理假设有这样一个场景,你需要根据多个 url 去进行网络请求下载图片。 每个网络请求都是会有耗时的,如果使用 for 循环或者普通的流操作去处理的话,最后下载这些图片的耗时,自然会是每个图片下载耗时累加的结果。 我们也很容易想到使用多线程来处理这个问题。在 Java8 之后,jdk 自身就提供了几种方式来让我们更加方便、透明的实现多线程处理数据。 parallel stream completable future 并行流是最简单、快速的一种实现方式,通过并行流可以很方便的实现并行,并且通过 lambda 表达式进行数据转换处理。 但同时它也有缺点。先想想我们之前想实现并行是怎么做的? 我们之前要实现并行,并且为了充分利用资源,往往会使用一个线程池,并在池中定义多个线程,将任务提交到池中,让线程池去分配线程处理任务。 并行流虽然是帮我们 ”透明“的实现了并行,但实际底层实现也还是基于线程池操作线程来执行任务的。并行流底层对应了 jdk 自身内部定义的一个公共线程池(ForkJoinPool)。 因此,在极端情况下,假设这个公共线程池的线程因为任务执行耗时比较久,一直卡住,那么它将不能执行新的任务。笔者在以前线上环境中就曾经遇到过这样一个问题。 当然,这种情况比较极端,如果只是少量使用并行流的话,其实也不需要担心这个问题。 另外就是 completable future 了。 这是 jdk8 的新特性。completable future 相比 parallel stream,优势就在于能够自定义线程池,是专门用来批量处理同种任务的。 代码示例如下: 123456789101112131415161718192021222324252627282930313233343536373839404142ArrayList<String> urls = new ArrayList<>(4);for (int i = 0; i < 100; i++) { urls.add("http://www.baidu.com");}int threads = (int) (Runtime.getRuntime().availableProcessors() * 0.8 * 100);System.out.println("threads = " + threads);// 自定义线程数,充分利用资源Executor executorService = Executors.newFixedThreadPool(Math.min(urls.size(), threads), new ThreadFactory() { private AtomicInteger threadNum = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setDaemon(true); thread.setPriority(5); thread.setName("Thread Monitor " + threadNum.incrementAndGet()); return thread; }});// 分为两个流操作进行调用,避免阻塞List<CompletableFuture<String>> collect = urls.stream().map(url -> CompletableFuture.supplyAsync(() -> { HttpResult httpResult = HTTP.builder().baseUrl(url).build().sync("").get(); int random = RandomUtil.randomInt(1, 10); try { int millis = random * 1000; String name = Thread.currentThread().getName(); System.out.println("Thread " + name + " millis = " + millis); Thread.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } return httpResult.getBody().toString();}, executorService)).collect(Collectors.toList());// 这样操作,耗时将会是最长的一个调用的时间Stopwatch s2 = Stopwatch.createStarted();List<String> results = collect.stream().map(CompletableFuture::join).collect(Collectors.toList());long d2 = s2.elapsed(TimeUnit.SECONDS);log.info("d2: {}", d2); 不过 completable future 相比 parallel stream,缺点就是在调用方式上比较麻烦。 2.2 减少 io 次数减少 io 次数,可以通过几种方式进行处理: 批量操作 预加载数据 优化逻辑 这里的批量处理是指使用一次批量处理代替多次循环。 比如,本来需要执行多条 sql 插入多条数据,但通过修改 sql,批量插入,那么只需要一条 sql 语句即可;更新多条数据的时候,也可以通过重复则更新的逻辑,批量更新。 预加载数据是指,在数据量不多的情况下,可以先将一些基础的数据查询出来,然后使用在内存中进行计算。比如,教师和学生这两种数据,查询到内存中之后,可以将两者根据外键进行分组得到 map,这样结果前面的批处理,也能减少一些 io 次数。 优化逻辑则是通过修改逻辑,减少不必要的 io 次数,比如将数据库查询出的数据在方法中进行传递,那么在后续逻辑的方法中则不需要再进行 io。 2.3 提升中间件性能提升中间件性能有以下几种方式: 更换好的硬件 更改参数进行调优 使用更优的中间件 这个方式之所以要放到最后是因为,普遍来讲,这个工作在技术选型,中间件环境搭建的时候就会设置好一些东西。 因此,主要的接口调优思路还是会放在前两项上。","link":"/2022/06/24/%E5%B8%B8%E8%A7%81%E6%8E%A5%E5%8F%A3%E4%BC%98%E5%8C%96%E6%80%9D%E8%B7%AF/"},{"title":"Spring Cloud Gateway CORS 配置","text":"1 前言笔者在使用 spring cloud gateway 代理 springboot 项目的时候,发现:虽然springboot项目配置了 cors,但是前端通过 gateway 访问接口时仍然存在跨域问题。 通过排查,可以确认是 spring cloud gateway 没有配置跨域,因此,需要对其进行配置。 基于 spring cloud gateway 3.1.3 版本配置。 2 具体配置由于 springboot 项目端可以通过配置文件配置 allow-origin,因此 gateway 这边就直接放开所有。 配置如下: 12345678910111213141516171819202122232425262728293031323334spring: application: name: rage-gateway cloud: gateway: filter: remove-hop-by-hop: headers: # 以下是去掉网关默认去掉的请求响应头 - trailer - te - keep-alive - transfer-encoding - upgrade - proxy-authenticate - connection - proxy-authorization - x-application-context # 以下是去掉服务层面定义的跨域 - access-control-allow-credentials - access-control-allow-headers - access-control-allow-methods - access-control-allow-origin - access-control-max-age - vary globalcors: cors-configurations: '[/**]': allowCredentials: true allowedOriginPatterns: "*" allowedHeaders: "*" allowedMethods: "*" maxAge: 3628800 add-to-simple-url-handler-mapping: true 要注意的点如下: 要去除 header,不然列表中的 header 会出现两次 配置所有域名都可以访问的时候,如果allowCredentials为 true,则必须使用 allowedOriginPatterns,而不能使用 allowedOrigins add-to-simple-url-handler-mapping 为 true 时,代表对 cors 的预检请求直接放行,不需要计算","link":"/2022/08/01/Spring-Cloud-Gateway-CORS-%E9%85%8D%E7%BD%AE/"},{"title":"k8s 入门","text":"K8s 指南文章总结: 搭建一个基础的 k8s 集群,并以部署 nginx 为例,模拟服务进行部署,配置外网访问 1 创建 k8s 集群sealos 3.3.9 机器要求: 机器数:k8s-manager × 1,k8s-master × 3,k8s-node × 2 硬件:manager、master 统一 2c 4g,node 大于或等于 2c 4g,硬盘大小统一为 150g k8s 版本:1.22.0 k8s 1.22 默认使用 ipvs,不使用 iptables。 主机要求: hostname 每个机器必须不同 时区保持一致 关闭防火墙、selinux 操作系统均为 centos7 用户必须为 root,机器密码必须统一 查看节点信息: kubectl get nodes 2 创建工作负载资源2.1 创建 node创建 namespace: kubectl create namespace k8s-tutorial Pod 配置: 123456789101112131415161718192021222324252627282930apiVersion: v1kind: Podmetadata: name: "nginx" namespace: k8s-tutorial labels: app: "nginx"spec: containers: - name: nginx image: "nginx:latest" imagePullPolicy: IfNotPresent resources: limits: cpu: 200m memory: 500Mi requests: cpu: 100m memory: 200Mi ports: - containerPort: 80 name: http volumeMounts: - name: localtime mountPath: /etc/localtime volumes: - name: localtime hostPath: path: /usr/share/zoneinfo/Asia/Shanghai restartPolicy: Always 运行 pod: kubectl apply -f xxxx 查看 pod kubectl get pod -n k8s-tutorial -o wide 进入 pod 容器: kubectl exec -it nginx -n k8s-tutorial -- bash 访问 nginx node: curl -X GET http://100.113.22.195 查看 pod 详细信息与事件: kubectl describe pod nginx -n k8s-tutorial 2.2 pod 健康检查与恢复机制pod 健康检查方式: 命令式 http 方式 tcp grpc 方式 k8s 有多种探测器: 启动探测器 就绪探测器 存活探测器 如果一个 pod 被探测器检测为异常,那么 k8s 提供了几种方式对 pod 进行恢复 Always:只要 pod 不在运行状态,就重启容器 OnFailure:只在异常时才重启容器 Never:永远不重启容器 2.3 initContainer正常来讲,一个 pod 只运行一个容器。但有些时候,会需要运行多个容器。这种情况,统称为 sidecar 模式,比如 war 包与 web 服务,还有日志收集。 因此,需要一个 initContainer 来持有 pod 的 namespace 环境 2.4 创建 deployment注意:创建 deployment 时,pod 模板必须添加健康检测机制 因为如果不添加健康检测机制,deployment 将无法知道容器是否需要更新回滚 滚动更新(rolling-update)机制: deployment 控制 replicaset,replicaset 控制 pod 在默认机制下,deployment 每次更新,都是创建一个新的 replica set,新的 replica set 创建新的 pod(可以将 replica set 理解为 deployment 更新的版本)。这样,如果某次 deployment 更新失败,将可以通过回退 replica set 进行应用发布回退。但也可以通过设置,不创建新的 replica set deployment 蓝绿发布方式:https://github.com/ContainerSolutions/k8s-deployment-strategies.git 2.5 创建 serviceservice 有四种类型: nodeport loadbalancer clusterip external name 除了 cluster ip,其他都能外部访问 需要特殊说明的是,loadbalancer 类型的 service 需要集群本身带有负载均衡器。而自建的 k8s 集群默认是没有这个功能的,云厂商的 k8s 集群有这个功能。 因此,如果自建 k8s 集群想使用 loadbalancer 类型 service,得自己引入负载均衡器。 目前使用最广的是 metallb 2.5.1 metallb 安装官方文档:https://metallb.universe.tf/installation/ 分三步: 安装 metallb 配置 metallb 为二层模式 创建 loadbalancer 类型 service 2.5.2 创建 service123456789101112131415161718192021apiVersion: v1kind: Servicemetadata: name: nginx-service namespace: k8s-tutorialspec: selector: app: nginx type: LoadBalancer sessionAffinity: ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 10800 ports: - name: nginx-service protocol: TCP port: 80 targetPort: 80 # If you set the `spec.type` field to `NodePort` and you want a specific port number, # you can specify a value in the `spec.ports[*].nodePort` field. # nodePort: 80 sessionAffinity 可以理解为负载均衡规则,支持 none 和 clientIp。none 可以理解为随机访问,clientIp 可以理解为 ip hash 查看 service 信息: kubectl get service -n k8s-tutorial -o wide 此时,已经包含 external ip 信息 2.6 创建 ingress 下载 ingress-controller 配置文件 https://kubernetes.github.io/ingress-nginx/deploy/ 修改官方 ingress - controller 配置文件,添加 hostnetwork 配置 修改镜像源 https://hub.docker.com/u/anjia0532 ingress 配置: 1234567891011121314151617181920212223# https://kubernetes.io/docs/concepts/services-networking/ingress/#the-ingress-resourceapiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: nginx-ingress namespace: k8s-tutorial annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/rewrite-target: /spec: ingressClassName: nginx rules: - host: nginx.k8s.io http: paths: - path: / pathType: Prefix backend: service: name: nginx-service port: number: 80 检查安装结果: 检查安装结果: 访问: 客户端 client 添加 host 解析: 浏览器访问:","link":"/2022/07/18/k8s-%E5%85%A5%E9%97%A8/"},{"title":"keycloak 使用外部数据库","text":"","link":"/2022/07/14/keycloak-%E4%BD%BF%E7%94%A8%E5%A4%96%E9%83%A8%E6%95%B0%E6%8D%AE%E5%BA%93/"},{"title":"mysql 事务响应过慢","text":"","link":"/2022/07/19/mysql-%E4%BA%8B%E5%8A%A1%E5%93%8D%E5%BA%94%E8%BF%87%E6%85%A2/"},{"title":"keycloak 自定义 token 响应","text":"1 前言大多数时候,我们需要将用户额外的一些封装到 token 中进行返回。 比如这个用户,在它的 attribute 中具有一个 avatar 字段。如果我们只是使用 keycloak 标准的 token 的封装方式,里面根本不会有这个字段。 那么,怎么让 token 信息中包含这个字段呢? 2 具体实现其实,使用 client 的 mapper 就能解决 具体操作流程如下: 找到对应 client,使用哪个 client 进行认证的,就选哪个。点击进入 找到 mapper,创建一个新的 mapper 此处我想将用户 attribute 中的 avatar 配置成 token 中的 avatar 字段,于是需要这样配置: Token claim name 就是 token 中字段的名称","link":"/2022/07/14/keycloak-%E8%87%AA%E5%AE%9A%E4%B9%89-token-%E5%93%8D%E5%BA%94/"},{"title":"mysql 并发新增数据问题","text":"","link":"/2022/07/19/mysql-%E5%B9%B6%E5%8F%91%E6%96%B0%E5%A2%9E%E6%95%B0%E6%8D%AE%E9%97%AE%E9%A2%98/"},{"title":"如何基于 keycloak spi 开发第三方社交账号登录","text":"1 概述本文以笔者开发的 keycloak 15.0 的微信第三方登录插件为例,讲述一下 keycloak 中第三方登录的社交","link":"/2022/07/14/%E5%A6%82%E4%BD%95%E5%9F%BA%E4%BA%8E-keycloak-spi-%E5%BC%80%E5%8F%91%E7%AC%AC%E4%B8%89%E6%96%B9%E7%A4%BE%E4%BA%A4%E8%B4%A6%E5%8F%B7%E7%99%BB%E5%BD%95/"},{"title":"基于 keycloak 的前后端分离架构","text":"1 概述本文主要讲述,在前后端分离的情况下,如何使用 keycloak 完成用户认证与鉴权。 2 环境架构环境架构图: 核心要点是,将 keycloak 作为一个统一认证平台。多个系统可以使用同一个 keycloak 进行认证、授权","link":"/2022/07/14/%E5%9F%BA%E4%BA%8E-keycloak-%E7%9A%84%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E6%9E%B6%E6%9E%84/"},{"title":"keycloak https配置","text":"1 前言默认情况下,直接启动 keycloak,master realm 都是要求外部请求需要 https 方式访问的。 有两种方式可以解决: 使用 keycloak 自己生成的证书 自己生成证书或者阿里云证书 第一种方式,不需要自己生成证书,操作简单;但生成的证书用浏览器访问会被认为不安全,只能用于测试。 第二种方式,操作复杂一点;但生成的证书安全可靠。 下文会针对两种方式的实现都进行描述 环境要求: Docker Docker-compose 搭建之前,需要先安装 docker、docker-compose keycloak 版本:15.0.0 2 自动生成直接在 docker-compose 文件中,将宿主机的 443 端口映射到 keycloak 的 8443 端口即可。 123456789101112131415161718192021222324252627282930313233343536373839404142version: '2.1'services: keycloak: image: jboss/keycloak:15.0.0 container_name: keycloak ports: - 443:8443 environment: DB_VENDOR: mysql DB_DATABASE: keycloak DB_USER: keycloak DB_PASSWORD: keycloak KEYCLOAK_USER: admin KEYCLOAK_PASSWORD: admin JDBC_PARAMS: "useSSL=false" depends_on: mysql: condition: service_healthy networks: - keycloak-network mysql: image: mysql:5.7 container_name: mysql ports: - 3306:3306 healthcheck: test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] interval: 5s timeout: 3s retries: 10 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: keycloak MYSQL_USER: keycloak MYSQL_PASSWORD: keycloak networks: - keycloak-networknetworks: keycloak-network: 3 手动生成 安装 certbot 1yum -y install certbot 生成证书 注意,这一步进行操作时,需要保证 80、443 端口打开且未被占用 1certbot certonly --standalone -d example.domain.cn -d example.domain.cn 使用证书 123cp /etc/letsencrypt/live/example.domain.cn/fullchain.pem ./certs/tls.crtcp /etc/letsencrypt/live/example.domain.cn/privkey.pem ./certs/tls.key 修改 docker-compose 文件 123456789101112131415161718192021222324252627282930313233343536373839404142434445version: '2.1'services: keycloak: image: jboss/keycloak:15.0.0 container_name: keycloak ports: - 443:8443 volumes: - ./certs/tls.crt:/etc/x509/https/tls.crt - ./certs/tls.key:/etc/x509/https/tls.key environment: DB_VENDOR: mysql DB_DATABASE: keycloak DB_USER: keycloak DB_PASSWORD: keycloak KEYCLOAK_USER: admin KEYCLOAK_PASSWORD: admin JDBC_PARAMS: "useSSL=false" depends_on: mysql: condition: service_healthy networks: - keycloak-network mysql: image: mysql:5.7 container_name: mysql ports: - 3306:3306 healthcheck: test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] interval: 5s timeout: 3s retries: 10 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: keycloak MYSQL_USER: keycloak MYSQL_PASSWORD: keycloak networks: - keycloak-networknetworks: keycloak-network: 其实这个方式的实现原理就是将证书文件,挂载容器内部即可。 但需要注意的是,证书文件名称必须为 tls.key、tls.crt 如果使用阿里云 nginx 证书的话,那么证书文件映射应该配置为: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647version: '2.1'services: keycloak: image: jboss/keycloak:15.0.0 container_name: keycloak ports: - 443:8443 volumes: - ./certs/xxx.pem:/etc/x509/https/tls.crt - ./certs/xxx.key:/etc/x509/https/tls.key environment: DB_VENDOR: mysql DB_DATABASE: keycloak DB_USER: keycloak DB_PASSWORD: keycloak KEYCLOAK_USER: admin KEYCLOAK_PASSWORD: admin JDBC_PARAMS: "useSSL=false" depends_on: mysql: condition: service_healthy networks: - keycloak-network mysql: image: mysql:5.7 container_name: mysql ports: - 3306:3306 healthcheck: test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] interval: 5s timeout: 3s retries: 10 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: keycloak MYSQL_USER: keycloak MYSQL_PASSWORD: keycloak networks: - keycloak-networknetworks: keycloak-network:","link":"/2022/07/14/keycloak-https%E9%85%8D%E7%BD%AE/"},{"title":"自动注册服务路由的实现","text":"1 概述最近由于公司后端需要开发的服务多了起来之后,于是考虑通过 spring cloud gateway 统一代理后端服务。 这里说的后端服务范围比较广,不止包含不同的服务,还包含相同服务在不同环境下的实例。 但是引入了 spring cloud gateway 之后,问题也随之而来了。 第一个问题是, spring cloud gateway 的路由配置都是通过 yaml 文件进行配置的,如果需要更改或者添加路由,那么得重新发布网关。这也就意味着,网关更新的过程中,这个网关代理的服务全部都会变为不可访问。并且,我希望服务上线之后网关能自动发现,服务下线之后网关也能自动感知。 第二个问题是,我们虽然存在多个后端服务,但现在并没有引入 spring cloud 的基础组件依赖,每个服务实际上都是单独的 SpringBoot 服务,也就是说我们没有注册中心、配置中心。但是对于相同服务的多实例,也需要进行负载均衡。 第二个实际上是比较好解决的,我们可以使用现成的 spring cloud 的解决方案,引入注册中心,比如 nacos。然后将网关与注册中心进行集成,配置 lb 的服务名即可简单的实现。 但笔者并不打算这样做,因为笔者是打算直接通过 spring boot + kubernetes 实现云原生,并不打算使用 spring cloud 的方案。(后面会单独说明为什么会这样选择) 下面将会根据这两个问题,分享一下笔者的解决方案。 2 具体实现本文使用的版本信息如下: spring cloud gateway:3.1.3 java: jdk17 2.1 动态路由与自动注册要解决第一个问题,首先需要考虑如何实现动态路由,即网关服务端允许客户端动态的添加、删除路由信息。 另外,自动注册有 “推” 和 “拉” 两种方式,考虑我们希望网关除了功能变动才需要发布,所以笔者选择服务端推送注册信息的方式进行实现。 动态路由这个功能 gateway 本身是支持的,我们只需要引入 actuator 的依赖,开放端点,就可以直接使用 gateway 已经实现的 gateway 端点 api 进行路由的操作。 但是使用 gateway 原生实现的端点 api 添加了路由之后,除非网关重启,否则路由会一直存在。 那么这就意味着如果我在服务端启动完成之后,自动调用一次接口,那么网关上将会永远有这个服务的信息,那么就无法实现网关对服务实例的下线感知。 所以,这就需要看源码,分析创建路由流程,并进行逻辑替换。并且,考虑到网关高可用后,路由信息在多个网关实例间共享的问题,需要将路由信息放到统一的一个地方进行存储。多个网关实例统一从这个地方读取路由信息就可以了。 分析源码发现,spring cloud gateway 默认用的是 CachingRouteLocator,也就是在对路由信息进行了存储之后,在内存之中做了路由信息的缓存。 并且,spring cloud gateway 的路由存储分为两种: 配置文件读取 redis 存储(直接使用 set 操作对路由信息进行存储) 而且,看了源码之后发现,如果要替换这两个类,暂时没有像 spring security 那种重新实现方法,或者实现类的方式进行替换。(吐槽一下,从这一点上来讲,gateway 对组件的抽象水平跟 spring security 真是差了一些)。 所以,笔者看了源码,只有对这个两个 bean 进行强制替换了。 locator 的逻辑改为,每次查询,都调用 repository 去查询。route repository 的逻辑改为:添加路由的时候,默认设置一个超时时间,如果后端服务超过这个时间没有发送请求,那么就认为服务已经下线,相当于维持心跳 先增加一个配置类: 12345678910111213141516171819202122232425262728293031323334/** * 打开这个配置意味着使用 redis 存储 route,并且 route 查询不使用缓存 */@Configuration@EnableConfigurationProperties(RainbowCloudGatewayProperties.class)public class GatewayRedisRouteConfig { @Autowired private RainbowCloudGatewayProperties rainbowCloudGatewayProperties; /** * 替代原有的 route locator,并且新的 locator 的名称必须为 'cachedCompositeRouteLocator',简直无语 * * @param routeLocators * @return */ @Primary @Bean(name = "cachedCompositeRouteLocator") public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) { return new RedisCacheRouteLocator(new CompositeRouteLocator(Flux.fromIterable(routeLocators))); } /** * 替代原有的 route definition repository * * @param reactiveRedisTemplate * @return */ @Bean public RedisExpireRouteDefinitionRepository redisRouteDefinitionRepository( ReactiveRedisTemplate<String, RouteDefinition> reactiveRedisTemplate) { return new RedisExpireRouteDefinitionRepository(reactiveRedisTemplate, this.rainbowCloudGatewayProperties.getRedisRouteProperties().getTimeout()); }} 对应属性类: 1234567891011121314151617181920212223242526@ConfigurationProperties(prefix = "rainbow.cloud.gateway")public class RainbowCloudGatewayProperties { private RedisRouteProperties redisRouteProperties = new RedisRouteProperties(); public RedisRouteProperties getRedisRouteProperties() { return redisRouteProperties; } public void setRedisRouteProperties(RedisRouteProperties redisRouteProperties) { this.redisRouteProperties = redisRouteProperties; } public static class RedisRouteProperties { private Duration timeout = Duration.ofSeconds(90); public Duration getTimeout() { return timeout; } public void setTimeout(Duration timeout) { this.timeout = timeout; } }} 对应 route locator 类 12345678910111213141516171819202122232425262728293031323334public class RedisCacheRouteLocator implements Ordered, RouteLocator, ApplicationListener<RefreshRoutesEvent>, ApplicationEventPublisherAware { private static final Logger logger = LoggerFactory.getLogger(RedisCacheRouteLocator.class); private RouteLocator delegate; public RedisCacheRouteLocator(RouteLocator delegate) { this.delegate = delegate; } @Override public Flux<Route> getRoutes() { return fetch(); } private Flux<Route> fetch() { logger.debug("开始获取路由信息"); return this.delegate.getRoutes().sort(AnnotationAwareOrderComparator.INSTANCE); } @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { } @Override public void onApplicationEvent(RefreshRoutesEvent event) { } @Override public int getOrder() { return 0; }} redis route repository 实现 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192public class RedisExpireRouteDefinitionRepository implements RouteDefinitionRepository { private static final Logger log = LoggerFactory.getLogger(RedisExpireRouteDefinitionRepository.class); /** * Key prefix for RouteDefinition queries to redis. */ private static final String ROUTEDEFINITION_REDIS_KEY_PREFIX_QUERY = "routedefinition_"; private Duration timeout; private ReactiveRedisTemplate<String, RouteDefinition> reactiveRedisTemplate; private ReactiveValueOperations<String, RouteDefinition> routeDefinitionReactiveValueOperations; public RedisExpireRouteDefinitionRepository(ReactiveRedisTemplate<String, RouteDefinition> reactiveRedisTemplate, Duration timeout) { this.reactiveRedisTemplate = reactiveRedisTemplate; this.routeDefinitionReactiveValueOperations = reactiveRedisTemplate.opsForValue(); this.timeout = timeout; } @Override public Flux<RouteDefinition> getRouteDefinitions() { log.debug("开始从 redis 查询路由信息"); return reactiveRedisTemplate.keys(createKey("*")).flatMap(key -> reactiveRedisTemplate.opsForValue().get(key)) .onErrorContinue((throwable, routeDefinition) -> { if (log.isErrorEnabled()) { log.error("get routes from redis error cause : {}", throwable.toString(), throwable); } }); } @Override public Mono<Void> save(Mono<RouteDefinition> route) { log.debug("开始保存路由信息到 redis "); return route.flatMap(routeDefinition -> { String key = createKey(routeDefinition.getId()); String toSaveUri = routeDefinition.getUri().toString(); return this.reactiveRedisTemplate.hasKey(key).flatMap(isExist -> { if (isExist) { // 存在相同 id 的路由 return this.routeDefinitionReactiveValueOperations.get(key).flatMap(existDefinition -> { String existUri = existDefinition.getUri().toString(); if (!existUri.contains(toSaveUri)) { // 更新数据 String toUpdateUri = String.join(",", existUri, toSaveUri); existDefinition.setUri(URI.create(toUpdateUri)); } // 续期 return routeDefinitionReactiveValueOperations.set(key, existDefinition, this.timeout); }); } else { // 添加数据 return routeDefinitionReactiveValueOperations.set(key, routeDefinition, this.timeout); } }).flatMap(success -> { if (success) { return Mono.empty(); } return Mono.defer(() -> Mono.error(new RuntimeException( String.format("Could not add route to redis repository: %s", routeDefinition)))); });// return routeDefinitionReactiveValueOperations// .set(key, routeDefinition, this.timeout)// .flatMap(success -> {// if (success) {// return Mono.empty();// }// return Mono.defer(() -> Mono.error(new RuntimeException(// String.format("Could not add route to redis repository: %s", routeDefinition))));// }); }); } @Override public Mono<Void> delete(Mono<String> routeId) { return routeId.flatMap(id -> routeDefinitionReactiveValueOperations.delete(createKey(id)).flatMap(success -> { if (success) { return Mono.empty(); } return Mono.defer(() -> Mono.error(new NotFoundException( String.format("Could not remove route from redis repository with id: %s", routeId)))); })); } private String createKey(String routeId) { return ROUTEDEFINITION_REDIS_KEY_PREFIX_QUERY + routeId; }} 到此,已经完成对 gateway 端的改造。后端服务可以通过定时向 api 发送请求,添加路由,维持心跳。 接下来就是再对后端服务进行改造,让后端服务启动的时候发送心跳请求。但是这里有个问题,那就是如果后端服务以容器方式部署了之后,怎么计算服务的 ip 和端口,因为这两个信息将作为参数在调用网关 api 的时候传递过去 笔者的解决方式为:使用 docker-compose 部署服务容器,在部署的时候,通过编写一个 shell 脚本,将主机的 ip 计算出来,写入到 docker-compose 的 env 文件中。而暴露的端口可以通过 docker-compose 模板的方式进行配置 当服务启动的时候,检查环境变量里面是否包含这两个环境变量,如果有,则认为是在 docker 环境,反之则获取本机的第一个非回环网卡的 ip 和端口,然后进行服务注册。 改造完成之后,其实发现,这有点类似一个小型的注册中心的逻辑,只是由于使用 redis 来存储数据,使得这个注册中心的实现就很轻量了,并且也有不错的性能。 2.2 多实例的负载均衡这个问题,笔者通过实现了一个过滤器完成了这个功能。 读者可以理解为,uri 只需要这样配置,也可以实现负载均衡。 12345678910spring: cloud: gateway: routes: - id: myRoute uri: http://localhost:9093/**,http://localhost:9094/** predicates: - Path=/api/model-service/** filters: - StripPrefix=2 多实例过滤器类 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152/** * 多实例处理过滤器,配置可以将多个 uri 以逗号分隔的方式进行配置 */@Componentpublic class ReactiveMultipleInstanceFilter implements GlobalFilter, Ordered { private static final Logger logger = LoggerFactory.getLogger(ReactiveMultipleInstanceFilter.class); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); String uri = route.getUri().toString(); if (uri.contains(",")) { logger.info("multiple instance filter handle"); // 多实例 String[] split = uri.split(","); String processServer = loadBalance(split); exchange.getAttributes().computeIfPresent(GATEWAY_ROUTE_ATTR, (s,v) -> { Route v1 = (Route) v; try { // 分隔之后,强制使用反射更改为 spring cloud gateway 支持的单个实例模式 Field field = v1.getClass().getDeclaredField("uri"); field.setAccessible(true); field.set(v1, URI.create(processServer)); return v1; } catch (Exception e) { return v1; } }); } return chain.filter(exchange); } /** * 负载均衡算法 * * @param split * @return */ private String loadBalance(String[] split) { int length = split.length; // 随机选取,因为 gateway 是能够感知到每个服务的情况的,所以获取到的时候,服务肯定在线 int position = RandomUtil.randomInt(0, length + 1); return split[position]; } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }}","link":"/2022/08/04/%E8%87%AA%E5%8A%A8%E6%B3%A8%E5%86%8C%E6%9C%8D%E5%8A%A1%E8%B7%AF%E7%94%B1%E7%9A%84%E5%AE%9E%E7%8E%B0/"},{"title":"如何使用 keycloak SAT 调用 rest api","text":"1 概述keycloak SAT 是指用 keycloak service account 登录获得的 token。通过 keycloak SAT 我们就可以直接调用 keycloak 的 api,实现查询用户、新增用户等功能。 具体实现分两步: 先创建 SA (service account) 用户 使用 SA 用户名和密码进行登录 2 创建 keycloak SASA 实际上也是 client,但这个 client 的类型必须是 confidential,这样才能开启 SA 功能。 因此,要使用 SA 功能,需要先创建一个 client。 创建好了之后,设置 client 信息 点击保存之后,会新增一个 service account role选项 把所有 client 的 roles 全部添加到这个 client 上。这样做,是给这个 client 的 sa 添加权限,这样才能有权限调 api。 3 使用以 postman 调用为例 获取 SAT: 根据 SAT 查询所有用户: 创建用户并指定密码: 请求参数体: 123456789101112131415161718192021222324252627{ "createdTimestamp": 1588880747548, "username": "Strange", "enabled": true, "totp": false, "credentials": [ { "type": "password", "value": "test123", "temporary": false } ], "emailVerified": true, "firstName": "Stephen", "lastName": "Strange", "email": "[email protected]", "disableableCredentialTypes": [], "requiredActions": [], "notBefore": 0, "access": { "manageGroupMembership": true, "view": true, "mapRoles": true, "impersonate": true, "manage": true }}","link":"/2022/08/10/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8-keycloak-SAT-%E8%B0%83%E7%94%A8-rest-api/"},{"title":"keycloak 整合微信 union id,实现微信统一登录","text":"1 概述微信的第三方登录最开始是做的比较烂的。 首先,微信有多种登录方式: 登录方式 管理后台 管理帐号 是否符合OAuth标准 App(Android/iOS) 开放平台-移动应用 帐号1 符合OAuth 网站扫码 开放平台-网站 帐号1 符合OAuth 微信App内嵌打开链接 公众平台-服务号 帐号2 符合OAuth 小程序 公众平台-小程序 帐号3 不符合OAuth 管理后台管理的资源也不一样: 微信开放平台(https://open.weixin.qq.com/) 移动应用 网站 微信公众平台(https://mp.weixin.qq.com/) 服务号(Service Account) 订阅号(Subscription Account) 小程序(Mini Program) 那么也就导致了小程序登录获得的 open id 与网站扫码获得的 open id 不是同一个,这也就会让开发者把这当成两个不同账号处理。 不过经过这几年的发展,到目前为主,虽然微信相关登录做的还是比较烂,但至少识别同一个用户这个问题已经解决了。 因为微信引入了新的 union id 机制。union id 就是说,管理者可以在微信开放平台上将上面提到的多个应用绑定在一起,绑定了之后用户再登录,开发者就能拿到一个 union id。这个 union id,不管用户是用小程序还是网站扫码登录,结果都是一样的。 以上是微信相关登录的背景。 回归到项目背景。 笔者在公司项目设计时,使用 keycloak 作为统一认证中心,但前端又有小程序和网页端两种表现形式。 网页端使用 oauth2.0 协议进行第三方登录,于是笔者开发了微信的第三方登录插件,成功解决。但小程序登录,并不符合 oauth2.0 规范,前端拿到的只是一个 oauth2 的授权码,相当于前端需要用授权码来完成在 keycloak 上的认证。 这也就是本文要解决的问题。 2 思路首先,笔者微信网站扫码登录的第三方插件是将 union id 作为 keycloak 的用户名(之所以要用 union id是因为这个值每个用户微信,微信用户的用户名可以相同,而keycloak 要求用户名不能相同)。 如果笔者拿到小程序的授权码,直接去 keycloak 认证肯定是无法支持的,因为这是非标准的协议流程。 那有没有其他办法呢? 翻阅 keycloak 文档,它可以自定义 SPI,自定义 endpoint等。 笔者曾想过给 keycloak 自定义一个 endpoint,让服务端调用这个 endpoint,完成 获取 union id=》创建用户 =》用户认证的逻辑。但后面发现里面逻辑太复杂,于是只有放弃。 但是这也让我想明白了,用 code 通过 keycloak 认证的方式。那就是需要先解析得到 union id,再用 union id 创建 keycloak 用户,然后再用账号密码方式进行认证。 因为刚才说了,第三方插件的效果实际也就是 keycloak 自动为其创建用户,那小程序登录也只需要完成这个逻辑就可以了。 3 自动创建用户具体实现最后发现了 keycloak 可以支持 SAT 对 rest api 的调用。并且,keycloak 提供的 api 能够满足我们需要实现的功能。 首先,编写一个 keycloak rest api 的操作类: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243package top.rainbowecho.web.servlet.client.keycloak;import cn.hutool.core.collection.CollectionUtil;import cn.hutool.http.HttpRequest;import cn.hutool.http.HttpResponse;import cn.hutool.http.HttpUtil;import com.alibaba.fastjson.JSONArray;import com.alibaba.fastjson.JSONObject;import com.google.common.collect.ImmutableMap;import com.google.common.reflect.TypeToken;import com.google.gson.Gson;import okhttp3.*;import org.apache.commons.lang3.StringUtils;import org.keycloak.representations.idm.CredentialRepresentation;import top.rainbowecho.web.servlet.base.component.MiniProgramOperatorType;import top.rainbowecho.web.servlet.handle.AppException;import java.io.IOException;import java.util.List;import java.util.Set;/** * 调用 keycloak rest api 的对象 * * @author rainbow */public class KeycloakHttpClient { private String keycloakAuthUrl; private String realm; private String clientId; private String clientSecret; private OkHttpClient httpClient; private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); public String getKeycloakAuthUrl() { return keycloakAuthUrl; } public String getRealm() { return realm; } public String getClientId() { return clientId; } public String getClientSecret() { return clientSecret; } public KeycloakHttpClient(String keycloakAuthUrl, String realm, String clientId, String clientSecret) { this.keycloakAuthUrl = keycloakAuthUrl; this.realm = realm; this.clientId = clientId; this.clientSecret = clientSecret; this.httpClient = new OkHttpClient().newBuilder() .addInterceptor(new KeycloakSatInterceptor(this)) .build(); } /** * 通过指定参数创建用户 * * @param username * @param password * @param type * @return 成功返回 true,否则返回 false */ public boolean createUser(String username, String password, MiniProgramOperatorType type) { String url = String.format("%s/admin/realms/%s/users", this.keycloakAuthUrl, this.realm); // 避免邮箱重复 String email = username + "@example.com"; KeycloakUserParam userParam = buildCreateUserParam(username, password, type, email); String json = new Gson().toJson(userParam); RequestBody body = RequestBody.create(json, JSON); Request request = new Request.Builder().url(url).post(body).build(); try ( Response response = this.httpClient.newCall(request).execute(); ) { int code = response.code(); if (code == 201) { return true; } else { throw new AppException(String.format("code: %s, msg: %s", code, response.body())); } } catch (IOException e) { throw new AppException(e.getMessage()); } } /** * 根据用户名查找用户 * * @param username * @return 返回 true 代表存在该用户 */ public boolean existUser(String username) { KeycloakUserParam userParam = userInfo(username); return userParam != null; } /** * kc 用户是否设置了密码 * * @param username * @return */ public boolean havePasswd(String username) { KeycloakUserParam kcUser = userInfo(username); String kcUserId = kcUser.getId(); String url = String.format("%s/admin/realms/%s/users/%s/credentials", this.keycloakAuthUrl, this.realm, kcUserId); Request request = new Request.Builder().url(url).get().build(); try ( Response response = this.httpClient.newCall(request).execute() ) { ResponseBody body = response.body(); String jsonRes = body.string(); if (response.isSuccessful()) { JSONArray rootArr = JSONArray.parseArray(jsonRes); long count = rootArr.stream().filter(e -> { JSONObject node = (JSONObject) e; String type = node.getString("type"); return StringUtils.equals(type, "password"); }).count(); return count > 0; } else { throw new AppException(jsonRes); } } catch (IOException e) { throw new RuntimeException(e); } } public boolean resetUserPasswd(String username) { KeycloakUserParam keycloakUser = userInfo(username); String kcUserId = keycloakUser.getId(); String url = String.format("%s/admin/realms/%s/users/%s/reset-password", this.keycloakAuthUrl, this.realm, kcUserId); ImmutableMap<String, Object> map = ImmutableMap.of("type", "password", "value", username, "temporary", false); String json = new Gson().toJson(map); RequestBody body = RequestBody.create(json, JSON); Request request = new Request.Builder().url(url).put(body).build(); try ( Response response = this.httpClient.newCall(request).execute() ) { if (response.isSuccessful()) { return true; } else { throw new AppException(response.body().string()); } } catch (IOException e) { throw new RuntimeException(e); } } public KeycloakUserParam userInfo(String username) { // 严格匹配,如果不加 exact,则是模糊匹配 String url = String.format("%s/admin/realms/%s/users?exact=true&username=%s", this.keycloakAuthUrl, this.realm, username); Request request = new Request.Builder().url(url).build(); try ( Response response = this.httpClient.newCall(request).execute() ) { if (response.isSuccessful()) { ResponseBody body = response.body(); String json = body.string(); Gson gson = new Gson(); List<KeycloakUserParam> arr = gson.fromJson(json, new TypeToken<List<KeycloakUserParam>>() { }.getType()); if (CollectionUtil.isEmpty(arr)) { return null; } else { return arr.get(0); } } else { throw new AppException(response.body().string()); } } catch (IOException e) { throw new AppException(e.getMessage()); } } /** * 用户登录,不需要 sat * * @param username * @param password * @return 登录返回的响应体 */ public String userLogin(String username, String password) { String url = String.format("%s/realms/%s/protocol/openid-connect/token", this.keycloakAuthUrl, this.realm); HttpRequest request = HttpUtil.createPost(url).form("client_id", this.clientId , "client_secret", this.clientSecret , "grant_type", "password" , "username", username , "password", password); try ( HttpResponse response = request.execute(); ) { if (response.isOk()) { return response.body(); } else { throw new AppException(response.body()); } } } private KeycloakUserParam buildCreateUserParam(String username, String password, MiniProgramOperatorType type, String email) { KeycloakUserParam userParam = new KeycloakUserParam(); userParam.setCreatedTimestamp(System.currentTimeMillis()); userParam.setUsername(username); userParam.setEnabled(true); userParam.setTotp(false); CredentialRepresentation credentialRepresentation = new CredentialRepresentation(); credentialRepresentation.setType("password"); credentialRepresentation.setValue(password); credentialRepresentation.setTemporary(false); userParam.setCredentials(List.of(credentialRepresentation)); userParam.setAttributes(ImmutableMap.of("type", List.of(type.name()))); userParam.setEmailVerified(true); userParam.setEmail(email); userParam.setDisableableCredentialTypes(Set.of()); userParam.setRequiredActions(List.of()); userParam.setNotBefore(0); userParam.setAccess(ImmutableMap.of("manageGroupMembership", true, "view", true, "mapRoles", true, "impersonate", true, "manage", true)); return userParam; }} keycloak sat 拦截器: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768package top.rainbowecho.web.servlet.client.keycloak;import cn.hutool.core.util.StrUtil;import cn.hutool.http.HttpRequest;import cn.hutool.http.HttpResponse;import cn.hutool.http.HttpUtil;import com.alibaba.fastjson.JSONObject;import okhttp3.Interceptor;import okhttp3.Request;import okhttp3.Response;import org.apache.commons.lang3.StringUtils;import org.jetbrains.annotations.NotNull;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.http.HttpHeaders;import java.io.IOException;import java.nio.charset.StandardCharsets;/** * keycloak service account token 拦截器 * * @author yangjian */public class KeycloakSatInterceptor implements Interceptor { private static final Logger log = LoggerFactory.getLogger(KeycloakSatInterceptor.class); private KeycloakHttpClient keycloakHttpClient; public KeycloakSatInterceptor(KeycloakHttpClient keycloakHttpClient) { this.keycloakHttpClient = keycloakHttpClient; } @NotNull @Override public Response intercept(@NotNull Chain chain) throws IOException { Request originalRequest = chain.request(); String token = originalRequest.header(HttpHeaders.AUTHORIZATION); if (StrUtil.isBlank(token)) { log.debug("begin get keycloak sat"); // 获取 token String satToken = obtainSat(); Request updateRequest = originalRequest.newBuilder().header(HttpHeaders.AUTHORIZATION, "Bearer " + satToken).build(); return chain.proceed(updateRequest); } else { return chain.proceed(originalRequest); } } private String obtainSat() { String auth = HttpUtil.buildBasicAuth(this.keycloakHttpClient.getClientId(), this.keycloakHttpClient.getClientSecret(), StandardCharsets.UTF_8); String url = this.keycloakHttpClient.getKeycloakAuthUrl() + String.format("/realms/%s/protocol/openid-connect/token", this.keycloakHttpClient.getRealm()); HttpRequest request = HttpUtil.createPost(url).form("grant_type", "client_credentials").header(HttpHeaders.AUTHORIZATION, auth); try ( HttpResponse response = request.execute() ) { String body = response.body(); if (response.isOk() && StrUtil.isNotBlank(body)) { JSONObject rootNode = JSONObject.parseObject(body); // 如果没获取到,那么也直接给出去,这样客户端会直接调用不了 api return rootNode.getString("access_token"); } else { return StringUtils.EMPTY; } } }} 微信小程序授权码转换 union id: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051package top.rainbowecho.web.servlet.base.component;import cn.hutool.core.util.StrUtil;import cn.hutool.http.HttpRequest;import cn.hutool.http.HttpResponse;import cn.hutool.http.HttpUtil;import com.alibaba.fastjson.JSONObject;import org.springframework.stereotype.Component;import top.rainbowecho.web.servlet.base.prop.CloudBaseProperties;import top.rainbowecho.web.servlet.handle.AppException;@Componentpublic class WeChatMiniProgramOperator implements MiniProgramOperator { private static final String url = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"; private CloudBaseProperties cloudBaseProperties; public WeChatMiniProgramOperator(CloudBaseProperties cloudBaseProperties) { this.cloudBaseProperties = cloudBaseProperties; } @Override public String unionId(String code) { CloudBaseProperties.MiniProgramProperties wechat = this.cloudBaseProperties.getWechat(); String appId = wechat.getAppId(); String appSecret = wechat.getAppSecret(); String requestUrl = String.format(url, appId, appSecret, code); HttpRequest request = HttpUtil.createGet(requestUrl); try ( HttpResponse response = request.execute(); ) { if (response.isOk()) { String json = response.body(); JSONObject rootNode = JSONObject.parseObject(json); String unionid = rootNode.getString("unionid"); String openid = rootNode.getString("openid"); return StrUtil.isBlank(unionid) ? openid : unionid; } else { throw new AppException("wechat mini code login error"); } } } @Override public MiniProgramOperatorType type() { return MiniProgramOperatorType.wechat; }} 实现了这几个类之后,只需要再定义一个针对小程序授权码的 spring security 拦截器即可,以定义一个 authentication manager 为例 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162package top.rainbowecho.web.servlet.security.filters.code;import org.apache.commons.lang3.StringUtils;import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.authentication.AuthenticationServiceException;import org.springframework.security.authentication.BadCredentialsException;import org.springframework.security.core.Authentication;import org.springframework.security.core.AuthenticationException;import org.springframework.security.core.authority.AuthorityUtils;import top.rainbowecho.web.servlet.base.component.MiniProgramOperator;import top.rainbowecho.web.servlet.client.keycloak.KeycloakHttpClient;public class MiniProgramCodeAuthenticationManager implements AuthenticationManager { private MiniProgramOperator miniProgramOperator; private KeycloakHttpClient keycloakHttpClient; public void setKeycloakHttpClient(KeycloakHttpClient keycloakHttpClient) { this.keycloakHttpClient = keycloakHttpClient; } public void setMiniProgramOperator(MiniProgramOperator miniProgramOperator) { this.miniProgramOperator = miniProgramOperator; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { MiniProgramCodeAuthenticationToken toAuthToken = new MiniProgramCodeAuthenticationToken((String) authentication.getPrincipal(), (String) authentication.getCredentials()); toAuthToken.setDetails(authentication.getDetails()); String principal = (String) toAuthToken.getPrincipal(); if (StringUtils.isBlank(principal)) { throw new BadCredentialsException("blank code principal"); } String unionId = this.miniProgramOperator.unionId(principal); String password = unionId; if (this.keycloakHttpClient.existUser(unionId)) { if (!this.keycloakHttpClient.havePasswd(unionId)) { this.keycloakHttpClient.resetUserPasswd(unionId); } try { this.keycloakHttpClient.userLogin(unionId, password); return new MiniProgramCodeAuthenticationToken(AuthorityUtils.NO_AUTHORITIES, unionId, "[PROTECTED]"); } catch (Exception e) { throw new AuthenticationCredentialsNotFoundException(e.getMessage()); } } else { try { this.keycloakHttpClient.createUser(unionId, password, this.miniProgramOperator.type()); this.keycloakHttpClient.userLogin(unionId, password); return new MiniProgramCodeAuthenticationToken(AuthorityUtils.NO_AUTHORITIES, unionId, "[PROTECTED]"); } catch (Exception e) { throw new AuthenticationServiceException("auto register code user failed: " + e.getMessage()); } } }} 以上代码完整地址,可以在:https://github.com/rainbowechoes/rainbow-cloud-dependencies.git 找到 4 微信第三方登录插件修改通过上一步,已经能够实现通过 code 获得 unionId,并将 unionId 作为用户名进行自动注册。 虽然笔者已经实现了一个微信扫码登录的插件,但是仅靠这个插件还是不够的。因为,keycloak 使用这个插件只是完成第三方登录的认证,在第一次认证通过时,默认有一个 Authentication flow,也就是认证流程。 这个认证流程目的在于如果检测到存在相同用户名的用户,会要求进行用户通过邮件或者密码进行绑定。但我们自动注册的用户,密码是默认的,邮箱是假的,根本不能走下这个流程。 也就是说,虽然用户已经成功创建了,但用户却不能扫码登录的,这肯定是不能接受的。 解决方式就是: 实现一个流程插件 定义一个新的认证流程,新的流程使用新的插件去进行操作 这个插件的逻辑应该是:用户存在的话,则将扫码登录能够拿到的用户信息更新到用户属性中;如果用户不存在,则根据拿到的用户信息存到属性中。 微信扫码登录插件与流程插件源码都在此项目中:https://github.com/rainbowechoes/keycloak-social-wechat.git 如果在 keycloak 中成功添加了流程插件后,在 server info 中将能看到此插件,如下图: 完成了流程插件的引入之后,就需要调整微信扫码插件的认证流程,操作如下: 新建认证流程 idp 中绑定流程 5 总结要实现微信体系下的统一登录,要实现很多东西: keycloak 的微信扫码插件 微信小程序的用户自动注册 keycloak 认证流程插件 也需要配置一些东西: keycloak idp 认证流程 keycloak 外部 idp 集成 完成了这些之后,用户不管是先通过小程序注册还是先通过微信扫码登录,都能自动追踪到同一个用户 6 引用文章:https://sinkcup.github.io/wechat-login-union-id 代码: https://github.com/rainbowechoes/keycloak-social-wechat.git https://github.com/rainbowechoes/rainbow-cloud-dependencies.git","link":"/2022/08/12/keycloak-%E6%95%B4%E5%90%88%E5%BE%AE%E4%BF%A1-union-id%EF%BC%8C%E5%AE%9E%E7%8E%B0%E5%BE%AE%E4%BF%A1%E7%BB%9F%E4%B8%80%E7%99%BB%E5%BD%95/"},{"title":"java8新特性及多线程总结","text":"1. 概述对于软件开发人员而言,在初次接触多线程编程以及java8中的stream时,估计可能会被其中的CPU、内核、线程等概念问题所困扰。 但其实理清了思路后,这些概念很简单。 要理解这些概念,还需要首先从CPU的发展历史说起。 需要声明的是,本文并不会对新特性及多线程中如何使用进行过多的讲解,因为有足够多的文章以及书籍进行讲解。笔者所做的只是对其中涉及到的一些关键概念以及对新特性的理解进行说明。 2. CPU的发展2.1 阶段CPU的含义在发展的过程中经历了许多语义上的变化,因此在这里首先对本文说明的CPU下一个定义。CPU(中央处理单元)实际上就是一个计算模块,作用就是执行汇编指令,进行计算。它的发展分为以下几个阶段: 第一阶段:在计算机最开始时,一台计算机只有一个CPU,因此,就存在不能同时执行多个任务(也就是无法并行,只能并发)的问题。一个CPU中都具有数据总线、地址总线以及与内存适配速度的cache。 第二阶段:为了能进行并行处理,那么很自然地能够想到可以通过添加多个计算模块进行解决。这就存在两个添加的方案:第一个方案是添加多个计算模块,这多个计算模块属于同级关系;另一个方案是一个计算模块中再添加多个计算模块。这两种方式有什么区别呢?区别就在于第一个方案分别使用不同的总线、不同的cache,因此存在数据不一致问题;第二个方案共用总线,共用cache,由内部协调。而第二个方案中添加的多个计算模块就是核心(core)。这其实是为了区别于CPU的称谓,两者可以视为等同。因为,此时的core与第一阶段中的CPU作用仍然相同,还是处理汇编指令。两种方案最后的名字分别叫做:多CPU单核心和单CPU多核心。 第三阶段:为了能使单CPU多核心中的每个核心再进一步地提升性能,英特尔和AMD都实现了超线程的技术。这种技术提升性能的机制在于通过将一个物理上的core模拟成逻辑上的多个线程(processor),每个processor同样能够实现计算模块的功能,也就是所谓几核几线程。细心的读者可能注意到,这里对processor的翻译与thread的翻译相同了,都是线程。这其实是翻译的一个误区,很多地方其实都是这样翻译的,而也正是因为这样的翻译错误,导致作为软件开发人员在接触一些编程概念时,容易产生混淆。后文中都会将processor翻译为处理器, 将thread翻译为线程。 参考下图: 至此,我们再进行一下CPU,内核,处理器(processor),这三个概念。 CPU代表的是一个或多个核心的集合,每个CPU具有独立的总线,cache。 核心指的是单纯进行计算的模块,能够独立完成汇编指令,包含在CPU中。 处理器是通过超线程技术对一个核心模拟出的计算模块。 内核(kernel)则是软件层对硬件层的第一层抽象,如Linux内核,向用户提供一些调度计算机的接口。 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。在如今的程序设计中,作用仅是使用PCB块进行资源(CPU、内存)的分配和调度。 线程(thread)是操作系统能够进行运算调度的最小单位。用于利用进程分配的资源进行程序的执行。 2.2 查看本机CPU信息推荐使用命令行的方式,Windows下: 在进行多线程编程时,如果设置了多个线程,那么这些线程将会由操作系统进行分配到不同的处理器中进行执行。比如4核8处理器的计算机,运行了一段设置了8个线程的程序,操作系统此时的分配可能会是每个处理器分配一个线程。分配的原则很明显,是尽量快的响应与处理。 2.3 计算机不同架构分析前面提及了一下如今计算机的架构的两种方案:单CPU多核心和多CPU单核心。 单CPU多核心 优点:成本容易得到控制,也会有比较好的性能,架构也比较简单清晰 缺点:如果在这样的计算机上运行多线程程序,那么由于多个核心之前会共用一套总线和cache,随着线程之间的通信,总线的压力就会比较大,且数据在cache会存在,更何况现在一般都是三级cache,数据一致性就很难保证 多CPU单核心 优点:可以适用于大规模的计算 缺点:I/O瓶颈大 所以,根据两种方案的不同,单CPU多核心通常用于普通消费市场,作为个人电脑使用。多CPU单核心常见于分布式系统、云计算平台、超级计算机等。 对于程序开发人员而言,以上的基础概念理清之后,对于Java8中新特性以及多线程中的理解就会更容易一些了。 3. Java8新特性3.1 Java8中重要的编程概念在了解Java8新特性及使用之前,首先需要了解在Java8中涉及到的三个重要的编程概念。这三个编程概念掌握的好坏会直接新特性使用。 这三个编程概念如下: 行为参数化 方法引用 lambda表达式 流计算 不在并行中使用共享的可变数据 行为参数化的意思是,将代码作为参数传递给某个方法执行。这在Java8之前的Java语言中,是无法实现的。 流计算的思想在于,抛弃了原本使用集合形式进行数据的计算。因为使用Java中的集合,你首先需要是集合中填充满所有你需要的元素,而这些元素都是直接存储到内存中的。这似乎没有什么问题,但假如数据量足够大的时候,你又如何处理呢? 读者可能会问,为何要一定要使用行为参数化和流计算,我之前使用Java的时候没有行为参数化也能使用的很好啊,也能完成我想要实现的目的。 的确,使用行为参数化并不会让你的代码具有新的功能。但是它能带来的是更好的灵活性和可读性,让你对功能的实现变得简便、高效。这难道不具有吸引力吗? 话说至此,尽快开始。 3.2 行为参数化行为参数化的实现,也就是传递代码作为参数,有两种方式: lambda表达式 方法引用。方法引用包括,普通方法的引用以及构造函数的引用 3.2.1 行为参数化的条件两种方式要实现都需要满足一定的条件。 要使用lambda表达式,一定需要一个函数式接口。 要使用方法引用,则要求lambda表达式只有一行。因为方法引用本质上是对lambda表达式的简化,所以方法引用还需要满足能使用lambda表达式的条件。 3.2.2 如何lambda化如果要对某个方法中的某段代码进行lambda化,那么下列过程将会指导你如何完成lambda化的抽取: 明确方法中可变的代码部分与不变代码部分,可变的代码部分应尽量集中 明确变化代码部分的返回值的类型与参数,返回值类型和参数的确认可以通过抽离后观察得出 定义一个相同返回值的函数接口 将该接口作为该方法的参数,并在代码抽取的部分调用该接口 将可变代码传递进该方法 Java8中提供了许多函数接口使用,有以下几类,如: 函数式接口 函数描述符 说明 Predicate<T> T -> boolean 断言 Consumer<T> T -> void 消费者 Function<T, R> T -> R 转换 Supplier<T> void -> T 生产者 UnaryOperator<T> T -> T 单元操作 BinarayOperator<T> (T, T) -> T 双元操作 BiPredicate<L, R> (L, R) -> boolean 双元断言 BiConsumer<T, U> (T, U) -> void 双元消费者 BiFunction<T, U, R> (T, U) -> R 双元转换 需要说明的是,每类函数接口都有针对于基本类型的实现,目的是为了避免自动装箱、拆箱。同时,也建议如果要对基本类型进行操作,最好选择对应基本类型函数接口,这样效率更高。但是对于Optional<T>,最好不要用基本类型的Optional。 3.2.3 类型检查、类型推断以及限制lambda表达式的类型检查过程十分重要,包含三个上下文:赋值上下文(找到对应的函数接口),方法调用上下文(找到函数接口中执行的方法),类型转换上下文(明确方法签名是否吻合),如下: lambda表达式的限制在于使用的局部变量必须是final的。因为lambda表达式会透明地使用多线程。 3.2.4 lambda表达式的应用场景在以下两种情况下,最适合使用lambda表达式: 延迟执行:当某段代码作为方法的参数,需要被延迟执行时 环绕执行:当某段代码被一系列模板代码包围时,可以将这部分代码抽取为lambda表达式,如文件操作 3.3 方法引用方法引用的目的在于简化lambda表达式,通过让方法名称代替lambda表达式,从而具有更好的可读性。 需要再次强调的是,方法引用只适用于一行lambda表达式的情况,如: lambda表达式 等效的方法引用 (Apple a) -> a.getWeight() Apple::getWeight (String s) -> System.out.println(s) System.out::println () -> new Apple() Apple::new 3.4 流计算流与集合区别在于,集合讲的是数据,流讲的是计算。流是惰性计算的,是按需生成的,是一次性的,对用户而言具有透明地并行、内部迭代的优点。 流的使用包括三件事: 根据一个数据源生成流 一个中间操作链,形成一条流的流水线 一个终端操作,执行流水线,并生成结果 操作分析如下: 操作 类型 返回类型 使用的类型/函数式接口 函数描述符 filter 中间 Stream<T> Predicate<T> T -> boolean distinct 中间(有状态-无界) Stream<T> skip 中间(有状态-有界) Stream<T> long limit 中间(有状态-有界) Stream<T> long map 中间 Stream<R> Function<T, R> T -> R flatMap 中间 Stream<R> Function<T, Stream<R>> T -> Stream<R> sorted 中间(有状态-无界) Stream<T> Comparator<T> (T, T) -> int anyMatch 终端 boolean Predicate<T> T -> boolean allMatch 终端 boolean Predicate<T> T -> boolean noneMatch 终端 boolean Predicate<T> T -> boolean findAny 终端 Optional<T> findFirst 终端 Optional<T> forEach 终端 void Consumer<T> T -> void collect 终端 R Collector<T, A, R> reduce 终端(有状态-有界) Optional<T> BinaryOperator<T> (T, T) -> T count 终端 long 无状态:输出流得到0个或1个结果 有状态:输出流有多个结果 有状态-有界:输出流有多个结果,但结果有限 有状态-无界:输出流有多个结果,且结果无限 3.4.1 具体例子flatMap方法: 将元素为List的元素进行合并 1234567891011121314151617// 类型为List的元素stream合并Person zhangsan = new Person("zhangsan", 22);Person lisi = new Person("lisi", 22);Person wangwu = new Person("wangwu", 22);ArrayList<Person> people1 = new ArrayList<>();people1.add(zhangsan);people1.add(lisi);ArrayList<Person> people2 = new ArrayList<>();people2.add(wangwu);ArrayList<List<Person>> lists = new ArrayList<>();lists.add(people1);lists.add(people2);List<Person> collect = lists.stream().flatMap(Collection::stream).collect(Collectors.toList());System.out.println("collect = " + collect); // collect = [Person{name='zhangsan', age=22}, Person{name='lisi', age=22}, Person{name='wangwu', age=22}] 3.5 Optional类Optional类在语义可以明确的告知开发人员,这个字段可能会缺失值,提高可读性。 在维护性上,Optional类可以消除因为null而引起的if连锁语句,还可以取代异常抛出无法返回值的情况。 3.5.1 重要的方法Optional类中有两个非常重要的方法:map和flatMap map:从Optional类中获取原始类型值 flatMap:从Optional类中获取Optional类 3.5.2 注意使用Optional,需要特别注意两点: Optional类型的字段无法序列化。但是可以通过手动提供一个返回Optional类的getter方法实现相同效果 不建议使用基础类型的Optional包装类。因为,无法使用map等方法。 3.6 构建异步APIJava8中可以通过两种方式构建异步的API,两种都是基于callable和future接口: 并行流 使用CompletableFuture发起异步请求 两种方案各有优缺点,适用场景为: 计算密集型,没有I/O,使用并行流 涉及I/O(网络I/O,文件I/O),使用CompletableFuture,灵活性更好 123456789101112131415161718192021222324252627282930313233343536373839404142ArrayList<String> urls = new ArrayList<>(4);for (int i = 0; i < 100; i++) { urls.add("http://www.baidu.com");}int threads = (int) (Runtime.getRuntime().availableProcessors() * 0.8 * 100);System.out.println("threads = " + threads);// 自定义线程数,充分利用资源Executor executorService = Executors.newFixedThreadPool(Math.min(urls.size(), threads), new ThreadFactory() { private AtomicInteger threadNum = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setDaemon(true); thread.setPriority(5); thread.setName("Thread Monitor " + threadNum.incrementAndGet()); return thread; }});// 分为两个流操作进行调用,避免阻塞List<CompletableFuture<String>> collect = urls.stream().map(url -> CompletableFuture.supplyAsync(() -> { HttpResult httpResult = HTTP.builder().baseUrl(url).build().sync("").get(); int random = RandomUtil.randomInt(1, 10); try { int millis = random * 1000; String name = Thread.currentThread().getName(); System.out.println("Thread " + name + " millis = " + millis); Thread.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } return httpResult.getBody().toString();}, executorService)).collect(Collectors.toList());// 这样操作,耗时将会是最长的一个调用的时间Stopwatch s2 = Stopwatch.createStarted();List<String> results = collect.stream().map(CompletableFuture::join).collect(Collectors.toList());long d2 = s2.elapsed(TimeUnit.SECONDS);log.info("d2: {}", d2); 参考 《Java8实战》 https://blog.csdn.net/u010250863/article/details/79965465","link":"/2022/08/15/java8%E6%96%B0%E7%89%B9%E6%80%A7%E5%8F%8A%E5%A4%9A%E7%BA%BF%E7%A8%8B%E6%80%BB%E7%BB%93/"},{"title":"k8s 私有集群使用 metallb","text":"1 概述自己搭建的 k8s 集群,默认是不支持 Loadbalancer 类型的 service 的,云厂商一般会实现。 如果想自己搭建的 k8s 集群也具有这个功能,就得找一些替代品,metallb 就是其中比较好的一个。 它支持 arp 二层协议路由 BGP 协议路由两种方式。 BGP 协议路由需要搭建额外的软路由,因此,笔者选用简单的 arp 方式直接进行配置。 基于版本: K8s: 1.22.0 metallb: 0.13.4 2 具体操作2.1 开启 arp 模式执行 1kubectl edit configmap -n kube-system kube-proxy 编辑 k8s 配置: 12345apiVersion: kubeproxy.config.k8s.io/v1alpha1kind: KubeProxyConfigurationmode: "ipvs"ipvs: strictARP: true 将 strictARP 设置为 true,保存退出 2.2 安装 metallb编辑 metallb 配置文件 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875apiVersion: v1kind: Namespacemetadata: labels: pod-security.kubernetes.io/audit: privileged pod-security.kubernetes.io/enforce: privileged pod-security.kubernetes.io/warn: privileged name: metallb-system---apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata: annotations: controller-gen.kubebuilder.io/version: v0.7.0 name: addresspools.metallb.iospec: conversion: strategy: Webhook webhook: clientConfig: caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlGWlRDQ0EwMmdBd0lCQWdJVU5GRW1XcTM3MVpKdGkrMmlSQzk1WmpBV1MxZ3dEUVlKS29aSWh2Y05BUUVMDQpCUUF3UWpFTE1Ba0dBMVVFQmhNQ1dGZ3hGVEFUQmdOVkJBY01ERVJsWm1GMWJIUWdRMmwwZVRFY01Cb0dBMVVFDQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlNakEzTVRrd09UTXlNek5hRncweU1qQTRNVGd3DQpPVE15TXpOYU1FSXhDekFKQmdOVkJBWVRBbGhZTVJVd0V3WURWUVFIREF4RVpXWmhkV3gwSUVOcGRIa3hIREFhDQpCZ05WQkFvTUUwUmxabUYxYkhRZ1EyOXRjR0Z1ZVNCTWRHUXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDDQpEd0F3Z2dJS0FvSUNBUUNxVFpxMWZRcC9vYkdlenhES0o3OVB3Ny94azJwellualNzMlkzb1ZYSm5sRmM4YjVlDQpma2ZZQnY2bndscW1keW5PL2phWFBaQmRQSS82aFdOUDBkdVhadEtWU0NCUUpyZzEyOGNXb3F0MGNTN3pLb1VpDQpvcU1tQ0QvRXVBeFFNZjhRZDF2c1gvVllkZ0poVTZBRXJLZEpIaXpFOUJtUkNkTDBGMW1OVW55Rk82UnRtWFZUDQpidkxsTDVYeTc2R0FaQVBLOFB4aVlDa0NtbDdxN0VnTWNiOXlLWldCYmlxQ3VkTXE5TGJLNmdKNzF6YkZnSXV4DQo1L1pXK2JraTB2RlplWk9ZODUxb1psckFUNzJvMDI4NHNTWW9uN0pHZVZkY3NoUnh5R1VpSFpSTzdkaXZVTDVTDQpmM2JmSDFYbWY1ZDQzT0NWTWRuUUV2NWVaOG8zeWVLa3ZrbkZQUGVJMU9BbjdGbDlFRVNNR2dhOGFaSG1URSttDQpsLzlMSmdDYjBnQmtPT0M0WnV4bWh2aERKV1EzWnJCS3pMQlNUZXN0NWlLNVlwcXRWVVk2THRyRW9FelVTK1lsDQpwWndXY2VQWHlHeHM5ZURsR3lNVmQraW15Y3NTU1UvVno2Mmx6MnZCS21NTXBkYldDQWhud0RsRTVqU2dyMjRRDQp0eGNXLys2N3d5KzhuQlI3UXdqVTFITndVRjBzeERWdEwrZ1NHVERnSEVZSlhZelYvT05zMy94TkpoVFNPSkxNDQpoeXNVdyttaGdackdhbUdXcHVIVU1DUitvTWJzMTc1UkcrQjJnUFFHVytPTjJnUTRyOXN2b0ZBNHBBQm8xd1dLDQpRYjRhY3pmeVVscElBOVFoSmFsZEY3S3dPSHVlV3gwRUNrNXg0T2tvVDBvWVp0dzFiR0JjRGtaSmF3SURBUUFCDQpvMU13VVRBZEJnTlZIUTRFRmdRVW90UlNIUm9IWTEyRFZ4R0NCdEhpb1g2ZmVFQXdId1lEVlIwakJCZ3dGb0FVDQpvdFJTSFJvSFkxMkRWeEdDQnRIaW9YNmZlRUF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCDQpBUXNGQUFPQ0FnRUFSbkpsWWRjMTFHd0VxWnh6RDF2R3BDR2pDN2VWTlQ3aVY1d3IybXlybHdPYi9aUWFEa0xYDQpvVStaOVVXT1VlSXJTdzUydDdmQUpvVVAwSm5iYkMveVIrU1lqUGhvUXNiVHduOTc2ZldBWTduM3FMOXhCd1Y0DQphek41OXNjeUp0dlhMeUtOL2N5ak1ReDRLajBIMFg0bWJ6bzVZNUtzWWtYVU0vOEFPdWZMcEd0S1NGVGgrSEFDDQpab1Q5YnZHS25adnNHd0tYZFF0Wnh0akhaUjVqK3U3ZGtQOTJBT051RFNabS8rWVV4b2tBK09JbzdSR3BwSHNXDQo1ZTdNY0FTVXRtb1FORXd6dVFoVkJaRWQ1OGtKYjUrV0VWbGNzanlXNnRTbzErZ25tTWNqR1BsMWgxR2hVbjV4DQpFY0lWRnBIWXM5YWo1NmpBSjk1MVQvZjhMaWxmTlVnanBLQ0c1bnl0SUt3emxhOHNtdGlPdm1UNEpYbXBwSkI2DQo4bmdHRVluVjUrUTYwWFJ2OEhSSGp1VG9CRHVhaERrVDA2R1JGODU1d09FR2V4bkZpMXZYWUxLVllWb1V2MXRKDQo4dVdUR1pwNllDSVJldlBqbzg5ZytWTlJSaVFYUThJd0dybXE5c0RoVTlqTjA0SjdVL1RvRDFpNHE3VnlsRUc5DQorV1VGNkNLaEdBeTJIaEhwVncyTGFoOS9lUzdZMUZ1YURrWmhPZG1laG1BOCtqdHNZamJadnR5Mm1SWlF0UUZzDQpUU1VUUjREbUR2bVVPRVRmeStpRHdzK2RkWXVNTnJGeVVYV2dkMnpBQU4ydVl1UHFGY2pRcFNPODFzVTJTU3R3DQoxVzAyeUtYOGJEYmZFdjBzbUh3UzliQnFlSGo5NEM1Mjg0YXpsdTBmaUdpTm1OUEM4ckJLRmhBPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ== service: name: webhook-service namespace: metallb-system path: /convert conversionReviewVersions: - v1alpha1 - v1beta1 group: metallb.io names: kind: AddressPool listKind: AddressPoolList plural: addresspools singular: addresspool scope: Namespaced versions: - deprecated: true deprecationWarning: metallb.io v1alpha1 AddressPool is deprecated name: v1alpha1 schema: openAPIV3Schema: description: AddressPool is the Schema for the addresspools API. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: AddressPoolSpec defines the desired state of AddressPool. properties: addresses: description: A list of IP address ranges over which MetalLB has authority. You can list multiple ranges in a single pool, they will all share the same settings. Each range can be either a CIDR prefix, or an explicit start-end range of IPs. items: type: string type: array autoAssign: default: true description: AutoAssign flag used to prevent MetallB from automatic allocation for a pool. type: boolean bgpAdvertisements: description: When an IP is allocated from this pool, how should it be translated into BGP announcements? items: properties: aggregationLength: default: 32 description: The aggregation-length advertisement option lets you “roll up” the /32s into a larger prefix. format: int32 minimum: 1 type: integer aggregationLengthV6: default: 128 description: Optional, defaults to 128 (i.e. no aggregation) if not specified. format: int32 type: integer communities: description: BGP communities items: type: string type: array localPref: description: BGP LOCAL_PREF attribute which is used by BGP best path algorithm, Path with higher localpref is preferred over one with lower localpref. format: int32 type: integer type: object type: array protocol: description: Protocol can be used to select how the announcement is done. enum: - layer2 - bgp type: string required: - addresses - protocol type: object status: description: AddressPoolStatus defines the observed state of AddressPool. type: object required: - spec type: object served: true storage: false subresources: status: {} - deprecated: true deprecationWarning: metallb.io v1beta1 AddressPool is deprecated, consider using IPAddressPool name: v1beta1 schema: openAPIV3Schema: description: AddressPool represents a pool of IP addresses that can be allocated to LoadBalancer services. AddressPool is deprecated and being replaced by IPAddressPool. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: AddressPoolSpec defines the desired state of AddressPool. properties: addresses: description: A list of IP address ranges over which MetalLB has authority. You can list multiple ranges in a single pool, they will all share the same settings. Each range can be either a CIDR prefix, or an explicit start-end range of IPs. items: type: string type: array autoAssign: default: true description: AutoAssign flag used to prevent MetallB from automatic allocation for a pool. type: boolean bgpAdvertisements: description: Drives how an IP allocated from this pool should translated into BGP announcements. items: properties: aggregationLength: default: 32 description: The aggregation-length advertisement option lets you “roll up” the /32s into a larger prefix. format: int32 minimum: 1 type: integer aggregationLengthV6: default: 128 description: Optional, defaults to 128 (i.e. no aggregation) if not specified. format: int32 type: integer communities: description: BGP communities to be associated with the given advertisement. items: type: string type: array localPref: description: BGP LOCAL_PREF attribute which is used by BGP best path algorithm, Path with higher localpref is preferred over one with lower localpref. format: int32 type: integer type: object type: array protocol: description: Protocol can be used to select how the announcement is done. enum: - layer2 - bgp type: string required: - addresses - protocol type: object status: description: AddressPoolStatus defines the observed state of AddressPool. type: object required: - spec type: object served: true storage: true subresources: status: {}status: acceptedNames: kind: "" plural: "" conditions: [] storedVersions: []---apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata: annotations: controller-gen.kubebuilder.io/version: v0.7.0 creationTimestamp: null name: bfdprofiles.metallb.iospec: group: metallb.io names: kind: BFDProfile listKind: BFDProfileList plural: bfdprofiles singular: bfdprofile scope: Namespaced versions: - name: v1beta1 schema: openAPIV3Schema: description: BFDProfile represents the settings of the bfd session that can be optionally associated with a BGP session. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: BFDProfileSpec defines the desired state of BFDProfile. properties: detectMultiplier: description: Configures the detection multiplier to determine packet loss. The remote transmission interval will be multiplied by this value to determine the connection loss detection timer. format: int32 maximum: 255 minimum: 2 type: integer echoInterval: description: Configures the minimal echo receive transmission interval that this system is capable of handling in milliseconds. Defaults to 50ms format: int32 maximum: 60000 minimum: 10 type: integer echoMode: description: Enables or disables the echo transmission mode. This mode is disabled by default, and not supported on multi hops setups. type: boolean minimumTtl: description: 'For multi hop sessions only: configure the minimum expected TTL for an incoming BFD control packet.' format: int32 maximum: 254 minimum: 1 type: integer passiveMode: description: 'Mark session as passive: a passive session will not attempt to start the connection and will wait for control packets from peer before it begins replying.' type: boolean receiveInterval: description: The minimum interval that this system is capable of receiving control packets in milliseconds. Defaults to 300ms. format: int32 maximum: 60000 minimum: 10 type: integer transmitInterval: description: The minimum transmission interval (less jitter) that this system wants to use to send BFD control packets in milliseconds. Defaults to 300ms format: int32 maximum: 60000 minimum: 10 type: integer type: object status: description: BFDProfileStatus defines the observed state of BFDProfile. type: object type: object served: true storage: true subresources: status: {}status: acceptedNames: kind: "" plural: "" conditions: [] storedVersions: []---apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata: annotations: controller-gen.kubebuilder.io/version: v0.7.0 creationTimestamp: null name: bgpadvertisements.metallb.iospec: group: metallb.io names: kind: BGPAdvertisement listKind: BGPAdvertisementList plural: bgpadvertisements singular: bgpadvertisement scope: Namespaced versions: - name: v1beta1 schema: openAPIV3Schema: description: BGPAdvertisement allows to advertise the IPs coming from the selected IPAddressPools via BGP, setting the parameters of the BGP Advertisement. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: BGPAdvertisementSpec defines the desired state of BGPAdvertisement. properties: aggregationLength: default: 32 description: The aggregation-length advertisement option lets you “roll up” the /32s into a larger prefix. Defaults to 32. Works for IPv4 addresses. format: int32 minimum: 1 type: integer aggregationLengthV6: default: 128 description: The aggregation-length advertisement option lets you “roll up” the /128s into a larger prefix. Defaults to 128. Works for IPv6 addresses. format: int32 type: integer communities: description: The BGP communities to be associated with the announcement. Each item can be a community of the form 1234:1234 or the name of an alias defined in the Community CRD. items: type: string type: array ipAddressPoolSelectors: description: A selector for the IPAddressPools which would get advertised via this advertisement. If no IPAddressPool is selected by this or by the list, the advertisement is applied to all the IPAddressPools. items: description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. items: type: string type: array required: - key - operator type: object type: array matchLabels: additionalProperties: type: string description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object type: array ipAddressPools: description: The list of IPAddressPools to advertise via this advertisement, selected by name. items: type: string type: array localPref: description: The BGP LOCAL_PREF attribute which is used by BGP best path algorithm, Path with higher localpref is preferred over one with lower localpref. format: int32 type: integer nodeSelectors: description: NodeSelectors allows to limit the nodes to announce as next hops for the LoadBalancer IP. When empty, all the nodes having are announced as next hops. items: description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. items: type: string type: array required: - key - operator type: object type: array matchLabels: additionalProperties: type: string description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object type: array peers: description: Peers limits the bgppeer to advertise the ips of the selected pools to. When empty, the loadbalancer IP is announced to all the BGPPeers configured. items: type: string type: array type: object status: description: BGPAdvertisementStatus defines the observed state of BGPAdvertisement. type: object type: object served: true storage: true subresources: status: {}status: acceptedNames: kind: "" plural: "" conditions: [] storedVersions: []---apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata: annotations: controller-gen.kubebuilder.io/version: v0.7.0 name: bgppeers.metallb.iospec: conversion: strategy: Webhook webhook: clientConfig: caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlGWlRDQ0EwMmdBd0lCQWdJVU5GRW1XcTM3MVpKdGkrMmlSQzk1WmpBV1MxZ3dEUVlKS29aSWh2Y05BUUVMDQpCUUF3UWpFTE1Ba0dBMVVFQmhNQ1dGZ3hGVEFUQmdOVkJBY01ERVJsWm1GMWJIUWdRMmwwZVRFY01Cb0dBMVVFDQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlNakEzTVRrd09UTXlNek5hRncweU1qQTRNVGd3DQpPVE15TXpOYU1FSXhDekFKQmdOVkJBWVRBbGhZTVJVd0V3WURWUVFIREF4RVpXWmhkV3gwSUVOcGRIa3hIREFhDQpCZ05WQkFvTUUwUmxabUYxYkhRZ1EyOXRjR0Z1ZVNCTWRHUXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDDQpEd0F3Z2dJS0FvSUNBUUNxVFpxMWZRcC9vYkdlenhES0o3OVB3Ny94azJwellualNzMlkzb1ZYSm5sRmM4YjVlDQpma2ZZQnY2bndscW1keW5PL2phWFBaQmRQSS82aFdOUDBkdVhadEtWU0NCUUpyZzEyOGNXb3F0MGNTN3pLb1VpDQpvcU1tQ0QvRXVBeFFNZjhRZDF2c1gvVllkZ0poVTZBRXJLZEpIaXpFOUJtUkNkTDBGMW1OVW55Rk82UnRtWFZUDQpidkxsTDVYeTc2R0FaQVBLOFB4aVlDa0NtbDdxN0VnTWNiOXlLWldCYmlxQ3VkTXE5TGJLNmdKNzF6YkZnSXV4DQo1L1pXK2JraTB2RlplWk9ZODUxb1psckFUNzJvMDI4NHNTWW9uN0pHZVZkY3NoUnh5R1VpSFpSTzdkaXZVTDVTDQpmM2JmSDFYbWY1ZDQzT0NWTWRuUUV2NWVaOG8zeWVLa3ZrbkZQUGVJMU9BbjdGbDlFRVNNR2dhOGFaSG1URSttDQpsLzlMSmdDYjBnQmtPT0M0WnV4bWh2aERKV1EzWnJCS3pMQlNUZXN0NWlLNVlwcXRWVVk2THRyRW9FelVTK1lsDQpwWndXY2VQWHlHeHM5ZURsR3lNVmQraW15Y3NTU1UvVno2Mmx6MnZCS21NTXBkYldDQWhud0RsRTVqU2dyMjRRDQp0eGNXLys2N3d5KzhuQlI3UXdqVTFITndVRjBzeERWdEwrZ1NHVERnSEVZSlhZelYvT05zMy94TkpoVFNPSkxNDQpoeXNVdyttaGdackdhbUdXcHVIVU1DUitvTWJzMTc1UkcrQjJnUFFHVytPTjJnUTRyOXN2b0ZBNHBBQm8xd1dLDQpRYjRhY3pmeVVscElBOVFoSmFsZEY3S3dPSHVlV3gwRUNrNXg0T2tvVDBvWVp0dzFiR0JjRGtaSmF3SURBUUFCDQpvMU13VVRBZEJnTlZIUTRFRmdRVW90UlNIUm9IWTEyRFZ4R0NCdEhpb1g2ZmVFQXdId1lEVlIwakJCZ3dGb0FVDQpvdFJTSFJvSFkxMkRWeEdDQnRIaW9YNmZlRUF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCDQpBUXNGQUFPQ0FnRUFSbkpsWWRjMTFHd0VxWnh6RDF2R3BDR2pDN2VWTlQ3aVY1d3IybXlybHdPYi9aUWFEa0xYDQpvVStaOVVXT1VlSXJTdzUydDdmQUpvVVAwSm5iYkMveVIrU1lqUGhvUXNiVHduOTc2ZldBWTduM3FMOXhCd1Y0DQphek41OXNjeUp0dlhMeUtOL2N5ak1ReDRLajBIMFg0bWJ6bzVZNUtzWWtYVU0vOEFPdWZMcEd0S1NGVGgrSEFDDQpab1Q5YnZHS25adnNHd0tYZFF0Wnh0akhaUjVqK3U3ZGtQOTJBT051RFNabS8rWVV4b2tBK09JbzdSR3BwSHNXDQo1ZTdNY0FTVXRtb1FORXd6dVFoVkJaRWQ1OGtKYjUrV0VWbGNzanlXNnRTbzErZ25tTWNqR1BsMWgxR2hVbjV4DQpFY0lWRnBIWXM5YWo1NmpBSjk1MVQvZjhMaWxmTlVnanBLQ0c1bnl0SUt3emxhOHNtdGlPdm1UNEpYbXBwSkI2DQo4bmdHRVluVjUrUTYwWFJ2OEhSSGp1VG9CRHVhaERrVDA2R1JGODU1d09FR2V4bkZpMXZYWUxLVllWb1V2MXRKDQo4dVdUR1pwNllDSVJldlBqbzg5ZytWTlJSaVFYUThJd0dybXE5c0RoVTlqTjA0SjdVL1RvRDFpNHE3VnlsRUc5DQorV1VGNkNLaEdBeTJIaEhwVncyTGFoOS9lUzdZMUZ1YURrWmhPZG1laG1BOCtqdHNZamJadnR5Mm1SWlF0UUZzDQpUU1VUUjREbUR2bVVPRVRmeStpRHdzK2RkWXVNTnJGeVVYV2dkMnpBQU4ydVl1UHFGY2pRcFNPODFzVTJTU3R3DQoxVzAyeUtYOGJEYmZFdjBzbUh3UzliQnFlSGo5NEM1Mjg0YXpsdTBmaUdpTm1OUEM4ckJLRmhBPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ== service: name: webhook-service namespace: metallb-system path: /convert conversionReviewVersions: - v1beta1 - v1beta2 group: metallb.io names: kind: BGPPeer listKind: BGPPeerList plural: bgppeers singular: bgppeer scope: Namespaced versions: - name: v1beta1 schema: openAPIV3Schema: description: BGPPeer is the Schema for the peers API. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: BGPPeerSpec defines the desired state of Peer. properties: bfdProfile: type: string ebgpMultiHop: description: EBGP peer is multi-hops away type: boolean holdTime: description: Requested BGP hold time, per RFC4271. type: string keepaliveTime: description: Requested BGP keepalive time, per RFC4271. type: string myASN: description: AS number to use for the local end of the session. format: int32 maximum: 4294967295 minimum: 0 type: integer nodeSelectors: description: Only connect to this peer on nodes that match one of these selectors. items: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string minItems: 1 type: array required: - key - operator - values type: object type: array matchLabels: additionalProperties: type: string type: object type: object type: array password: description: Authentication password for routers enforcing TCP MD5 authenticated sessions type: string peerASN: description: AS number to expect from the remote end of the session. format: int32 maximum: 4294967295 minimum: 0 type: integer peerAddress: description: Address to dial when establishing the session. type: string peerPort: description: Port to dial when establishing the session. maximum: 16384 minimum: 0 type: integer routerID: description: BGP router ID to advertise to the peer type: string sourceAddress: description: Source address to use when establishing the session. type: string required: - myASN - peerASN - peerAddress type: object status: description: BGPPeerStatus defines the observed state of Peer. type: object type: object served: true storage: false subresources: status: {} - name: v1beta2 schema: openAPIV3Schema: description: BGPPeer is the Schema for the peers API. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: BGPPeerSpec defines the desired state of Peer. properties: bfdProfile: description: The name of the BFD Profile to be used for the BFD session associated to the BGP session. If not set, the BFD session won't be set up. type: string ebgpMultiHop: description: To set if the BGPPeer is multi-hops away. Needed for FRR mode only. type: boolean holdTime: description: Requested BGP hold time, per RFC4271. type: string keepaliveTime: description: Requested BGP keepalive time, per RFC4271. type: string myASN: description: AS number to use for the local end of the session. format: int32 maximum: 4294967295 minimum: 0 type: integer nodeSelectors: description: Only connect to this peer on nodes that match one of these selectors. items: description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. items: type: string type: array required: - key - operator type: object type: array matchLabels: additionalProperties: type: string description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object type: array password: description: Authentication password for routers enforcing TCP MD5 authenticated sessions type: string passwordSecret: description: passwordSecret is name of the authentication secret for BGP Peer. the secret must be of type "kubernetes.io/basic-auth", and created in the same namespace as the MetalLB deployment. The password is stored in the secret as the key "password". properties: name: description: name is unique within a namespace to reference a secret resource. type: string namespace: description: namespace defines the space within which the secret name must be unique. type: string type: object peerASN: description: AS number to expect from the remote end of the session. format: int32 maximum: 4294967295 minimum: 0 type: integer peerAddress: description: Address to dial when establishing the session. type: string peerPort: default: 179 description: Port to dial when establishing the session. maximum: 16384 minimum: 0 type: integer routerID: description: BGP router ID to advertise to the peer type: string sourceAddress: description: Source address to use when establishing the session. type: string required: - myASN - peerASN - peerAddress type: object status: description: BGPPeerStatus defines the observed state of Peer. type: object type: object served: true storage: true subresources: status: {}status: acceptedNames: kind: "" plural: "" conditions: [] storedVersions: []---apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata: annotations: controller-gen.kubebuilder.io/version: v0.7.0 creationTimestamp: null name: communities.metallb.iospec: group: metallb.io names: kind: Community listKind: CommunityList plural: communities singular: community scope: Namespaced versions: - name: v1beta1 schema: openAPIV3Schema: description: Community is a collection of aliases for communities. Users can define named aliases to be used in the BGPPeer CRD. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: CommunitySpec defines the desired state of Community. properties: communities: items: properties: name: description: The name of the alias for the community. type: string value: description: The BGP community value corresponding to the given name. type: string type: object type: array type: object status: description: CommunityStatus defines the observed state of Community. type: object type: object served: true storage: true subresources: status: {}status: acceptedNames: kind: "" plural: "" conditions: [] storedVersions: []---apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata: annotations: controller-gen.kubebuilder.io/version: v0.7.0 creationTimestamp: null name: ipaddresspools.metallb.iospec: group: metallb.io names: kind: IPAddressPool listKind: IPAddressPoolList plural: ipaddresspools singular: ipaddresspool scope: Namespaced versions: - name: v1beta1 schema: openAPIV3Schema: description: IPAddressPool represents a pool of IP addresses that can be allocated to LoadBalancer services. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: IPAddressPoolSpec defines the desired state of IPAddressPool. properties: addresses: description: A list of IP address ranges over which MetalLB has authority. You can list multiple ranges in a single pool, they will all share the same settings. Each range can be either a CIDR prefix, or an explicit start-end range of IPs. items: type: string type: array autoAssign: default: true description: AutoAssign flag used to prevent MetallB from automatic allocation for a pool. type: boolean avoidBuggyIPs: default: false description: AvoidBuggyIPs prevents addresses ending with .0 and .255 to be used by a pool. type: boolean required: - addresses type: object status: description: IPAddressPoolStatus defines the observed state of IPAddressPool. type: object required: - spec type: object served: true storage: true subresources: status: {}status: acceptedNames: kind: "" plural: "" conditions: [] storedVersions: []---apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata: annotations: controller-gen.kubebuilder.io/version: v0.7.0 creationTimestamp: null name: l2advertisements.metallb.iospec: group: metallb.io names: kind: L2Advertisement listKind: L2AdvertisementList plural: l2advertisements singular: l2advertisement scope: Namespaced versions: - name: v1beta1 schema: openAPIV3Schema: description: L2Advertisement allows to advertise the LoadBalancer IPs provided by the selected pools via L2. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: L2AdvertisementSpec defines the desired state of L2Advertisement. properties: ipAddressPoolSelectors: description: A selector for the IPAddressPools which would get advertised via this advertisement. If no IPAddressPool is selected by this or by the list, the advertisement is applied to all the IPAddressPools. items: description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. items: type: string type: array required: - key - operator type: object type: array matchLabels: additionalProperties: type: string description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object type: array ipAddressPools: description: The list of IPAddressPools to advertise via this advertisement, selected by name. items: type: string type: array nodeSelectors: description: NodeSelectors allows to limit the nodes to announce as next hops for the LoadBalancer IP. When empty, all the nodes having are announced as next hops. items: description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. items: type: string type: array required: - key - operator type: object type: array matchLabels: additionalProperties: type: string description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object type: array type: object status: description: L2AdvertisementStatus defines the observed state of L2Advertisement. type: object type: object served: true storage: true subresources: status: {}status: acceptedNames: kind: "" plural: "" conditions: [] storedVersions: []---apiVersion: v1kind: ServiceAccountmetadata: labels: app: metallb name: controller namespace: metallb-system---apiVersion: v1kind: ServiceAccountmetadata: labels: app: metallb name: speaker namespace: metallb-system---apiVersion: policy/v1beta1kind: PodSecurityPolicymetadata: labels: app: metallb name: controllerspec: allowPrivilegeEscalation: false allowedCapabilities: [] allowedHostPaths: [] defaultAddCapabilities: [] defaultAllowPrivilegeEscalation: false fsGroup: ranges: - max: 65535 min: 1 rule: MustRunAs hostIPC: false hostNetwork: false hostPID: false privileged: false readOnlyRootFilesystem: true requiredDropCapabilities: - ALL runAsUser: ranges: - max: 65535 min: 1 rule: MustRunAs seLinux: rule: RunAsAny supplementalGroups: ranges: - max: 65535 min: 1 rule: MustRunAs volumes: - configMap - secret - emptyDir---apiVersion: policy/v1beta1kind: PodSecurityPolicymetadata: labels: app: metallb name: speakerspec: allowPrivilegeEscalation: false allowedCapabilities: - NET_RAW allowedHostPaths: [] defaultAddCapabilities: [] defaultAllowPrivilegeEscalation: false fsGroup: rule: RunAsAny hostIPC: false hostNetwork: true hostPID: false hostPorts: - max: 7472 min: 7472 - max: 7946 min: 7946 privileged: true readOnlyRootFilesystem: true requiredDropCapabilities: - ALL runAsUser: rule: RunAsAny seLinux: rule: RunAsAny supplementalGroups: rule: RunAsAny volumes: - configMap - secret - emptyDir---apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: labels: app: metallb name: controller namespace: metallb-systemrules:- apiGroups: - "" resources: - secrets verbs: - create - delete - get - list - patch - update - watch- apiGroups: - "" resourceNames: - memberlist resources: - secrets verbs: - list- apiGroups: - apps resourceNames: - controller resources: - deployments verbs: - get- apiGroups: - metallb.io resources: - bgppeers verbs: - get - list- apiGroups: - metallb.io resources: - addresspools verbs: - get - list - watch- apiGroups: - metallb.io resources: - bfdprofiles verbs: - get - list - watch- apiGroups: - metallb.io resources: - ipaddresspools verbs: - get - list - watch- apiGroups: - metallb.io resources: - bgpadvertisements verbs: - get - list - watch- apiGroups: - metallb.io resources: - l2advertisements verbs: - get - list - watch- apiGroups: - metallb.io resources: - communities verbs: - get - list - watch---apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: labels: app: metallb name: pod-lister namespace: metallb-systemrules:- apiGroups: - "" resources: - pods verbs: - list- apiGroups: - "" resources: - secrets verbs: - get - list - watch- apiGroups: - metallb.io resources: - addresspools verbs: - get - list - watch- apiGroups: - metallb.io resources: - bfdprofiles verbs: - get - list - watch- apiGroups: - metallb.io resources: - bgppeers verbs: - get - list - watch- apiGroups: - metallb.io resources: - l2advertisements verbs: - get - list - watch- apiGroups: - metallb.io resources: - bgpadvertisements verbs: - get - list - watch- apiGroups: - metallb.io resources: - ipaddresspools verbs: - get - list - watch- apiGroups: - metallb.io resources: - communities verbs: - get - list - watch---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: labels: app: metallb name: metallb-system:controllerrules:- apiGroups: - "" resources: - services verbs: - get - list - watch- apiGroups: - "" resources: - services/status verbs: - update- apiGroups: - "" resources: - events verbs: - create - patch- apiGroups: - policy resourceNames: - controller resources: - podsecuritypolicies verbs: - use- apiGroups: - admissionregistration.k8s.io resources: - validatingwebhookconfigurations - mutatingwebhookconfigurations verbs: - create - delete - get - list - patch - update - watch- apiGroups: - apiextensions.k8s.io resources: - customresourcedefinitions verbs: - create - delete - get - list - patch - update - watch---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: labels: app: metallb name: metallb-system:speakerrules:- apiGroups: - "" resources: - services - endpoints - nodes verbs: - get - list - watch- apiGroups: - discovery.k8s.io resources: - endpointslices verbs: - get - list - watch- apiGroups: - "" resources: - events verbs: - create - patch- apiGroups: - policy resourceNames: - speaker resources: - podsecuritypolicies verbs: - use---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: labels: app: metallb name: controller namespace: metallb-systemroleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: controllersubjects:- kind: ServiceAccount name: controller namespace: metallb-system---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: labels: app: metallb name: pod-lister namespace: metallb-systemroleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: pod-listersubjects:- kind: ServiceAccount name: speaker namespace: metallb-system---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: labels: app: metallb name: metallb-system:controllerroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: metallb-system:controllersubjects:- kind: ServiceAccount name: controller namespace: metallb-system---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: labels: app: metallb name: metallb-system:speakerroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: metallb-system:speakersubjects:- kind: ServiceAccount name: speaker namespace: metallb-system---apiVersion: v1kind: Secretmetadata: name: webhook-server-cert namespace: metallb-system---apiVersion: v1kind: Servicemetadata: name: webhook-service namespace: metallb-systemspec: ports: - port: 443 targetPort: 9443 selector: component: controller---apiVersion: apps/v1kind: Deploymentmetadata: labels: app: metallb component: controller name: controller namespace: metallb-systemspec: revisionHistoryLimit: 3 selector: matchLabels: app: metallb component: controller template: metadata: annotations: prometheus.io/port: "7472" prometheus.io/scrape: "true" labels: app: metallb component: controller spec: containers: - args: - --port=7472 - --log-level=info env: - name: METALLB_ML_SECRET_NAME value: memberlist - name: METALLB_DEPLOYMENT value: controller image: quay.io/metallb/controller:v0.13.4 livenessProbe: failureThreshold: 3 httpGet: path: /metrics port: monitoring initialDelaySeconds: 10 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 1 name: controller ports: - containerPort: 7472 name: monitoring - containerPort: 9443 name: webhook-server protocol: TCP readinessProbe: failureThreshold: 3 httpGet: path: /metrics port: monitoring initialDelaySeconds: 10 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 1 securityContext: allowPrivilegeEscalation: false capabilities: drop: - all readOnlyRootFilesystem: true volumeMounts: - mountPath: /tmp/k8s-webhook-server/serving-certs name: cert readOnly: true nodeSelector: kubernetes.io/os: linux securityContext: fsGroup: 65534 runAsNonRoot: true runAsUser: 65534 serviceAccountName: controller terminationGracePeriodSeconds: 0 volumes: - name: cert secret: defaultMode: 420 secretName: webhook-server-cert---apiVersion: apps/v1kind: DaemonSetmetadata: labels: app: metallb component: speaker name: speaker namespace: metallb-systemspec: selector: matchLabels: app: metallb component: speaker template: metadata: annotations: prometheus.io/port: "7472" prometheus.io/scrape: "true" labels: app: metallb component: speaker spec: containers: - args: - --port=7472 - --log-level=info env: - name: METALLB_NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName - name: METALLB_HOST valueFrom: fieldRef: fieldPath: status.hostIP - name: METALLB_ML_BIND_ADDR valueFrom: fieldRef: fieldPath: status.podIP - name: METALLB_ML_LABELS value: app=metallb,component=speaker - name: METALLB_ML_SECRET_KEY valueFrom: secretKeyRef: key: secretkey name: memberlist image: quay.io/metallb/speaker:v0.13.4 livenessProbe: failureThreshold: 3 httpGet: path: /metrics port: monitoring initialDelaySeconds: 10 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 1 name: speaker ports: - containerPort: 7472 name: monitoring - containerPort: 7946 name: memberlist-tcp - containerPort: 7946 name: memberlist-udp protocol: UDP readinessProbe: failureThreshold: 3 httpGet: path: /metrics port: monitoring initialDelaySeconds: 10 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 1 securityContext: allowPrivilegeEscalation: false capabilities: add: - NET_RAW drop: - ALL readOnlyRootFilesystem: true hostNetwork: true nodeSelector: kubernetes.io/os: linux serviceAccountName: speaker terminationGracePeriodSeconds: 2 tolerations: - effect: NoSchedule key: node-role.kubernetes.io/master operator: Exists - effect: NoSchedule key: node-role.kubernetes.io/control-plane operator: Exists---apiVersion: admissionregistration.k8s.io/v1kind: ValidatingWebhookConfigurationmetadata: creationTimestamp: null name: metallb-webhook-configurationwebhooks:- admissionReviewVersions: - v1 clientConfig: service: name: webhook-service namespace: metallb-system path: /validate-metallb-io-v1beta2-bgppeer failurePolicy: Fail name: bgppeersvalidationwebhook.metallb.io rules: - apiGroups: - metallb.io apiVersions: - v1beta2 operations: - CREATE - UPDATE resources: - bgppeers sideEffects: None- admissionReviewVersions: - v1 clientConfig: service: name: webhook-service namespace: metallb-system path: /validate-metallb-io-v1beta1-addresspool failurePolicy: Fail name: addresspoolvalidationwebhook.metallb.io rules: - apiGroups: - metallb.io apiVersions: - v1beta1 operations: - CREATE - UPDATE resources: - addresspools sideEffects: None- admissionReviewVersions: - v1 clientConfig: service: name: webhook-service namespace: metallb-system path: /validate-metallb-io-v1beta1-bfdprofile failurePolicy: Fail name: bfdprofilevalidationwebhook.metallb.io rules: - apiGroups: - metallb.io apiVersions: - v1beta1 operations: - DELETE resources: - bfdprofiles sideEffects: None- admissionReviewVersions: - v1 clientConfig: service: name: webhook-service namespace: metallb-system path: /validate-metallb-io-v1beta1-bgpadvertisement failurePolicy: Fail name: bgpadvertisementvalidationwebhook.metallb.io rules: - apiGroups: - metallb.io apiVersions: - v1beta1 operations: - CREATE - UPDATE resources: - bgpadvertisements sideEffects: None- admissionReviewVersions: - v1 clientConfig: service: name: webhook-service namespace: metallb-system path: /validate-metallb-io-v1beta1-community failurePolicy: Fail name: communityvalidationwebhook.metallb.io rules: - apiGroups: - metallb.io apiVersions: - v1beta1 operations: - CREATE - UPDATE resources: - communities sideEffects: None- admissionReviewVersions: - v1 clientConfig: service: name: webhook-service namespace: metallb-system path: /validate-metallb-io-v1beta1-ipaddresspool failurePolicy: Fail name: ipaddresspoolvalidationwebhook.metallb.io rules: - apiGroups: - metallb.io apiVersions: - v1beta1 operations: - CREATE - UPDATE resources: - ipaddresspools sideEffects: None- admissionReviewVersions: - v1 clientConfig: service: name: webhook-service namespace: metallb-system path: /validate-metallb-io-v1beta1-l2advertisement failurePolicy: Fail name: l2advertisementvalidationwebhook.metallb.io rules: - apiGroups: - metallb.io apiVersions: - v1beta1 operations: - CREATE - UPDATE resources: - l2advertisements sideEffects: None 保存之后,执行: 1kubectl apply -f metallb-native.yml 查看安装状态: 1kubectl get pods -n metallb-system 2.3 配置地址池编辑配置文件 12345678apiVersion: metallb.io/v1beta1kind: IPAddressPoolmetadata: name: cheap namespace: metallb-systemspec: addresses: - 192.168.10.0/24 这个配置将会创建一个名称为 cheap 的 ip 池。 以后如果我们要创建一个 loadbalancer 类型的 service,metallb 将会从这个 ip 池中分配一个 ip 给它进行使用。 需要说明的是,这个 ip 池的网段可以跟主机的网段相同,也可以不同。 比如 k8s 节点主机所属网段为 192.168.3.0/24,当网段相同时,那么这个 service 的外部 ip 就能够直接被集群外的主机访问。 如果网段不同时,service 的外部 ip 将只能被集群内的主机访问。 如果此时没理解也没关系,这一点后面具体说明 2.4 测试以创建一个代理 nginx pod 的 service 为例。配置文件如下: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849kind: Podmetadata: name: "nginx" namespace: tutorial labels: app: "nginx"spec: containers: - name: nginx image: "nginx:latest" imagePullPolicy: IfNotPresent resources: limits: cpu: 200m memory: 500Mi requests: cpu: 100m memory: 200Mi ports: - containerPort: 80 name: http volumeMounts: - name: localtime mountPath: /etc/localtime volumes: - name: localtime hostPath: path: /usr/share/zoneinfo/Asia/Shanghai restartPolicy: Always---apiVersion: v1kind: Servicemetadata: name: nginx-service namespace: tutorialspec: selector: app: nginx type: LoadBalancer sessionAffinity: ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 10800 ports: - name: nginx-service protocol: TCP port: 80 targetPort: 80 应用该配置:kubectl apply -f xxx.yaml 查看 pod 与 service: K8s 集群外相同网段主机 ping external ip: K8s 集群内主机 ping external ip:","link":"/2022/08/15/k8s-%E7%A7%81%E6%9C%89%E9%9B%86%E7%BE%A4%E4%BD%BF%E7%94%A8-metallb/"},{"title":"k8s 配置 helm","text":"","link":"/2022/08/26/k8s-%E9%85%8D%E7%BD%AE-helm/"},{"title":"wsl2 使用宿主机 clash 访问外网","text":"1 概述以 wsl2 为例,实际上可以拓展到局域网虚拟机通过电脑 pc 场景,实现一台主机通过另一台主机的 clash 进行翻墙 2 具体操作修改 /etc/profile,追加: 1234# wsl to clashexport hostip=$(cat /etc/resolv.conf |grep -oP '(?<=nameserver\\ ).*')export https_proxy="http://${hostip}:7890"export http_proxy="http://${hostip}:7890" 执行 source /etc/profile 使修改生效 在 wsl2 场景中,此处的 hostip 应该是 wsl2 网关的 ip,而这个网关 ip 实际上是宿主机的一个网卡。这样,wsl 2 通过访问网关 ip ,实际上也就将流量转发到了宿主机上。 在虚拟机场景中,此处的 hostip 也就是另一台主机的网关 ip。 配置好了之后,可以在主机上 telnet ${hostip} 7890 测试连通性,如果不通,可以通过下面的故障排除进行修复。 3 故障排除如果在 wsl2 中,telnet 网关IP 7890 不通,可以尝试以下操作: 宿主机,使用管理员权限执行:New-NetFirewallRule -DisplayName "WSL" -Direction Inbound -InterfaceAlias "vEthernet (WSL)" -Action Allow 放开宿主机 clash 防火墙:https://www.v2fy.com/p/2021-09-24-windows-clash-wsl2-1632440722000/","link":"/2022/08/26/wsl2-%E4%BD%BF%E7%94%A8%E5%AE%BF%E4%B8%BB%E6%9C%BA-clash-%E8%AE%BF%E9%97%AE%E5%A4%96%E7%BD%91/"},{"title":"k8s 使用 storage class 为 pvc 自动创建 pv","text":"1 概述之前创建了一个裸 k8s 集群,现在由于部署需求,pod 需要使用外部存储卷,也就是需要 pvc。 说到这儿,自然就需要先明白 k8s 中的 pvc,pv 和 storage class 这几个概念。 pvc 可以理解为 pod 的 volume,用于存储数据,相当于 docker-compose 中的 volume 配置 pv 可以理解为用于创建 pvc 的资源池 storage class 相当于存储卷的实现,也相当于 pv 的模板,可以通过指定 storage class 创建出需要的 pv 比如,我们可以在创建 pvc 的时候,直接指定 storage class,storage class 会创建出符合我们需要的 pv,绑定到 pvc。 由于是私有 k8s 集群的原因,没有提供默认的 storage class,跟 ingress 一样,公有云厂商一般都提供了默认的实现。 普遍的做法是使用 nfs 实现。目前也有一个开源项目进行了实现: https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/ 本文也是基于这个进行了部署,基于 k8s 1.22.0 2 具体实现新增一台主机:nfs 固定 ip 关闭防火墙、selinux 假设 nfs ip 为:192.168.1.19 2.1 nfs 服务端操作执行命令 1yum install -y nfs-utils rpcbind 创建一个共享目录,这个目录下存放未来所有的挂载数据 1mkdir /root/nfs 在 /etc/profile 文件尾部追加: 1/root/nfs *(rw,no_root_squash,sync) 查看配置是否生效 123exportfs -rexportfs 启动并检查服务: 123systemctl restart rpcbind && systemctl enable rpcbindsystemctl restart nfs && systemctl enable nfslsof -i :111 2.2 nfs 客户端操作所有 k8s 节点都执行以下操作: 1234yum -y install nfs-utilssystemctl start nfs && systemctl enable nfsshowmount -e 192.168.1.19mount -t nfs 192.168.1.19:/root/nfs /mnt 2.3 master 节点操作安装 NFS Provisioner 引入 helm 源 12helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/helm repo update 拉取 helm 模板 12helm pull nfs-subdir-external-provisioner/nfs-subdir-external-provisionertar -zxvf nfs-subdir-external-provisioner-4.0.16.tgz 解压后,修改 nfs-subdir-external-provisioner 目录下的 value.yaml 安装 provisioner,在 /root 目录下执行: 1helm install nfs-subdir-external-provisioner nfs-subdir-external-provisioner/ --set nfs.server=192.168.1.19 --set nfs.path=/root/nfs 查看 provisioner pod: pod 创建成功会自动创建一个 storage class: 这样,即可创建完成 2.4 测试创建 test-pvc.yaml ,内容如下: 1234567891011kind: PersistentVolumeClaimapiVersion: v1metadata: name: test-pvcspec: storageClassName: nfs-client accessModes: - ReadWriteOnce resources: requests: storage: 1Mi 执行安装: 1kubectl apply -f test-pvc.yaml 如图,pvc、pv 都成功创建,测试通过","link":"/2022/08/26/k8s-%E4%BD%BF%E7%94%A8-storage-class-%E4%B8%BA-pvc-%E8%87%AA%E5%8A%A8%E5%88%9B%E5%BB%BA-pv/"},{"title":"okteto 连接私有 k8s 集群","text":"1 概述okteto 是一个 cli 工具,可以将代码部署到 k8s 集群,并能够实现代码的同步。 官方给出的示例是基于 okteto cloud进行配置的。但是如果我们想用自己的 k8s 集群应该怎么搞,官方还没有写出详细的教程。 以下就是笔者的趟坑之旅 2 具体实现实现分以下几步: 创建一个裸 k8s 集群 安装 helm 配置 nfs 存储 配置 kubeconfig okteto 连接 2.1 创建一个裸 k8s 集群具体机器要求与配置方式,参考:https://rainbowechoes.github.io/2022/07/18/k8s-%E5%85%A5%E9%97%A8/ 2.2 安装 helmk8s master 和本机安装 helm,这一步可选。因为是否需要安装 helm 取决于你使用 okteto 同步的工程,是否是 helm 部署程序 这一步 helm 官网有具体操作,不再赘述。 2.3 配置 nfs 存储okteto 构建程序的时候,大概率都会需要使用 pvc,那么就需要我们的私有 k8s 集群本身也能提供存储功能的支持。 搭建参考方式参考:https://rainbowechoes.github.io/2022/08/26/k8s-%E4%BD%BF%E7%94%A8-storage-class-%E4%B8%BA-pvc-%E8%87%AA%E5%8A%A8%E5%88%9B%E5%BB%BA-pv/ 2.4 配置 kubeconfig将 master 主机上的 /root/.kube/config 文件放在本机家目录同样位置,并在本机上安装 kubectl。 安装 kubectl 这一步,根据自身电脑操作系统,可自行搜索 安装 kubectl 参考:https://kubernetes.io/zh-cn/docs/tasks/tools/install-kubectl-windows/ 笔者使用 scoop 安装 验证: 由于我们已经将 kubeconfig 文件放到了本机,因此可以像在 master 上,直接使用 kubectl 命令查看集群信息 12(base) PS C:\\Users\\10744> kubectl.exe get podsUnable to connect to the server: dial tcp: lookup apiserver.cluster.local: no such host 但是得到这个信息,因为我们本机没有这个域名的解析,在 master 主机上 ping 一下这个域名,知道 ip 后,放入我们自己的 hosts 文件里面进行解析即可。 之后,就可以成功获取到资源信息 123456789(base) PS C:\\Users\\10744> kubectl.exe get podsNAME READY STATUS RESTARTS AGEapi-6dcd44c666-s8hdr 1/1 Running 1 (9d ago) 49dfrontend-okteto-65f97ddfbc-4znzl 1/1 Running 1 (9d ago) 49dk8s-cus-demo-okteto-67f788468f-zvdq4 1/1 Running 1 (9d ago) 41dk8s-demo-okteto-dcff788d4-lxtzd 1/1 Running 1 (9d ago) 41dmicronaut-k8s-okteto-96c47957c-lttkd 1/1 Running 1 (9d ago) 14dmongodb-0 1/1 Running 1 (9d ago) 49dnfs-subdir-external-provisioner-9fbc4fdf4-j6qm5 1/1 Running 2 (9d ago) 49d 2.5 okteto 连接这步是最重要的,但是也很简单。 okteto 官方文档地址:https://www.okteto.com/docs/welcome/overview/ 先安装 okteto,参考官方文档 选择并使用 context: 123456(base) PS C:\\Users\\10744> okteto context ✓ Context 'kubernetes-admin@kubernetes' selected ✓ Using context default @ kubernetes-admin@kubernetes(base) PS C:\\Users\\10744> okteto context listName Namespace Builder Registrykubernetes-admin@kubernetes * default docker 使用 okteto context 能够自动显示可用的所有 context。第一个是 okteto cloud,第二个就是我们的私有 k8s 集群 至此,我们已经实现了使用 okteto 连接私有 k8s 集群。 后面将会有文章讲述,连接了集群之后,怎么使用 okteto 对 springboot 工程进行远程云原生开发","link":"/2022/08/26/okteto-%E8%BF%9E%E6%8E%A5%E7%A7%81%E6%9C%89-k8s-%E9%9B%86%E7%BE%A4/"},{"title":"k8s 配置 dashboard","text":"","link":"/2022/08/26/k8s-%E9%85%8D%E7%BD%AE-dashboard/"},{"title":"keycloak 源码分析","text":"keycloak 源码分析以 15.1.1 版本为例: sat 相关接口位置通过 sat 调用的接口,代码在 org.keycloak.services.resources 包下,模块名为 services 比如与用户相关的 sat 接口,对应类为 org.keycloak.services.resources.admin.UsersResource Authenticator 自定义org.keycloak.authentication.authenticators.broker.IdpAutoLinkAuthenticator Authentication flow 中的 Automatically set existing user Authenticator 角色、权限控制keycloak 集成配置如下图: 其中,use-resource-role-mappings 与角色控制相关。在源码里面,它的作用是: 当该配置为 true 时,使用 client 中的 role 作为用户的鉴权角色。 当该配置为 false 时,使用 realm 中的 role 作为用户的鉴权角色。 这个配置,推荐设置为 false。因为一个 realm 可以对应一个程序,client 对应不同服务,他们都应该对应同一套角色体系。","link":"/2023/01/05/keycloak-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"},{"title":"java17 中反射的坑","text":"1 概述笔者目前的一个项目使用了 java 17 进行开发。并且在项目开发中,由于需要去获取某个接口下的所有子类,并为其自动创建 bean。于是用了反射去处理,但后面没想到却踩了坑。 2 具体过程项目开发环境如下: 笔者自定义了一个开发框架,并以 springboot starter 的形式进行引入。 定义一个反射工具类,这个工具类核心原理是通过接收一个 Class 类型的参数,获取到这个 Class 所在 package 同级及以下的所有类,并通过 isAssignableFrom 方法,从这些类中找出参数 Class 的子类。 在业务工程引入了自己开发的框架后,使用该工具类去获取某个接口下的子类,并将子类变为 spring 的 bean 照着这样的逻辑,程序工作了一段时间。 直到今天我运行项目的时候发现,在第三步的时候,使用工具类获取不到子类了,这让我很不解。 在工具类的方法中调试发现,子类与父类所属的 module 不一样。 我当时认为是:这几个类虽然在一个工程里面,但是我并没有添加 module-info.java 文件,导致 java 默认生成 module 的时候生成了两个 module。 于是,我定义 module-info.java 然后重试,发现还是一样。 后面尝试发现,只要我把这个工具类从框架代码中迁到业务工程中,就能够成功获取到子类。 所以原因是,加载父类的时候,是在业务工程里面,属于业务工程的 module,但是加载子类的时候,是在框架代码中,属于框架工程的 module。而 java 9 之后,不仅基于了全限定类名,也基于模块进行了类的判定。因此,子类并不属于父类。 不过,最奇怪的就是,跑了一个多月都没出问题,突然一下就这样。还是很奇怪","link":"/2022/10/21/java17-%E4%B8%AD%E5%8F%8D%E5%B0%84%E7%9A%84%E5%9D%91/"},{"title":"keycloak 集成 spring security 实现角色、权限控制","text":"1. 概述本文主要讲述如何基于 keycloak 实现 rbac 以及 rbac0 模型。因为这两种对于大多数产品而言,已经足够用了。 二者区别简单来说就是,rbac 依赖于 user 和 role。rbac0 则多了一个权限,包含了 user、role 和 permission,permission 是对角色具有权限的进一步细化,属于多对多关系。 下面开始分别讲述实现方式 2. rbac实现的关键在于,权限系统中需要包含 user和role两个表。而 keycloak 中,这两个表自然存在,并且角色信息也已经能在 token 中返回。 因此,关键在于业务服务使用 spring security 后,需要进行鉴权。 这一步其实也很简单,只需要使用 @PreAuthorize 注解即可。如下: 12345@PreAuthorize("hasRole('radmin')")@GetMapping("/admin")public String adminApi() { return "this is admin api";} 对于 rainbow-cloud-dependencies 框架,权限表达式中的角色名只需要与 keycloak 中角色保持一致即可。 3. rbac0keycloak 中有了 user、role 表,但是并没有 permission 表,这就是比较头疼的一点,但也不是不能实现。 实现思路如下: 业务服务中,自建 permission 表,添加权限记录 通过 spi 调用 keycloak 接口,在 keycloak 的角色上添加 permission 属性 在 client 上添加 script 类型的 token mapper,将角色及其所有属性放到 token 中 业务服务中,自定义新的 spring security权限表达式,解析 token 进行鉴权 3.1 添加 permission 属性如下: 3.2 添加脚本 mapper首先 keycloak 需要开启此功能,此功能在 keycloak 18.0.0 版本已被移除,笔者使用的是 15.1.1 版本 开启方式为:添加 docker 环境变量 JAVA_OPTS_APPEND 12345678910111213141516171819version: '2.1'services: keycloak: image: jboss/keycloak:15.1.1 container_name: keycloak ports: - 8443:8443 volumes: - ./certs/xxx.pem:/etc/x509/https/tls.crt - ./certs/xxx.key:/etc/x509/https/tls.key environment: JDBC_PARAMS: "useSSL=false" JAVA_OPTS_APPEND: "-Dkeycloak.profile.feature.upload_scripts=enabled" networks: - keycloak-networknetworks: keycloak-network: 在对应认证的 client 中,添加 mapper: js 代码如下: 123456789101112131415var roles = [];var client = keycloakSession.getContext().getClient();user.getRoleMappings().forEach(function(roleModel) { var attr = {}; var names = {}; var rn = roleModel.getName(); var map = roleModel.getAttributes(); map.forEach(function(key, value){ attr[key] = value; }); roles[rn] = attr;});exports = roles; 3.3 自定义 spring security 权限表达式rainbow-cloud-dependencies 中已经完成实现,对应类为:RolePermissionSecurityExpressionRoot 新版本 idea 不仅能识别 spring 自带权限表达式,也能识别自定义的权限表达式了。","link":"/2023/01/05/keycloak-%E9%9B%86%E6%88%90-spring-security-%E5%AE%9E%E7%8E%B0%E8%A7%92%E8%89%B2%E3%80%81%E6%9D%83%E9%99%90%E6%8E%A7%E5%88%B6/"},{"title":"code runner 使用 conda","text":"1 概述本文主要讲述,windows 环境下,在 vscode 中, code runner 如何使用 conda 的环境,Mac OS 环境下,code runner 能自动识别,就不再赘述。 2 具体操作安装好 conda 后,要求打开终端,能够自动看到 base 环境。如果没有,可以使用 conda init 命令对 shell 环境进行初始化,这样,对于该 shell 将会成功获取到 默认的 base 环境。 之后,直接在终端输入 code 命令,将会直接打开 vscode,这样 code runner 就能直接使用 conda 的虚拟环境。因为,这个时候命令行中的环境变量已经传递到了 vscode。","link":"/2022/10/14/code-runner-%E4%BD%BF%E7%94%A8-conda/"},{"title":"micronaut 源码之 IoC 实现","text":"1 概述本文主要对 micronaut 框架中,IoC 实现的部分进行源码分析。 2 具体实现2.1 原理在分析源码之前,需要首先了解 micronaut 这个框架对于 IoC 实现的原理是什么,懂了原理,才能更有方向、更有目的性的去阅读源码。 在官方文档上,这一点实际上是有描述的:https://docs.micronaut.io/latest/guide/#how。翻译过来就是: 在这一点上,你可能想知道Micronaut是如何在不需要反射的情况下执行上述依赖注入的。 关键是一组AST转换(对于Groovy)和注解处理器(对于Java),它们生成了实现BeanDefinition接口的类。 Micronaut使用ASM字节码库来生成类,由于Micronaut提前知道了注入点,所以不需要像Spring等其他框架那样在运行时扫描所有的方法、字段、构造函数等等。 另外,由于在构造Bean时没有使用反射,JVM可以更好地内联和优化代码,从而获得更好的运行时性能和减少内存消耗。这对于非单体作用域来说特别重要,因为应用性能取决于Bean创建性能。 此外,使用Micronaut,你的应用程序启动时间和内存消耗不会像使用反射的框架那样受到代码库大小的影响。基于反射的IoC框架为你代码中的每一个字段、方法和构造函数加载和缓存反射数据。因此,随着你的代码大小的增长,你的内存需求也在增长,而Micronaut则不是这样的情况。 这段话很简洁,但也包含了所有的核心逻辑,接下来我们就按照原文来一步步对应源码进行解释原文真正的含义。 2.2 自动生成类首先,micronaut 相比于 spring 的 IoC,不同的一点是,他不需要使用反射去得到 bean 的相关信息,来生成 BeanDefinition。 BeanDefinition 是 Spring 框架中,一个核心的接口,micronaut 也有同样意义的接口。 spring 的做法是,对于当前工程,通过反射扫描启动类同级以及下属所有包下的所有类,判断这些类上是否有与生成 bean 相关的注解。如果有,那么则解析它,得到一个 BeanDefinition。 这样做,有两个问题: 反射本身的效率问题 内存占用 对于 java17 后,云原生编译的问题 因此,为了解析这几个问题,micronaut 的做法是,对于这些标记为 bean 的类,直接为他们生成对应的 BeanDefinition 以及相关类,这样,框架直接就能知道那些类需要成为 bean。 那么,怎么为类生成相关类呢? 这就要是原文说的: 对于 Groovy,使用 AST 转换 对于 java,使用 annotation processor AST 转换可以简单理解为直接解析语法,生成类。并且这类语言跟 java 关系不大,可以暂且跳过,annotation processor 则是关键。 对于 annotation processor,网上有很多文章进行介绍,比如这篇:https://www.cnblogs.com/exmyth/p/11396503.html 实际上仅靠一个 annotation processor 是不行的,它只是提供了一个生成类的入口,并不提供具体实现生成类的方法。要生成类的方式有很多,比如使用 themeleaf 这些模板根据类模板,也是可以生成的。但是这样,太不灵活,而且性能也不太高。 因此,micronaut 使用了 ASM 字节码库,直接生成类的字节码文件,性能更快,也更灵活。 说到这儿,有些读者可能觉得这个技术的实现一定很难。其实不是,这两项技术,已经足够成熟。并且,将这两样技术融合到一起的工具类库也是有很多,比如著名的 lombok。 那么,micronaut 是怎么用的呢? 在 micronaut 的 inject-java 模块中,有这样一个文件: 文件内容为: 12345io.micronaut.annotation.processing.TypeElementVisitorProcessorio.micronaut.annotation.processing.AggregatingTypeElementVisitorProcessorio.micronaut.annotation.processing.PackageConfigurationInjectProcessorio.micronaut.annotation.processing.BeanDefinitionInjectProcessorio.micronaut.annotation.processing.ConfigurationMetadataProcessor 其中,关键的一个类就是 BeanDefinitionInjectProcessor。 它就是对 BeanDefinition 进行生成的入口,通过这个类,将会在工程编译的时候,对 bean 生成两个字节码文件:Definition 以及 DefinitionReference,如: 另外,还有一个 SPI 文件,micronaut 通过这个文件,就能够直接知道当前工程下有哪些 bean 可用。","link":"/2022/10/16/micronaut-%E6%BA%90%E7%A0%81%E4%B9%8B-IoC-%E5%AE%9E%E7%8E%B0/"},{"title":"okteto 远程开发 springboot 工程","text":"","link":"/2022/10/15/okteto-%E8%BF%9C%E7%A8%8B%E5%BC%80%E5%8F%91-springboot-%E5%B7%A5%E7%A8%8B/"},{"title":"rainbow cloud dependencies 框架 demo 项目","text":"项目地址https://github.com/rainbowechoes/framework-demo.git 该项目目前是私有项目,用于示范如何使用 rainbow-cloud-dependencies 框架","link":"/2023/01/05/rainbow-cloud-dependencies-%E6%A1%86%E6%9E%B6-demo-%E9%A1%B9%E7%9B%AE/"},{"title":"keycloak 的模板配置","text":"分享一个,统一了微信登录、支持 rbac0以及其他防爆破的一些配置的 keycloak realm 配置。配置内容为: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705{ "id": "face", "realm": "face", "notBefore": 0, "defaultSignatureAlgorithm": "RS256", "revokeRefreshToken": false, "refreshTokenMaxReuse": 0, "accessTokenLifespan": 7200, "accessTokenLifespanForImplicitFlow": 900, "ssoSessionIdleTimeout": 604800, "ssoSessionMaxLifespan": 36000, "ssoSessionIdleTimeoutRememberMe": 0, "ssoSessionMaxLifespanRememberMe": 0, "offlineSessionIdleTimeout": 2592000, "offlineSessionMaxLifespanEnabled": false, "offlineSessionMaxLifespan": 5184000, "clientSessionIdleTimeout": 0, "clientSessionMaxLifespan": 0, "clientOfflineSessionIdleTimeout": 0, "clientOfflineSessionMaxLifespan": 0, "accessCodeLifespan": 60, "accessCodeLifespanUserAction": 300, "accessCodeLifespanLogin": 1800, "actionTokenGeneratedByAdminLifespan": 43200, "actionTokenGeneratedByUserLifespan": 300, "oauth2DeviceCodeLifespan": 600, "oauth2DevicePollingInterval": 5, "enabled": true, "sslRequired": "external", "registrationAllowed": true, "registrationEmailAsUsername": false, "rememberMe": false, "verifyEmail": false, "loginWithEmailAllowed": true, "duplicateEmailsAllowed": false, "resetPasswordAllowed": true, "editUsernameAllowed": false, "bruteForceProtected": true, "permanentLockout": false, "maxFailureWaitSeconds": 900, "minimumQuickLoginWaitSeconds": 60, "waitIncrementSeconds": 60, "quickLoginCheckMilliSeconds": 1000, "maxDeltaTimeSeconds": 43200, "failureFactor": 5, "roles": { "realm": [ { "id": "cb012a8b-2cbf-409f-ab5e-4c98c3d65ffe", "name": "offline_access", "description": "${role_offline-access}", "composite": false, "clientRole": false, "containerId": "face", "attributes": {} }, { "id": "5dfb8c7f-e7f1-41c9-8195-f68eccfb1ac1", "name": "admin", "description": "admin 用户", "composite": false, "clientRole": false, "containerId": "face", "attributes": { "permissions": [ "add" ] } }, { "id": "d5e6e37d-b26b-4c8e-9f69-b537ae9553f2", "name": "uma_authorization", "description": "${role_uma_authorization}", "composite": false, "clientRole": false, "containerId": "face", "attributes": {} }, { "id": "6bd531b3-fda6-45c5-a16b-29e362eab525", "name": "default-roles-face", "description": "${role_default-roles}", "composite": true, "composites": { "realm": [ "offline_access", "uma_authorization" ], "client": { "account": [ "manage-account", "view-profile" ] } }, "clientRole": false, "containerId": "face", "attributes": {} } ], "client": { "realm-management": [ { "id": "e887fe2e-6c78-4c64-80ad-eece62d1195c", "name": "manage-authorization", "description": "${role_manage-authorization}", "composite": false, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "b8da9403-2644-41df-861d-78843b072789", "name": "realm-admin", "description": "${role_realm-admin}", "composite": true, "composites": { "client": { "realm-management": [ "manage-authorization", "view-users", "query-realms", "query-users", "query-clients", "view-identity-providers", "manage-identity-providers", "view-clients", "query-groups", "manage-clients", "create-client", "view-authorization", "manage-users", "manage-events", "impersonation", "view-events", "manage-realm", "view-realm" ] } }, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "b129d109-712b-4dab-9fa7-a9568979ba41", "name": "view-users", "description": "${role_view-users}", "composite": true, "composites": { "client": { "realm-management": [ "query-users", "query-groups" ] } }, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "f9d5907f-23f0-4ce3-b55b-837b08d49d72", "name": "query-realms", "description": "${role_query-realms}", "composite": false, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "a67cef16-1f4e-4a4a-ac72-00f9fe61214a", "name": "query-users", "description": "${role_query-users}", "composite": false, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "34339400-a694-420e-933c-797f566d1f09", "name": "query-clients", "description": "${role_query-clients}", "composite": false, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "8d7793b3-9bcc-427d-a0cf-d7306806cd59", "name": "view-identity-providers", "description": "${role_view-identity-providers}", "composite": false, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "97e0198b-b12b-4eea-8161-5c4c1078fc4c", "name": "manage-identity-providers", "description": "${role_manage-identity-providers}", "composite": false, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "f003c421-553b-4d21-9add-c3aaf80b8f6a", "name": "view-clients", "description": "${role_view-clients}", "composite": true, "composites": { "client": { "realm-management": [ "query-clients" ] } }, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "d1268f31-f18a-475d-b101-da5f22bfac22", "name": "manage-clients", "description": "${role_manage-clients}", "composite": false, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "92ec40c2-4da1-4c38-a7b8-b5c89320c894", "name": "query-groups", "description": "${role_query-groups}", "composite": false, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "4f978d3a-5f38-4c33-8c43-c857bcaf723a", "name": "create-client", "description": "${role_create-client}", "composite": false, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "9f714cd6-0a21-4cde-b183-5a730b54cc28", "name": "view-authorization", "description": "${role_view-authorization}", "composite": false, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "fb00a8d6-2fb4-459e-b047-6855c57ed63d", "name": "impersonation", "description": "${role_impersonation}", "composite": false, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "81da3506-9191-4ba2-98fc-82e02786c031", "name": "manage-events", "description": "${role_manage-events}", "composite": false, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "0825bac3-6ced-499d-99b8-24fa27d079f6", "name": "manage-users", "description": "${role_manage-users}", "composite": false, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "ddde9f8e-d81e-4bc0-a94d-81cf5a5db350", "name": "view-events", "description": "${role_view-events}", "composite": false, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "8ce6e0a0-d72b-4afd-830c-220a210fd2a8", "name": "manage-realm", "description": "${role_manage-realm}", "composite": false, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} }, { "id": "4860fe76-d610-4eab-952a-cf88651bc8fd", "name": "view-realm", "description": "${role_view-realm}", "composite": false, "clientRole": true, "containerId": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "attributes": {} } ], "anno": [], "security-admin-console": [], "service": [ { "id": "ee77d2b0-2d84-4057-912f-63ccfe1aaf16", "name": "SADMIN", "description": "admin 用户", "composite": false, "clientRole": true, "containerId": "97afe8ed-3260-4cde-871f-4aecbd174041", "attributes": {} } ], "admin-cli": [], "account-console": [], "broker": [ { "id": "b9e04e90-f216-44e5-9439-32935f07bb55", "name": "read-token", "description": "${role_read-token}", "composite": false, "clientRole": true, "containerId": "02cccb03-a9ef-4792-a246-270cc202c9d2", "attributes": {} } ], "account": [ { "id": "929d90b4-3ded-4c84-8c8e-4e7ce970010f", "name": "manage-account", "description": "${role_manage-account}", "composite": true, "composites": { "client": { "account": [ "manage-account-links" ] } }, "clientRole": true, "containerId": "e2541145-83ae-4d87-83e3-a1dcd8f4e027", "attributes": {} }, { "id": "0f297aff-889b-4179-967f-5d48d1d54098", "name": "manage-account-links", "description": "${role_manage-account-links}", "composite": false, "clientRole": true, "containerId": "e2541145-83ae-4d87-83e3-a1dcd8f4e027", "attributes": {} }, { "id": "5218855f-7e91-4dfb-a4fb-9e3721b2f2b2", "name": "delete-account", "description": "${role_delete-account}", "composite": false, "clientRole": true, "containerId": "e2541145-83ae-4d87-83e3-a1dcd8f4e027", "attributes": {} }, { "id": "d26640bd-f5bf-4337-baa2-f2817f1950e0", "name": "view-consent", "description": "${role_view-consent}", "composite": false, "clientRole": true, "containerId": "e2541145-83ae-4d87-83e3-a1dcd8f4e027", "attributes": {} }, { "id": "2708c4d4-f8f4-4b3f-9413-d9240cba1237", "name": "view-applications", "description": "${role_view-applications}", "composite": false, "clientRole": true, "containerId": "e2541145-83ae-4d87-83e3-a1dcd8f4e027", "attributes": {} }, { "id": "54733133-06c1-4957-9b59-31483d763ceb", "name": "manage-consent", "description": "${role_manage-consent}", "composite": true, "composites": { "client": { "account": [ "view-consent" ] } }, "clientRole": true, "containerId": "e2541145-83ae-4d87-83e3-a1dcd8f4e027", "attributes": {} }, { "id": "4a44257b-a3b3-4f56-b310-70a9e0bf965e", "name": "view-profile", "description": "${role_view-profile}", "composite": false, "clientRole": true, "containerId": "e2541145-83ae-4d87-83e3-a1dcd8f4e027", "attributes": {} } ], "open-front": [] } }, "groups": [], "defaultRole": { "id": "6bd531b3-fda6-45c5-a16b-29e362eab525", "name": "default-roles-face", "description": "${role_default-roles}", "composite": true, "clientRole": false, "containerId": "face" }, "requiredCredentials": [ "password" ], "otpPolicyType": "totp", "otpPolicyAlgorithm": "HmacSHA1", "otpPolicyInitialCounter": 0, "otpPolicyDigits": 6, "otpPolicyLookAheadWindow": 1, "otpPolicyPeriod": 30, "otpSupportedApplications": [ "FreeOTP", "Google Authenticator" ], "webAuthnPolicyRpEntityName": "keycloak", "webAuthnPolicySignatureAlgorithms": [ "ES256" ], "webAuthnPolicyRpId": "", "webAuthnPolicyAttestationConveyancePreference": "not specified", "webAuthnPolicyAuthenticatorAttachment": "not specified", "webAuthnPolicyRequireResidentKey": "not specified", "webAuthnPolicyUserVerificationRequirement": "not specified", "webAuthnPolicyCreateTimeout": 0, "webAuthnPolicyAvoidSameAuthenticatorRegister": false, "webAuthnPolicyAcceptableAaguids": [], "webAuthnPolicyPasswordlessRpEntityName": "keycloak", "webAuthnPolicyPasswordlessSignatureAlgorithms": [ "ES256" ], "webAuthnPolicyPasswordlessRpId": "", "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", "webAuthnPolicyPasswordlessCreateTimeout": 0, "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, "webAuthnPolicyPasswordlessAcceptableAaguids": [], "users": [ { "id": "c0f5a305-8180-4ad0-9ee6-c7051f84d30d", "createdTimestamp": 1667789794650, "username": "service-account-service", "enabled": true, "totp": false, "emailVerified": false, "serviceAccountClientId": "service", "disableableCredentialTypes": [], "requiredActions": [], "realmRoles": [ "default-roles-face" ], "clientRoles": { "realm-management": [ "manage-authorization", "realm-admin", "query-realms", "query-users", "query-clients", "manage-identity-providers", "query-groups", "manage-clients", "create-client", "manage-users", "manage-events", "impersonation", "manage-realm" ], "broker": [ "read-token" ], "account": [ "delete-account", "view-applications", "manage-consent" ] }, "notBefore": 0, "groups": [] } ], "scopeMappings": [ { "clientScope": "offline_access", "roles": [ "offline_access" ] } ], "clientScopeMappings": { "account": [ { "client": "account-console", "roles": [ "manage-account" ] } ] }, "clients": [ { "id": "e2541145-83ae-4d87-83e3-a1dcd8f4e027", "clientId": "account", "name": "${client_account}", "rootUrl": "${authBaseUrl}", "baseUrl": "/realms/face/account/", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "redirectUris": [ "/realms/face/account/*" ], "webOrigins": [], "notBefore": 0, "bearerOnly": false, "consentRequired": false, "standardFlowEnabled": true, "implicitFlowEnabled": false, "directAccessGrantsEnabled": false, "serviceAccountsEnabled": false, "publicClient": true, "frontchannelLogout": false, "protocol": "openid-connect", "attributes": {}, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": false, "nodeReRegistrationTimeout": 0, "defaultClientScopes": [ "web-origins", "profile", "roles", "email" ], "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] }, { "id": "bf228e7d-0ab7-41b8-8f5e-e4276bf43fef", "clientId": "account-console", "name": "${client_account-console}", "rootUrl": "${authBaseUrl}", "baseUrl": "/realms/face/account/", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "redirectUris": [ "/realms/face/account/*" ], "webOrigins": [], "notBefore": 0, "bearerOnly": false, "consentRequired": false, "standardFlowEnabled": true, "implicitFlowEnabled": false, "directAccessGrantsEnabled": false, "serviceAccountsEnabled": false, "publicClient": true, "frontchannelLogout": false, "protocol": "openid-connect", "attributes": { "pkce.code.challenge.method": "S256" }, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": false, "nodeReRegistrationTimeout": 0, "protocolMappers": [ { "id": "9b9ce5ff-c4cb-4b6e-af0b-f895dcea3a97", "name": "audience resolve", "protocol": "openid-connect", "protocolMapper": "oidc-audience-resolve-mapper", "consentRequired": false, "config": {} } ], "defaultClientScopes": [ "web-origins", "profile", "roles", "email" ], "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] }, { "id": "33df6690-f9b1-4fd5-837e-235d42a893bc", "clientId": "admin-cli", "name": "${client_admin-cli}", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "redirectUris": [], "webOrigins": [], "notBefore": 0, "bearerOnly": false, "consentRequired": false, "standardFlowEnabled": false, "implicitFlowEnabled": false, "directAccessGrantsEnabled": true, "serviceAccountsEnabled": false, "publicClient": true, "frontchannelLogout": false, "protocol": "openid-connect", "attributes": {}, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": false, "nodeReRegistrationTimeout": 0, "defaultClientScopes": [ "web-origins", "profile", "roles", "email" ], "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] }, { "id": "79f76ce7-1c3e-4f54-9e73-d8a9ba717ae1", "clientId": "anno", "description": "自动标注平台前端", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "redirectUris": [ "*" ], "webOrigins": [ "*" ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, "standardFlowEnabled": true, "implicitFlowEnabled": false, "directAccessGrantsEnabled": true, "serviceAccountsEnabled": false, "publicClient": true, "frontchannelLogout": false, "protocol": "openid-connect", "attributes": { "id.token.as.detached.signature": "false", "saml.assertion.signature": "false", "saml.force.post.binding": "false", "saml.multivalued.roles": "false", "saml.encrypt": "false", "oauth2.device.authorization.grant.enabled": "false", "backchannel.logout.revoke.offline.tokens": "false", "saml.server.signature": "false", "saml.server.signature.keyinfo.ext": "false", "use.refresh.tokens": "true", "exclude.session.state.from.auth.response": "false", "oidc.ciba.grant.enabled": "false", "saml.artifact.binding": "false", "backchannel.logout.session.required": "true", "client_credentials.use_refresh_token": "false", "saml_force_name_id_format": "false", "require.pushed.authorization.requests": "false", "saml.client.signature": "false", "tls.client.certificate.bound.access.tokens": "false", "saml.authnstatement": "false", "display.on.consent.screen": "false", "saml.onetimeuse.condition": "false" }, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": true, "nodeReRegistrationTimeout": -1, "defaultClientScopes": [ "web-origins", "profile", "roles", "email" ], "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] }, { "id": "02cccb03-a9ef-4792-a246-270cc202c9d2", "clientId": "broker", "name": "${client_broker}", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "redirectUris": [], "webOrigins": [], "notBefore": 0, "bearerOnly": true, "consentRequired": false, "standardFlowEnabled": true, "implicitFlowEnabled": false, "directAccessGrantsEnabled": false, "serviceAccountsEnabled": false, "publicClient": false, "frontchannelLogout": false, "protocol": "openid-connect", "attributes": {}, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": false, "nodeReRegistrationTimeout": 0, "defaultClientScopes": [ "web-origins", "profile", "roles", "email" ], "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] }, { "id": "35b9746f-9e47-45b0-a555-e1f4395ec888", "clientId": "open-front", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "redirectUris": [ "*" ], "webOrigins": [], "notBefore": 0, "bearerOnly": false, "consentRequired": false, "standardFlowEnabled": true, "implicitFlowEnabled": false, "directAccessGrantsEnabled": true, "serviceAccountsEnabled": false, "publicClient": true, "frontchannelLogout": false, "protocol": "openid-connect", "attributes": { "id.token.as.detached.signature": "false", "saml.assertion.signature": "false", "saml.force.post.binding": "false", "saml.multivalued.roles": "false", "saml.encrypt": "false", "oauth2.device.authorization.grant.enabled": "false", "backchannel.logout.revoke.offline.tokens": "false", "saml.server.signature": "false", "saml.server.signature.keyinfo.ext": "false", "use.refresh.tokens": "true", "exclude.session.state.from.auth.response": "false", "oidc.ciba.grant.enabled": "false", "saml.artifact.binding": "false", "backchannel.logout.session.required": "true", "client_credentials.use_refresh_token": "false", "saml_force_name_id_format": "false", "require.pushed.authorization.requests": "false", "saml.client.signature": "false", "tls.client.certificate.bound.access.tokens": "false", "saml.authnstatement": "false", "display.on.consent.screen": "false", "saml.onetimeuse.condition": "false" }, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": true, "nodeReRegistrationTimeout": -1, "protocolMappers": [ { "id": "fe2b55d4-ffc6-441d-89a3-f3466b825200", "name": "avatar", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "avatar", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "avatar", "jsonType.label": "String" } }, { "id": "b85fa073-91ab-4c7e-870d-565690f0a267", "name": "role_attr", "protocol": "openid-connect", "protocolMapper": "oidc-script-based-protocol-mapper", "consentRequired": false, "config": { "multivalued": "true", "userinfo.token.claim": "true", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "permissions", "jsonType.label": "JSON", "script": "/**\\n * Available variables: \\n * user - the current user\\n * realm - the current realm\\n * token - the current token\\n * userSession - the current userSession\\n * keycloakSession - the current keycloakSession\\n */\\n\\n\\n//insert your code here...\\nvar roles = [];\\nvar client = keycloakSession.getContext().getClient();\\nuser.getRoleMappings().forEach(function(roleModel) {\\n \\n var attr = {};\\n var names = {};\\n var rn = roleModel.getName();\\n var map = roleModel.getAttributes();\\n map.forEach(function(key, value){\\n attr[key] = value;\\n }); \\n roles[rn] = attr;\\n});\\n\\nexports = roles;" } } ], "defaultClientScopes": [ "web-origins", "profile", "roles", "email" ], "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] }, { "id": "7d60d9d1-dc1a-45c0-a685-0260afabe4b5", "clientId": "realm-management", "name": "${client_realm-management}", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "redirectUris": [], "webOrigins": [], "notBefore": 0, "bearerOnly": true, "consentRequired": false, "standardFlowEnabled": true, "implicitFlowEnabled": false, "directAccessGrantsEnabled": false, "serviceAccountsEnabled": false, "publicClient": false, "frontchannelLogout": false, "protocol": "openid-connect", "attributes": {}, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": false, "nodeReRegistrationTimeout": 0, "defaultClientScopes": [ "web-origins", "profile", "roles", "email" ], "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] }, { "id": "6047c2b1-ac57-47d9-879c-5a15af6be259", "clientId": "security-admin-console", "name": "${client_security-admin-console}", "rootUrl": "${authAdminUrl}", "baseUrl": "/admin/face/console/", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "redirectUris": [ "/admin/face/console/*" ], "webOrigins": [ "+" ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, "standardFlowEnabled": true, "implicitFlowEnabled": false, "directAccessGrantsEnabled": false, "serviceAccountsEnabled": false, "publicClient": true, "frontchannelLogout": false, "protocol": "openid-connect", "attributes": { "pkce.code.challenge.method": "S256" }, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": false, "nodeReRegistrationTimeout": 0, "protocolMappers": [ { "id": "d0ca9bb3-e9bb-4038-b2ae-85152d990956", "name": "locale", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "locale", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "locale", "jsonType.label": "String" } } ], "defaultClientScopes": [ "web-origins", "profile", "roles", "email" ], "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] }, { "id": "97afe8ed-3260-4cde-871f-4aecbd174041", "clientId": "service", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "secret": "**********", "redirectUris": [ "*" ], "webOrigins": [], "notBefore": 0, "bearerOnly": false, "consentRequired": false, "standardFlowEnabled": true, "implicitFlowEnabled": false, "directAccessGrantsEnabled": true, "serviceAccountsEnabled": true, "publicClient": false, "frontchannelLogout": false, "protocol": "openid-connect", "attributes": { "id.token.as.detached.signature": "false", "saml.assertion.signature": "false", "saml.force.post.binding": "false", "saml.multivalued.roles": "false", "saml.encrypt": "false", "oauth2.device.authorization.grant.enabled": "false", "backchannel.logout.revoke.offline.tokens": "false", "saml.server.signature": "false", "saml.server.signature.keyinfo.ext": "false", "use.refresh.tokens": "true", "exclude.session.state.from.auth.response": "false", "oidc.ciba.grant.enabled": "false", "saml.artifact.binding": "false", "backchannel.logout.session.required": "true", "client_credentials.use_refresh_token": "false", "saml_force_name_id_format": "false", "require.pushed.authorization.requests": "false", "saml.client.signature": "false", "tls.client.certificate.bound.access.tokens": "false", "saml.authnstatement": "false", "display.on.consent.screen": "false", "saml.onetimeuse.condition": "false" }, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": true, "nodeReRegistrationTimeout": -1, "protocolMappers": [ { "id": "3a9b1805-ba3f-4d2c-a108-269a02c3f7e2", "name": "Client IP Address", "protocol": "openid-connect", "protocolMapper": "oidc-usersessionmodel-note-mapper", "consentRequired": false, "config": { "user.session.note": "clientAddress", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "clientAddress", "jsonType.label": "String" } }, { "id": "365a67bb-6f47-4b1b-96fc-26eff835723b", "name": "Client Host", "protocol": "openid-connect", "protocolMapper": "oidc-usersessionmodel-note-mapper", "consentRequired": false, "config": { "user.session.note": "clientHost", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "clientHost", "jsonType.label": "String" } }, { "id": "b5c7453d-39fc-4cf3-8e8c-82e5ec55cfbf", "name": "Client ID", "protocol": "openid-connect", "protocolMapper": "oidc-usersessionmodel-note-mapper", "consentRequired": false, "config": { "user.session.note": "clientId", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "clientId", "jsonType.label": "String" } } ], "defaultClientScopes": [ "web-origins", "profile", "roles", "email" ], "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] } ], "clientScopes": [ { "id": "f81da97d-4e46-4a4e-8366-a678dd0415cc", "name": "phone", "description": "OpenID Connect built-in scope: phone", "protocol": "openid-connect", "attributes": { "include.in.token.scope": "true", "display.on.consent.screen": "true", "consent.screen.text": "${phoneScopeConsentText}" }, "protocolMappers": [ { "id": "b68fca07-f0b9-4b0f-b890-283bdc15471b", "name": "phone number", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "phoneNumber", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "phone_number", "jsonType.label": "String" } }, { "id": "b8301832-49a1-4763-a4cc-c1e7f2fcc335", "name": "phone number verified", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "phoneNumberVerified", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "phone_number_verified", "jsonType.label": "boolean" } } ] }, { "id": "88d67f19-f7c1-45d5-ad75-7c1a0cc8ede4", "name": "profile", "description": "OpenID Connect built-in scope: profile", "protocol": "openid-connect", "attributes": { "include.in.token.scope": "true", "display.on.consent.screen": "true", "consent.screen.text": "${profileScopeConsentText}" }, "protocolMappers": [ { "id": "96341031-7c6a-4a3c-b452-a02af3323b7f", "name": "picture", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "picture", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "picture", "jsonType.label": "String" } }, { "id": "b600b9c1-0467-4539-943f-0ae31296970d", "name": "full name", "protocol": "openid-connect", "protocolMapper": "oidc-full-name-mapper", "consentRequired": false, "config": { "id.token.claim": "true", "access.token.claim": "true", "userinfo.token.claim": "true" } }, { "id": "6d1c8d2e-533f-4b06-b348-02a3975f30fa", "name": "locale", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "locale", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "locale", "jsonType.label": "String" } }, { "id": "8ac5f497-5268-4a12-ab12-1a31c1521ae4", "name": "website", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "website", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "website", "jsonType.label": "String" } }, { "id": "5e951fa5-bad5-4422-96b7-fc8a9345774b", "name": "birthdate", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "birthdate", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "birthdate", "jsonType.label": "String" } }, { "id": "4112ba8c-8867-4753-8d7d-12b0f38efcb2", "name": "nickname", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "nickname", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "nickname", "jsonType.label": "String" } }, { "id": "4e081b21-047f-41ef-850a-d366927390ea", "name": "given name", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-property-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "firstName", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "given_name", "jsonType.label": "String" } }, { "id": "34baaf5d-6f70-41fc-9bb0-66d9a715c81e", "name": "updated at", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "updatedAt", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "updated_at", "jsonType.label": "String" } }, { "id": "8605b33b-08b5-468f-be81-40bf7f13ecfa", "name": "zoneinfo", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "zoneinfo", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "zoneinfo", "jsonType.label": "String" } }, { "id": "ba40094e-863f-42b9-a90b-4ad670977441", "name": "username", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-property-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "username", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "preferred_username", "jsonType.label": "String" } }, { "id": "0d9d9e02-bf58-400e-bf2c-dc0b81d4d66f", "name": "gender", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "gender", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "gender", "jsonType.label": "String" } }, { "id": "5eed6af3-169e-4593-aa80-93f0e825f5ee", "name": "middle name", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "middleName", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "middle_name", "jsonType.label": "String" } }, { "id": "f1baebe3-234e-475a-b81c-d5b48001084a", "name": "family name", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-property-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "lastName", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "family_name", "jsonType.label": "String" } }, { "id": "b938266e-2dbd-458a-ab3c-9226f6a6c597", "name": "profile", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "profile", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "profile", "jsonType.label": "String" } } ] }, { "id": "df98bbeb-e280-44e8-acac-c5b58f1485bb", "name": "roles", "description": "OpenID Connect scope for add user roles to the access token", "protocol": "openid-connect", "attributes": { "include.in.token.scope": "false", "display.on.consent.screen": "true", "consent.screen.text": "${rolesScopeConsentText}" }, "protocolMappers": [ { "id": "0bccd14c-3a34-49d2-ba99-647f1355a730", "name": "realm roles", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-realm-role-mapper", "consentRequired": false, "config": { "user.attribute": "foo", "access.token.claim": "true", "claim.name": "realm_access.roles", "jsonType.label": "String", "multivalued": "true" } }, { "id": "1b98f820-1f58-43e3-b6f5-ae9a6a223f63", "name": "client roles", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-client-role-mapper", "consentRequired": false, "config": { "user.attribute": "foo", "access.token.claim": "true", "claim.name": "resource_access.${client_id}.roles", "jsonType.label": "String", "multivalued": "true" } }, { "id": "b8dec23b-ff52-440c-be1e-316892677498", "name": "audience resolve", "protocol": "openid-connect", "protocolMapper": "oidc-audience-resolve-mapper", "consentRequired": false, "config": {} } ] }, { "id": "e400334b-efdc-46fe-a038-01e12eb957e8", "name": "email", "description": "OpenID Connect built-in scope: email", "protocol": "openid-connect", "attributes": { "include.in.token.scope": "true", "display.on.consent.screen": "true", "consent.screen.text": "${emailScopeConsentText}" }, "protocolMappers": [ { "id": "c02e0c31-8f84-46eb-9e58-251f0d29e3eb", "name": "email", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-property-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "email", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "email", "jsonType.label": "String" } }, { "id": "9ac058b6-f90b-4a1a-8305-136240724c96", "name": "email verified", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-property-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "emailVerified", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "email_verified", "jsonType.label": "boolean" } } ] }, { "id": "f0f7cf0a-389d-4916-8ab5-6ebf3ad05ac3", "name": "microprofile-jwt", "description": "Microprofile - JWT built-in scope", "protocol": "openid-connect", "attributes": { "include.in.token.scope": "true", "display.on.consent.screen": "false" }, "protocolMappers": [ { "id": "8dea6e06-a422-483f-8406-2d50b8500d4d", "name": "groups", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-realm-role-mapper", "consentRequired": false, "config": { "multivalued": "true", "user.attribute": "foo", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "groups", "jsonType.label": "String" } }, { "id": "7bca3289-7391-4670-90bd-30ec36fe52e6", "name": "upn", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-property-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "username", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "upn", "jsonType.label": "String" } } ] }, { "id": "60b345bc-3212-4cde-b2af-1c4775b2bc82", "name": "role_list", "description": "SAML role list", "protocol": "saml", "attributes": { "consent.screen.text": "${samlRoleListScopeConsentText}", "display.on.consent.screen": "true" }, "protocolMappers": [ { "id": "67c661ae-56cf-4c5c-93ef-115944e6c08c", "name": "role list", "protocol": "saml", "protocolMapper": "saml-role-list-mapper", "consentRequired": false, "config": { "single": "false", "attribute.nameformat": "Basic", "attribute.name": "Role" } } ] }, { "id": "61966b88-2b48-4441-9b44-ef1aa49bc1e3", "name": "address", "description": "OpenID Connect built-in scope: address", "protocol": "openid-connect", "attributes": { "include.in.token.scope": "true", "display.on.consent.screen": "true", "consent.screen.text": "${addressScopeConsentText}" }, "protocolMappers": [ { "id": "4a7a7b84-a6f2-4818-a1dd-69bfb4cb5a39", "name": "address", "protocol": "openid-connect", "protocolMapper": "oidc-address-mapper", "consentRequired": false, "config": { "user.attribute.formatted": "formatted", "user.attribute.country": "country", "user.attribute.postal_code": "postal_code", "userinfo.token.claim": "true", "user.attribute.street": "street", "id.token.claim": "true", "user.attribute.region": "region", "access.token.claim": "true", "user.attribute.locality": "locality" } } ] }, { "id": "70943452-0e72-4a68-aad5-75a1c7d66e6c", "name": "offline_access", "description": "OpenID Connect built-in scope: offline_access", "protocol": "openid-connect", "attributes": { "consent.screen.text": "${offlineAccessScopeConsentText}", "display.on.consent.screen": "true" } }, { "id": "75050293-99fa-4de3-94f2-8c4b729a2b87", "name": "web-origins", "description": "OpenID Connect scope for add allowed web origins to the access token", "protocol": "openid-connect", "attributes": { "include.in.token.scope": "false", "display.on.consent.screen": "false", "consent.screen.text": "" }, "protocolMappers": [ { "id": "088bfa54-1bb6-4401-846a-7ffcb9eed01b", "name": "allowed web origins", "protocol": "openid-connect", "protocolMapper": "oidc-allowed-origins-mapper", "consentRequired": false, "config": {} } ] } ], "defaultDefaultClientScopes": [ "role_list", "web-origins", "profile", "roles", "email" ], "defaultOptionalClientScopes": [ "address", "offline_access", "microprofile-jwt", "phone" ], "browserSecurityHeaders": { "contentSecurityPolicyReportOnly": "", "xContentTypeOptions": "nosniff", "xRobotsTag": "none", "xFrameOptions": "SAMEORIGIN", "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", "xXSSProtection": "1; mode=block", "strictTransportSecurity": "max-age=31536000; includeSubDomains" }, "smtpServer": { "password": "**********", "starttls": "", "auth": "true", "port": "465", "host": "smtp.qiye.aliyun.com", "from": "[email protected]", "ssl": "true", "user": "[email protected]" }, "loginTheme": "keycloak", "eventsEnabled": false, "eventsListeners": [ "jboss-logging" ], "enabledEventTypes": [], "adminEventsEnabled": false, "adminEventsDetailsEnabled": false, "identityProviders": [ { "alias": "wechat", "internalId": "e63c54b9-60df-44e5-bce6-5a6cd9f6ec79", "providerId": "wechat", "enabled": true, "updateProfileFirstLoginMode": "on", "trustEmail": false, "storeToken": false, "addReadTokenRoleOnCreate": false, "authenticateByDefault": false, "linkOnly": false, "firstBrokerLoginFlowAlias": "wechat code login", "config": { "syncMode": "IMPORT", "clientSecret": "**********", "clientId": "wx44bba0d905e03c29", "useJwksUrl": "true" } } ], "identityProviderMappers": [], "components": { "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ { "id": "d9f367f5-b1b3-4bcf-98c6-841dc17773de", "name": "Allowed Protocol Mapper Types", "providerId": "allowed-protocol-mappers", "subType": "anonymous", "subComponents": {}, "config": { "allowed-protocol-mapper-types": [ "oidc-address-mapper", "oidc-usermodel-attribute-mapper", "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-property-mapper", "saml-role-list-mapper" ] } }, { "id": "11a54008-e617-419e-82ce-b2068fe46617", "name": "Allowed Protocol Mapper Types", "providerId": "allowed-protocol-mappers", "subType": "authenticated", "subComponents": {}, "config": { "allowed-protocol-mapper-types": [ "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "saml-role-list-mapper", "saml-user-property-mapper", "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "oidc-address-mapper" ] } }, { "id": "eec09d86-f177-4f79-b951-0af48e65e02c", "name": "Consent Required", "providerId": "consent-required", "subType": "anonymous", "subComponents": {}, "config": {} }, { "id": "d2ac5391-79d8-4abc-a8ff-cf94ee99c4d0", "name": "Max Clients Limit", "providerId": "max-clients", "subType": "anonymous", "subComponents": {}, "config": { "max-clients": [ "200" ] } }, { "id": "c1ecf3b5-3ea7-4587-9457-331879d2333d", "name": "Trusted Hosts", "providerId": "trusted-hosts", "subType": "anonymous", "subComponents": {}, "config": { "host-sending-registration-request-must-match": [ "true" ], "client-uris-must-match": [ "true" ] } }, { "id": "c64deffb-ca6d-47c9-892f-d8180455b76e", "name": "Allowed Client Scopes", "providerId": "allowed-client-templates", "subType": "authenticated", "subComponents": {}, "config": { "allow-default-scopes": [ "true" ] } }, { "id": "67d27f12-396c-46aa-aa29-11d88a31e056", "name": "Allowed Client Scopes", "providerId": "allowed-client-templates", "subType": "anonymous", "subComponents": {}, "config": { "allow-default-scopes": [ "true" ] } }, { "id": "5846b5d6-1a07-4adf-ba59-7074d53b2182", "name": "Full Scope Disabled", "providerId": "scope", "subType": "anonymous", "subComponents": {}, "config": {} } ], "org.keycloak.keys.KeyProvider": [ { "id": "cba68539-8dbf-4860-a4de-4406eca2d3f2", "name": "hmac-generated", "providerId": "hmac-generated", "subComponents": {}, "config": { "priority": [ "100" ], "algorithm": [ "HS256" ] } }, { "id": "b292ba24-6767-4885-b181-adc1be5ec971", "name": "aes-generated", "providerId": "aes-generated", "subComponents": {}, "config": { "priority": [ "100" ] } }, { "id": "b9b940ef-3054-4259-8c16-a0cd85169c36", "name": "rsa-enc-generated", "providerId": "rsa-generated", "subComponents": {}, "config": { "priority": [ "100" ] } }, { "id": "f98d54dc-e95c-40c7-bae5-7ed39e586f00", "name": "rsa-generated", "providerId": "rsa-generated", "subComponents": {}, "config": { "priority": [ "100" ] } } ] }, "internationalizationEnabled": false, "supportedLocales": [ "" ], "authenticationFlows": [ { "id": "d013783b-fbad-4244-a092-7e280ae03299", "alias": "Account verification options", "description": "Method with which to verity the existing account", "providerId": "basic-flow", "topLevel": false, "builtIn": true, "authenticationExecutions": [ { "authenticator": "idp-email-verification", "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticatorFlow": true, "requirement": "ALTERNATIVE", "priority": 20, "flowAlias": "Verify Existing Account by Re-authentication", "userSetupAllowed": false, "autheticatorFlow": true } ] }, { "id": "ce212cbe-6826-4958-aa0e-2ac9e29ee42a", "alias": "Authentication Options", "description": "Authentication options.", "providerId": "basic-flow", "topLevel": false, "builtIn": true, "authenticationExecutions": [ { "authenticator": "basic-auth", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "basic-auth-otp", "authenticatorFlow": false, "requirement": "DISABLED", "priority": 20, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "auth-spnego", "authenticatorFlow": false, "requirement": "DISABLED", "priority": 30, "userSetupAllowed": false, "autheticatorFlow": false } ] }, { "id": "0fddeca4-72ec-4b2c-b311-660094b6de90", "alias": "Browser - Conditional OTP", "description": "Flow to determine if the OTP is required for the authentication", "providerId": "basic-flow", "topLevel": false, "builtIn": true, "authenticationExecutions": [ { "authenticator": "conditional-user-configured", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "auth-otp-form", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, "userSetupAllowed": false, "autheticatorFlow": false } ] }, { "id": "f4f6f94f-25b4-4d99-8d7c-0fc8891b99cc", "alias": "Copy of first broker login", "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "providerId": "basic-flow", "topLevel": true, "builtIn": false, "authenticationExecutions": [ { "authenticatorConfig": "review profile config", "authenticator": "idp-review-profile", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "idp-create-user-if-unique", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 21, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticatorFlow": true, "requirement": "CONDITIONAL", "priority": 22, "flowAlias": "create user or update", "userSetupAllowed": false, "autheticatorFlow": true } ] }, { "id": "2f765fb4-631e-49e1-a640-a9133efd1f74", "alias": "Copy of first broker login First broker login - Conditional OTP", "description": "Flow to determine if the OTP is required for the authentication", "providerId": "basic-flow", "topLevel": false, "builtIn": false, "authenticationExecutions": [ { "authenticator": "conditional-user-configured", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "auth-otp-form", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, "userSetupAllowed": false, "autheticatorFlow": false } ] }, { "id": "d1b48ce3-402e-4494-971e-20bff572b003", "alias": "Copy of first broker login Handle Existing Account", "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", "providerId": "basic-flow", "topLevel": false, "builtIn": false, "authenticationExecutions": [ { "authenticator": "idp-auto-link", "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false } ] }, { "id": "806704d3-c9d1-4e9e-9e75-b28f5ef06a6f", "alias": "Copy of first broker login Verify Existing Account by Re-authentication", "description": "Reauthentication of existing account", "providerId": "basic-flow", "topLevel": false, "builtIn": false, "authenticationExecutions": [ { "authenticator": "idp-username-password-form", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticatorFlow": true, "requirement": "CONDITIONAL", "priority": 20, "flowAlias": "Copy of first broker login First broker login - Conditional OTP", "userSetupAllowed": false, "autheticatorFlow": true } ] }, { "id": "bb7872f1-0e07-4fb7-89f6-59e53f4217d6", "alias": "Direct Grant - Conditional OTP", "description": "Flow to determine if the OTP is required for the authentication", "providerId": "basic-flow", "topLevel": false, "builtIn": true, "authenticationExecutions": [ { "authenticator": "conditional-user-configured", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "direct-grant-validate-otp", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, "userSetupAllowed": false, "autheticatorFlow": false } ] }, { "id": "99e59b16-11ea-47c2-a4e3-14edc3110623", "alias": "First broker login - Conditional OTP", "description": "Flow to determine if the OTP is required for the authentication", "providerId": "basic-flow", "topLevel": false, "builtIn": true, "authenticationExecutions": [ { "authenticator": "conditional-user-configured", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "auth-otp-form", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, "userSetupAllowed": false, "autheticatorFlow": false } ] }, { "id": "cb86a303-4f73-4e5a-bca7-3276983a0d90", "alias": "Handle Existing Account", "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", "providerId": "basic-flow", "topLevel": false, "builtIn": true, "authenticationExecutions": [ { "authenticator": "idp-confirm-link", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticatorFlow": true, "requirement": "REQUIRED", "priority": 20, "flowAlias": "Account verification options", "userSetupAllowed": false, "autheticatorFlow": true } ] }, { "id": "4f785f02-5b63-4c51-86bc-a5e12bcbef6e", "alias": "Reset - Conditional OTP", "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId": "basic-flow", "topLevel": false, "builtIn": true, "authenticationExecutions": [ { "authenticator": "conditional-user-configured", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "reset-otp", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, "userSetupAllowed": false, "autheticatorFlow": false } ] }, { "id": "3b191a54-6de2-4da4-a9c0-9535d1899adb", "alias": "User creation or linking", "description": "Flow for the existing/non-existing user alternatives", "providerId": "basic-flow", "topLevel": false, "builtIn": true, "authenticationExecutions": [ { "authenticatorConfig": "create unique user config", "authenticator": "idp-create-user-if-unique", "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticatorFlow": true, "requirement": "ALTERNATIVE", "priority": 20, "flowAlias": "Handle Existing Account", "userSetupAllowed": false, "autheticatorFlow": true } ] }, { "id": "7ca2dc39-04cb-4677-8ddd-ff48ecd0518b", "alias": "Verify Existing Account by Re-authentication", "description": "Reauthentication of existing account", "providerId": "basic-flow", "topLevel": false, "builtIn": true, "authenticationExecutions": [ { "authenticator": "idp-username-password-form", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticatorFlow": true, "requirement": "CONDITIONAL", "priority": 20, "flowAlias": "First broker login - Conditional OTP", "userSetupAllowed": false, "autheticatorFlow": true } ] }, { "id": "3a9f8edc-0a8b-4e49-b256-481e11d3b317", "alias": "browser", "description": "browser based authentication", "providerId": "basic-flow", "topLevel": true, "builtIn": true, "authenticationExecutions": [ { "authenticator": "auth-cookie", "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "auth-spnego", "authenticatorFlow": false, "requirement": "DISABLED", "priority": 20, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "identity-provider-redirector", "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 25, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticatorFlow": true, "requirement": "ALTERNATIVE", "priority": 30, "flowAlias": "forms", "userSetupAllowed": false, "autheticatorFlow": true } ] }, { "id": "4b8d6283-0766-4982-99e3-d71950c35755", "alias": "clients", "description": "Base authentication for clients", "providerId": "client-flow", "topLevel": true, "builtIn": true, "authenticationExecutions": [ { "authenticator": "client-secret", "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "client-jwt", "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 20, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "client-secret-jwt", "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 30, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "client-x509", "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 40, "userSetupAllowed": false, "autheticatorFlow": false } ] }, { "id": "a266388c-39f4-4692-80d8-ab4c2aa06f79", "alias": "create user or update", "description": "", "providerId": "basic-flow", "topLevel": false, "builtIn": false, "authenticationExecutions": [ { "authenticator": "idp-detect-existing-broker-user", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 0, "userSetupAllowed": false, "autheticatorFlow": false } ] }, { "id": "72263036-da9c-4e9b-80af-be5364b52739", "alias": "direct grant", "description": "OpenID Connect Resource Owner Grant", "providerId": "basic-flow", "topLevel": true, "builtIn": true, "authenticationExecutions": [ { "authenticator": "direct-grant-validate-username", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "direct-grant-validate-password", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticatorFlow": true, "requirement": "CONDITIONAL", "priority": 30, "flowAlias": "Direct Grant - Conditional OTP", "userSetupAllowed": false, "autheticatorFlow": true } ] }, { "id": "8f21ffe4-38d7-4e48-82ce-41aaf06e53f3", "alias": "docker auth", "description": "Used by Docker clients to authenticate against the IDP", "providerId": "basic-flow", "topLevel": true, "builtIn": true, "authenticationExecutions": [ { "authenticator": "docker-http-basic-authenticator", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false } ] }, { "id": "25708194-bf09-444a-a120-4995d72d8fe6", "alias": "first broker login", "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "providerId": "basic-flow", "topLevel": true, "builtIn": true, "authenticationExecutions": [ { "authenticatorConfig": "review profile config", "authenticator": "idp-review-profile", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticatorFlow": true, "requirement": "REQUIRED", "priority": 20, "flowAlias": "User creation or linking", "userSetupAllowed": false, "autheticatorFlow": true } ] }, { "id": "88989059-7363-4ac8-b9f0-ee3438e0eba2", "alias": "first broker login 备份 Account verification options", "description": "Method with which to verity the existing account", "providerId": "basic-flow", "topLevel": false, "builtIn": false, "authenticationExecutions": [ { "authenticator": "idp-email-verification", "authenticatorFlow": false, "requirement": "DISABLED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticatorFlow": true, "requirement": "DISABLED", "priority": 20, "flowAlias": "first broker login 备份 Verify Existing Account by Re-authentication", "userSetupAllowed": false, "autheticatorFlow": true } ] }, { "id": "8ca867f7-8c29-41a1-a61a-319f0aa3ffb6", "alias": "first broker login 备份 First broker login - Conditional OTP", "description": "Flow to determine if the OTP is required for the authentication", "providerId": "basic-flow", "topLevel": false, "builtIn": false, "authenticationExecutions": [ { "authenticator": "conditional-user-configured", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "auth-otp-form", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, "userSetupAllowed": false, "autheticatorFlow": false } ] }, { "id": "78a91ba2-ccbe-4f50-8003-942956f6c769", "alias": "first broker login 备份 Handle Existing Account", "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", "providerId": "basic-flow", "topLevel": false, "builtIn": false, "authenticationExecutions": [ { "authenticator": "idp-confirm-link", "authenticatorFlow": false, "requirement": "DISABLED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticatorFlow": true, "requirement": "DISABLED", "priority": 20, "flowAlias": "first broker login 备份 Account verification options", "userSetupAllowed": false, "autheticatorFlow": true } ] }, { "id": "5c1d52ed-3b54-471a-bd7e-bd0a86026f4e", "alias": "first broker login 备份 Verify Existing Account by Re-authentication", "description": "Reauthentication of existing account", "providerId": "basic-flow", "topLevel": false, "builtIn": false, "authenticationExecutions": [ { "authenticator": "idp-username-password-form", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticatorFlow": true, "requirement": "CONDITIONAL", "priority": 20, "flowAlias": "first broker login 备份 First broker login - Conditional OTP", "userSetupAllowed": false, "autheticatorFlow": true } ] }, { "id": "fa97ed7c-5e7a-490f-8a28-77ce26c3adf1", "alias": "forms", "description": "Username, password, otp and other auth forms.", "providerId": "basic-flow", "topLevel": false, "builtIn": true, "authenticationExecutions": [ { "authenticator": "auth-username-password-form", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticatorFlow": true, "requirement": "CONDITIONAL", "priority": 20, "flowAlias": "Browser - Conditional OTP", "userSetupAllowed": false, "autheticatorFlow": true } ] }, { "id": "479d90a9-a35c-4b02-8387-c879f0399538", "alias": "http challenge", "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId": "basic-flow", "topLevel": true, "builtIn": true, "authenticationExecutions": [ { "authenticator": "no-cookie-redirect", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticatorFlow": true, "requirement": "REQUIRED", "priority": 20, "flowAlias": "Authentication Options", "userSetupAllowed": false, "autheticatorFlow": true } ] }, { "id": "97e67c80-26fe-4892-a9cc-340dba0ad945", "alias": "registration", "description": "registration flow", "providerId": "basic-flow", "topLevel": true, "builtIn": true, "authenticationExecutions": [ { "authenticator": "registration-page-form", "authenticatorFlow": true, "requirement": "REQUIRED", "priority": 10, "flowAlias": "registration form", "userSetupAllowed": false, "autheticatorFlow": true } ] }, { "id": "e8065f55-7b37-4deb-85a4-6e3dda9e69a9", "alias": "registration form", "description": "registration form", "providerId": "form-flow", "topLevel": false, "builtIn": true, "authenticationExecutions": [ { "authenticator": "registration-user-creation", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "registration-profile-action", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 40, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "registration-password-action", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 50, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "registration-recaptcha-action", "authenticatorFlow": false, "requirement": "DISABLED", "priority": 60, "userSetupAllowed": false, "autheticatorFlow": false } ] }, { "id": "e2ba04da-f6f3-44e1-a635-3856946ee80d", "alias": "reset credentials", "description": "Reset credentials for a user if they forgot their password or something", "providerId": "basic-flow", "topLevel": true, "builtIn": true, "authenticationExecutions": [ { "authenticator": "reset-credentials-choose-user", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "reset-credential-email", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticator": "reset-password", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 30, "userSetupAllowed": false, "autheticatorFlow": false }, { "authenticatorFlow": true, "requirement": "CONDITIONAL", "priority": 40, "flowAlias": "Reset - Conditional OTP", "userSetupAllowed": false, "autheticatorFlow": true } ] }, { "id": "68deb9e7-4612-42fc-b359-40a2690038c2", "alias": "saml ecp", "description": "SAML ECP Profile Authentication Flow", "providerId": "basic-flow", "topLevel": true, "builtIn": true, "authenticationExecutions": [ { "authenticator": "http-basic-authenticator", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, "userSetupAllowed": false, "autheticatorFlow": false } ] }, { "id": "f0bfa282-c929-4257-b637-c4bec906479d", "alias": "wechat code login", "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "providerId": "basic-flow", "topLevel": true, "builtIn": false, "authenticationExecutions": [ { "authenticator": "idp-own-create-user-if-unique", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 0, "userSetupAllowed": false, "autheticatorFlow": false } ] } ], "authenticatorConfig": [ { "id": "fc8767dd-af2e-48f5-bde6-42d5c2b13b5e", "alias": "create unique user config", "config": { "require.password.update.after.registration": "false" } }, { "id": "862e8a7d-88a9-442b-a4ca-af4d7aef8a36", "alias": "review profile config", "config": { "update.profile.on.first.login": "missing" } } ], "requiredActions": [ { "alias": "CONFIGURE_TOTP", "name": "Configure OTP", "providerId": "CONFIGURE_TOTP", "enabled": true, "defaultAction": false, "priority": 10, "config": {} }, { "alias": "terms_and_conditions", "name": "Terms and Conditions", "providerId": "terms_and_conditions", "enabled": false, "defaultAction": false, "priority": 20, "config": {} }, { "alias": "UPDATE_PASSWORD", "name": "Update Password", "providerId": "UPDATE_PASSWORD", "enabled": true, "defaultAction": false, "priority": 30, "config": {} }, { "alias": "UPDATE_PROFILE", "name": "Update Profile", "providerId": "UPDATE_PROFILE", "enabled": true, "defaultAction": false, "priority": 40, "config": {} }, { "alias": "VERIFY_EMAIL", "name": "Verify Email", "providerId": "VERIFY_EMAIL", "enabled": true, "defaultAction": false, "priority": 50, "config": {} }, { "alias": "delete_account", "name": "Delete Account", "providerId": "delete_account", "enabled": false, "defaultAction": false, "priority": 60, "config": {} }, { "alias": "update_user_locale", "name": "Update User Locale", "providerId": "update_user_locale", "enabled": true, "defaultAction": false, "priority": 1000, "config": {} } ], "browserFlow": "browser", "registrationFlow": "registration", "directGrantFlow": "direct grant", "resetCredentialsFlow": "reset credentials", "clientAuthenticationFlow": "clients", "dockerAuthenticationFlow": "docker auth", "attributes": { "cibaBackchannelTokenDeliveryMode": "poll", "cibaExpiresIn": "120", "cibaAuthRequestedUserHint": "login_hint", "oauth2DeviceCodeLifespan": "600", "clientOfflineSessionMaxLifespan": "0", "oauth2DevicePollingInterval": "5", "clientSessionIdleTimeout": "0", "parRequestUriLifespan": "60", "clientSessionMaxLifespan": "0", "clientOfflineSessionIdleTimeout": "0", "cibaInterval": "5" }, "keycloakVersion": "15.1.1", "userManagedAccessAllowed": false, "clientProfiles": { "profiles": [] }, "clientPolicies": { "policies": [] }}","link":"/2023/01/05/keycloak-%E7%9A%84%E6%A8%A1%E6%9D%BF%E9%85%8D%E7%BD%AE/"}],"tags":[{"name":"nginx","slug":"nginx","link":"/tags/nginx/"},{"name":"keycloak","slug":"keycloak","link":"/tags/keycloak/"},{"name":"jackson","slug":"jackson","link":"/tags/jackson/"},{"name":"rabbitmq","slug":"rabbitmq","link":"/tags/rabbitmq/"},{"name":"elasticsearch","slug":"elasticsearch","link":"/tags/elasticsearch/"},{"name":"spring","slug":"spring","link":"/tags/spring/"},{"name":"spring data","slug":"spring-data","link":"/tags/spring-data/"},{"name":"websocket","slug":"websocket","link":"/tags/websocket/"},{"name":"分布式","slug":"分布式","link":"/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"},{"name":"优化","slug":"优化","link":"/tags/%E4%BC%98%E5%8C%96/"},{"name":"spring cloud gateway","slug":"spring-cloud-gateway","link":"/tags/spring-cloud-gateway/"},{"name":"cors","slug":"cors","link":"/tags/cors/"},{"name":"spring cloud","slug":"spring-cloud","link":"/tags/spring-cloud/"},{"name":"k8s","slug":"k8s","link":"/tags/k8s/"},{"name":"token","slug":"token","link":"/tags/token/"},{"name":"前后端分离","slug":"前后端分离","link":"/tags/%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB/"},{"name":"动态路由","slug":"动态路由","link":"/tags/%E5%8A%A8%E6%80%81%E8%B7%AF%E7%94%B1/"},{"name":"注册中心","slug":"注册中心","link":"/tags/%E6%B3%A8%E5%86%8C%E4%B8%AD%E5%BF%83/"},{"name":"webflux","slug":"webflux","link":"/tags/webflux/"},{"name":"java","slug":"java","link":"/tags/java/"},{"name":"spring security","slug":"spring-security","link":"/tags/spring-security/"},{"name":"微信","slug":"微信","link":"/tags/%E5%BE%AE%E4%BF%A1/"},{"name":"微信小程序","slug":"微信小程序","link":"/tags/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/"},{"name":"微信统一登录","slug":"微信统一登录","link":"/tags/%E5%BE%AE%E4%BF%A1%E7%BB%9F%E4%B8%80%E7%99%BB%E5%BD%95/"},{"name":"stream 操作","slug":"stream-操作","link":"/tags/stream-%E6%93%8D%E4%BD%9C/"},{"name":"函数式编程","slug":"函数式编程","link":"/tags/%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B/"},{"name":"负载均衡 service","slug":"负载均衡-service","link":"/tags/%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1-service/"},{"name":"metallb","slug":"metallb","link":"/tags/metallb/"},{"name":"wsl2","slug":"wsl2","link":"/tags/wsl2/"},{"name":"clash","slug":"clash","link":"/tags/clash/"},{"name":"okteto","slug":"okteto","link":"/tags/okteto/"},{"name":"nfs","slug":"nfs","link":"/tags/nfs/"},{"name":"java17","slug":"java17","link":"/tags/java17/"},{"name":"反射","slug":"反射","link":"/tags/%E5%8F%8D%E5%B0%84/"},{"name":"conda","slug":"conda","link":"/tags/conda/"},{"name":"工具使用","slug":"工具使用","link":"/tags/%E5%B7%A5%E5%85%B7%E4%BD%BF%E7%94%A8/"},{"name":"micronaut","slug":"micronaut","link":"/tags/micronaut/"},{"name":"源码分析","slug":"源码分析","link":"/tags/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"},{"name":"wechat","slug":"wechat","link":"/tags/wechat/"},{"name":"rainbow-cloud-dependencies","slug":"rainbow-cloud-dependencies","link":"/tags/rainbow-cloud-dependencies/"}],"categories":[{"name":"后端开发","slug":"后端开发","link":"/categories/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/"},{"name":"运维","slug":"运维","link":"/categories/%E8%BF%90%E7%BB%B4/"}]}